From 4afdfb7b51bdb44fa97ba0ca136a780e2587b44f Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Wed, 18 Jun 2025 12:30:53 +0200 Subject: [PATCH 1/3] feat: add coder_ai_task resource Signed-off-by: Danny Kopping --- provider/ai_task.go | 61 ++++++++++++++++++++++++++++++ provider/ai_task_test.go | 82 ++++++++++++++++++++++++++++++++++++++++ provider/provider.go | 1 + 3 files changed, 144 insertions(+) create mode 100644 provider/ai_task.go create mode 100644 provider/ai_task_test.go diff --git a/provider/ai_task.go b/provider/ai_task.go new file mode 100644 index 00000000..45984f64 --- /dev/null +++ b/provider/ai_task.go @@ -0,0 +1,61 @@ +package provider + +import ( + "context" + + "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" +) + +type AITask struct { + ID string `mapstructure:"id"` + SidebarApp []AITaskSidebarApp `mapstructure:"sidebar_app"` +} + +type AITaskSidebarApp struct { + ID string `mapstructure:"id"` +} + +// TaskPromptParameterName is the name of the parameter which is *required* to be defined when a coder_ai_task is used. +const TaskPromptParameterName = "task_prompt" + +func aiTask() *schema.Resource { + return &schema.Resource{ + SchemaVersion: 1, + + Description: "Use this resource to define Coder tasks.", // TODO: docs link. + CreateContext: func(c context.Context, resourceData *schema.ResourceData, i any) diag.Diagnostics { + resourceData.SetId(uuid.NewString()) + return nil + }, + ReadContext: schema.NoopContext, + DeleteContext: schema.NoopContext, + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Description: "A unique identifier for this resource.", + Computed: true, + }, + "sidebar_app": { + Type: schema.TypeSet, + Description: "The coder_app to display in the sidebar.", // TODO: need some clear guidance on what type of app to use here. + ForceNew: true, + Required: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Description: "A reference to an existing `coder_app` resource in your template.", + Required: true, + ForceNew: true, + ValidateFunc: validation.IsUUID, + }, + }, + }, + }, + }, + } +} diff --git a/provider/ai_task_test.go b/provider/ai_task_test.go new file mode 100644 index 00000000..5f7a8a49 --- /dev/null +++ b/provider/ai_task_test.go @@ -0,0 +1,82 @@ +package provider_test + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/stretchr/testify/require" +) + +func TestAITask(t *testing.T) { + t.Parallel() + + t.Run("OK", func(t *testing.T) { + t.Parallel() + + resource.Test(t, resource.TestCase{ + ProviderFactories: coderFactory(), + IsUnitTest: true, + Steps: []resource.TestStep{{ + Config: ` + provider "coder" { + } + resource "coder_agent" "dev" { + os = "linux" + arch = "amd64" + } + 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" + } + resource "coder_ai_task" "test" { + sidebar_app { + id = coder_app.code-server.id + } + } + `, + 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", + "sidebar_app.#", + } { + value := resource.Primary.Attributes[key] + require.NotNil(t, value) + require.Greater(t, len(value), 0) + } + require.Equal(t, "1", resource.Primary.Attributes["sidebar_app.#"]) + return nil + }, + }}, + }) + }) + + t.Run("InvalidSidebarAppID", 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 = "not-a-uuid" + } + } + `, + ExpectError: regexp.MustCompile(`expected "sidebar_app.0.id" to be a valid UUID`), + }}, + }) + }) +} diff --git a/provider/provider.go b/provider/provider.go index cc2644ef..43e3a6ac 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -72,6 +72,7 @@ func New() *schema.Provider { ResourcesMap: map[string]*schema.Resource{ "coder_agent": agentResource(), "coder_agent_instance": agentInstanceResource(), + "coder_ai_task": aiTask(), "coder_app": appResource(), "coder_metadata": metadataResource(), "coder_script": scriptResource(), From d9b0f892f9c2b3768b9647b089900b0628cb7828 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Thu, 19 Jun 2025 11:35:24 +0200 Subject: [PATCH 2/3] chore: renaming prompt param name Signed-off-by: Danny Kopping --- provider/ai_task.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/provider/ai_task.go b/provider/ai_task.go index 45984f64..ce84cfc7 100644 --- a/provider/ai_task.go +++ b/provider/ai_task.go @@ -19,7 +19,7 @@ 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 = "task_prompt" +const TaskPromptParameterName = "AI Prompt" func aiTask() *schema.Resource { return &schema.Resource{ From d418ad9079d34417f1ef923e064a2e172f6c2cb0 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Thu, 19 Jun 2025 16:45:55 +0200 Subject: [PATCH 3/3] chore: address TODOs Signed-off-by: Danny Kopping --- docs/resources/ai_task.md | 31 +++++++++++++++++++++++++++++++ provider/ai_task.go | 4 ++-- 2 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 docs/resources/ai_task.md diff --git a/docs/resources/ai_task.md b/docs/resources/ai_task.md new file mode 100644 index 00000000..1922ef59 --- /dev/null +++ b/docs/resources/ai_task.md @@ -0,0 +1,31 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "coder_ai_task Resource - terraform-provider-coder" +subcategory: "" +description: |- + Use this resource to define Coder tasks. +--- + +# coder_ai_task (Resource) + +Use this resource to define Coder tasks. + + + + +## Schema + +### Required + +- `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)) + +### Read-Only + +- `id` (String) A unique identifier for this resource. + + +### Nested Schema for `sidebar_app` + +Required: + +- `id` (String) A reference to an existing `coder_app` resource in your template. diff --git a/provider/ai_task.go b/provider/ai_task.go index ce84cfc7..76b19f3c 100644 --- a/provider/ai_task.go +++ b/provider/ai_task.go @@ -25,7 +25,7 @@ func aiTask() *schema.Resource { return &schema.Resource{ SchemaVersion: 1, - Description: "Use this resource to define Coder tasks.", // TODO: docs link. + Description: "Use this resource to define Coder tasks.", CreateContext: func(c context.Context, resourceData *schema.ResourceData, i any) diag.Diagnostics { resourceData.SetId(uuid.NewString()) return nil @@ -40,7 +40,7 @@ func aiTask() *schema.Resource { }, "sidebar_app": { Type: schema.TypeSet, - Description: "The coder_app to display in the sidebar.", // TODO: need some clear guidance on what type of app to use here. + 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,