diff --git a/docs/resources/ai_task.md b/docs/resources/ai_task.md index 1922ef59..bd5cfcdd 100644 --- a/docs/resources/ai_task.md +++ b/docs/resources/ai_task.md @@ -15,13 +15,15 @@ Use this resource to define Coder tasks. ## Schema -### Required +### Optional -- `sidebar_app` (Block Set, Min: 1, Max: 1) The coder_app to display in the sidebar. Usually a chat interface with the AI agent running in the workspace, like https://github.com/coder/agentapi. (see [below for nested schema](#nestedblock--sidebar_app)) +- `app_id` (String) The ID of the `coder_app` resource that provides the AI interface for this task. +- `sidebar_app` (Block Set, Max: 1, Deprecated) The coder_app to display in the sidebar. Usually a chat interface with the AI agent running in the workspace, like https://github.com/coder/agentapi. (see [below for nested schema](#nestedblock--sidebar_app)) ### Read-Only - `id` (String) A unique identifier for this resource. +- `prompt` (String) The prompt text provided to the task by Coder. ### Nested Schema for `sidebar_app` diff --git a/provider/ai_task.go b/provider/ai_task.go index 76b19f3c..520eeb4a 100644 --- a/provider/ai_task.go +++ b/provider/ai_task.go @@ -2,8 +2,8 @@ package provider import ( "context" + "os" - "github.com/google/uuid" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" @@ -21,13 +21,43 @@ type AITaskSidebarApp struct { // TaskPromptParameterName is the name of the parameter which is *required* to be defined when a coder_ai_task is used. const TaskPromptParameterName = "AI Prompt" -func aiTask() *schema.Resource { +func aiTaskResource() *schema.Resource { return &schema.Resource{ SchemaVersion: 1, Description: "Use this resource to define Coder tasks.", CreateContext: func(c context.Context, resourceData *schema.ResourceData, i any) diag.Diagnostics { - resourceData.SetId(uuid.NewString()) + if idStr := os.Getenv("CODER_TASK_ID"); idStr != "" { + resourceData.SetId(idStr) + } else { + return diag.Errorf("CODER_TASK_ID must be set") + } + + if prompt := os.Getenv("CODER_TASK_PROMPT"); prompt != "" { + resourceData.Set("prompt", prompt) + } else { + resourceData.Set("prompt", "default") + } + + var ( + appID = resourceData.Get("app_id").(string) + sidebarAppSet = resourceData.Get("sidebar_app").(*schema.Set) + ) + + if appID == "" && sidebarAppSet.Len() > 0 { + sidebarApps := sidebarAppSet.List() + sidebarApp := sidebarApps[0].(map[string]any) + + if id, ok := sidebarApp["id"].(string); ok && id != "" { + appID = id + resourceData.Set("app_id", id) + } + } + + if appID == "" { + return diag.Errorf("'app_id' must be set") + } + return nil }, ReadContext: schema.NoopContext, @@ -39,11 +69,13 @@ func aiTask() *schema.Resource { Computed: true, }, "sidebar_app": { - Type: schema.TypeSet, - Description: "The coder_app to display in the sidebar. Usually a chat interface with the AI agent running in the workspace, like https://github.com/coder/agentapi.", - ForceNew: true, - Required: true, - MaxItems: 1, + Type: schema.TypeSet, + Description: "The coder_app to display in the sidebar. Usually a chat interface with the AI agent running in the workspace, like https://github.com/coder/agentapi.", + Deprecated: "This field has been deprecated in favor of the `app_id` field.", + ForceNew: true, + Optional: true, + MaxItems: 1, + ConflictsWith: []string{"app_id"}, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "id": { @@ -56,6 +88,20 @@ func aiTask() *schema.Resource { }, }, }, + "prompt": { + Type: schema.TypeString, + Description: "The prompt text provided to the task by Coder.", + Computed: true, + }, + "app_id": { + Type: schema.TypeString, + Description: "The ID of the `coder_app` resource that provides the AI interface for this task.", + ForceNew: true, + Optional: true, + Computed: true, + ValidateFunc: validation.IsUUID, + ConflictsWith: []string{"sidebar_app"}, + }, }, } } diff --git a/provider/ai_task_test.go b/provider/ai_task_test.go index 5f7a8a49..d7357646 100644 --- a/provider/ai_task_test.go +++ b/provider/ai_task_test.go @@ -10,7 +10,8 @@ import ( ) func TestAITask(t *testing.T) { - t.Parallel() + t.Setenv("CODER_TASK_ID", "7d8d4c2e-fb57-44f9-a183-22509819c2e7") + t.Setenv("CODER_TASK_PROMPT", "some task prompt") t.Run("OK", func(t *testing.T) { t.Parallel() @@ -22,21 +23,68 @@ func TestAITask(t *testing.T) { Config: ` provider "coder" { } - resource "coder_agent" "dev" { - os = "linux" - arch = "amd64" + resource "coder_ai_task" "test" { + app_id = "9a3ff7b4-4b3f-48c6-8d3a-a8118ac921fc" + } + `, + Check: func(state *terraform.State) error { + require.Len(t, state.Modules, 1) + resource := state.Modules[0].Resources["coder_ai_task.test"] + require.NotNil(t, resource) + for _, key := range []string{ + "id", + "prompt", + "app_id", + } { + value := resource.Primary.Attributes[key] + require.NotNil(t, value) + require.Greater(t, len(value), 0) + } + + taskID := resource.Primary.Attributes["id"] + require.Equal(t, "7d8d4c2e-fb57-44f9-a183-22509819c2e7", taskID) + + taskPrompt := resource.Primary.Attributes["prompt"] + require.Equal(t, "some task prompt", taskPrompt) + + return nil + }, + }}, + }) + }) + + t.Run("InvalidAppID", func(t *testing.T) { + t.Parallel() + + resource.Test(t, resource.TestCase{ + ProviderFactories: coderFactory(), + IsUnitTest: true, + Steps: []resource.TestStep{{ + Config: ` + provider "coder" { + } + resource "coder_ai_task" "test" { + app_id = "not-a-uuid" } - resource "coder_app" "code-server" { - agent_id = coder_agent.dev.id - slug = "code-server" - display_name = "code-server" - icon = "builtin:vim" - url = "http://localhost:13337" - open_in = "slim-window" + `, + ExpectError: regexp.MustCompile(`expected "app_id" to be a valid UUID`), + }}, + }) + }) + + t.Run("DeprecatedSidebarApp", func(t *testing.T) { + t.Parallel() + + resource.Test(t, resource.TestCase{ + ProviderFactories: coderFactory(), + IsUnitTest: true, + Steps: []resource.TestStep{{ + Config: ` + provider "coder" { } resource "coder_ai_task" "test" { sidebar_app { - id = coder_app.code-server.id + id = "9a3ff7b4-4b3f-48c6-8d3a-a8118ac921fc" } } `, @@ -44,22 +92,31 @@ func TestAITask(t *testing.T) { require.Len(t, state.Modules, 1) resource := state.Modules[0].Resources["coder_ai_task.test"] require.NotNil(t, resource) + for _, key := range []string{ "id", - "sidebar_app.#", + "prompt", + "app_id", } { value := resource.Primary.Attributes[key] require.NotNil(t, value) require.Greater(t, len(value), 0) } + require.Equal(t, "1", resource.Primary.Attributes["sidebar_app.#"]) + sidebarAppID := resource.Primary.Attributes["sidebar_app.0.id"] + require.Equal(t, "9a3ff7b4-4b3f-48c6-8d3a-a8118ac921fc", sidebarAppID) + + actualAppID := resource.Primary.Attributes["app_id"] + require.Equal(t, "9a3ff7b4-4b3f-48c6-8d3a-a8118ac921fc", actualAppID) + return nil }, }}, }) }) - t.Run("InvalidSidebarAppID", func(t *testing.T) { + t.Run("ConflictingFields", func(t *testing.T) { t.Parallel() resource.Test(t, resource.TestCase{ @@ -70,12 +127,31 @@ func TestAITask(t *testing.T) { provider "coder" { } resource "coder_ai_task" "test" { + app_id = "9a3ff7b4-4b3f-48c6-8d3a-a8118ac921fc" sidebar_app { - id = "not-a-uuid" + id = "9a3ff7b4-4b3f-48c6-8d3a-a8118ac921fc" } } `, - ExpectError: regexp.MustCompile(`expected "sidebar_app.0.id" to be a valid UUID`), + ExpectError: regexp.MustCompile(`"app_id": conflicts with sidebar_app`), + }}, + }) + }) + + t.Run("NoAppID", func(t *testing.T) { + t.Parallel() + + resource.Test(t, resource.TestCase{ + ProviderFactories: coderFactory(), + IsUnitTest: true, + Steps: []resource.TestStep{{ + Config: ` + provider "coder" { + } + resource "coder_ai_task" "test" { + } + `, + ExpectError: regexp.MustCompile(`'app_id' must be set`), }}, }) }) diff --git a/provider/provider.go b/provider/provider.go index 5a2f1972..2b6409ba 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -68,7 +68,7 @@ func New() *schema.Provider { ResourcesMap: map[string]*schema.Resource{ "coder_agent": agentResource(), "coder_agent_instance": agentInstanceResource(), - "coder_ai_task": aiTask(), + "coder_ai_task": aiTaskResource(), "coder_app": appResource(), "coder_metadata": metadataResource(), "coder_script": scriptResource(),