diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d52ff5cd..d7e1201d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,7 +28,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v6 with: - go-version: "1.24.2" + go-version: "1.24.6" id: go - name: Get dependencies @@ -86,7 +86,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v6 with: - go-version: "1.24.2" + go-version: "1.24.6" - name: Import GPG key id: import_gpg @@ -97,7 +97,7 @@ jobs: passphrase: ${{ secrets.PASSPHRASE }} - name: Run GoReleaser - uses: goreleaser/goreleaser-action@v6.3.0 + uses: goreleaser/goreleaser-action@v6.4.0 with: version: '~> v2' args: release --clean diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d93f59f7..1a909288 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -28,7 +28,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v6 with: - go-version: "1.24.2" + go-version: "1.24.6" id: go - name: Get dependencies @@ -104,7 +104,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v6 with: - go-version: "1.24.2" + go-version: "1.24.6" id: go - uses: hashicorp/setup-terraform@v3 @@ -137,7 +137,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v6 with: - go-version: "1.24.2" + go-version: "1.24.6" id: go - uses: hashicorp/setup-terraform@v3 diff --git a/docs/data-sources/task.md b/docs/data-sources/task.md new file mode 100644 index 00000000..43396eae --- /dev/null +++ b/docs/data-sources/task.md @@ -0,0 +1,43 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "coder_task Data Source - terraform-provider-coder" +subcategory: "" +description: |- + Use this data source to read information about Coder Tasks. +--- + +# coder_task (Data Source) + +Use this data source to read information about Coder Tasks. + +## Example Usage + +```terraform +provider "coder" {} + +data "coder_workspace" "me" {} +data "coder_task" "me" {} + +resource "coder_ai_task" "task" { + count = data.coder_task.me.enabled ? data.coder_workspace.me.start_count : 0 + app_id = module.example-agent.task_app_id +} + +module "example-agent" { + count = data.coder_task.me.enabled ? data.coder_workspace.me.start_count : 0 + prompt = data.coder_ai_task.me.prompt +} +``` + + +## Schema + +### Read-Only + +- `enabled` (Boolean) True when executing in a Coder Task context, false when in a Coder Workspace context. + + -> The `enabled` field is only populated in Coder v2.28 and later. +- `id` (String) The UUID of the task, if executing in a Coder Task context. Empty in a Coder Workspace context. +- `prompt` (String) The prompt text provided to the task by Coder, if executing in a Coder Task context. Empty in a Coder Workspace context. + + -> The `prompt` field is only populated in Coder v2.28 and later. diff --git a/docs/resources/ai_task.md b/docs/resources/ai_task.md index bd5cfcdd..3619c680 100644 --- a/docs/resources/ai_task.md +++ b/docs/resources/ai_task.md @@ -22,9 +22,14 @@ Use this resource to define Coder tasks. ### Read-Only +- `enabled` (Boolean) True when executing in a Coder Task context, false when in a Coder Workspace context. + + -> The `enabled` field is only populated in Coder v2.28 and later. - `id` (String) A unique identifier for this resource. - `prompt` (String) The prompt text provided to the task by Coder. + -> The `prompt` field is only populated in Coder v2.28 and later. + ### Nested Schema for `sidebar_app` diff --git a/examples/data-sources/coder_task/data-source.tf b/examples/data-sources/coder_task/data-source.tf new file mode 100644 index 00000000..af2098e1 --- /dev/null +++ b/examples/data-sources/coder_task/data-source.tf @@ -0,0 +1,14 @@ +provider "coder" {} + +data "coder_workspace" "me" {} +data "coder_task" "me" {} + +resource "coder_ai_task" "task" { + count = data.coder_task.me.enabled ? data.coder_workspace.me.start_count : 0 + app_id = module.example-agent.task_app_id +} + +module "example-agent" { + count = data.coder_task.me.enabled ? data.coder_workspace.me.start_count : 0 + prompt = data.coder_ai_task.me.prompt +} diff --git a/go.mod b/go.mod index bee6e849..11bec153 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/coder/terraform-provider-coder/v2 -go 1.24.2 +go 1.24.6 require ( github.com/docker/docker v26.1.5+incompatible diff --git a/integration/coder-ai-task/main.tf b/integration/coder-ai-task/main.tf index 263d7298..50e5289d 100644 --- a/integration/coder-ai-task/main.tf +++ b/integration/coder-ai-task/main.tf @@ -32,6 +32,8 @@ data "coder_parameter" "ai_prompt" { mutable = true } +data "coder_task" "me" {} + resource "coder_ai_task" "task" { sidebar_app { id = coder_app.ai_interface.id @@ -41,10 +43,15 @@ resource "coder_ai_task" "task" { locals { # NOTE: these must all be strings in the output output = { - "ai_task.id" = coder_ai_task.task.id - "ai_task.app_id" = coder_ai_task.task.app_id - "ai_task.prompt" = coder_ai_task.task.prompt - "app.id" = coder_app.ai_interface.id + "ai_task.id" = coder_ai_task.task.id + "ai_task.app_id" = coder_ai_task.task.app_id + "ai_task.prompt" = coder_ai_task.task.prompt + "ai_task.enabled" = tostring(coder_ai_task.task.enabled) + "app.id" = coder_app.ai_interface.id + + "task.id" = data.coder_task.me.id + "task.prompt" = data.coder_task.me.prompt + "task.enabled" = tostring(data.coder_task.me.enabled) } } diff --git a/integration/integration_test.go b/integration/integration_test.go index 4ff35e06..bc6fac96 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -43,7 +43,7 @@ func TestIntegration(t *testing.T) { coderImg := os.Getenv("CODER_IMAGE") if coderImg == "" { - coderImg = "ghcr.io/coder/coder" + coderImg = "ghcr.io/coder/coder-preview" } coderVersion := os.Getenv("CODER_VERSION") @@ -215,14 +215,17 @@ func TestIntegration(t *testing.T) { name: "coder-ai-task", minVersion: "v2.26.0", expectedOutput: map[string]string{ - "ai_task.id": `^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$`, - "ai_task.prompt": "default", - "ai_task.app_id": `^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$`, - "app.id": `^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$`, + "ai_task.id": `^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`, + "ai_task.prompt": "", + "ai_task.app_id": `^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`, + "ai_task.enabled": "false", + "app.id": `^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`, + "task.id": `^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`, + "task.prompt": "", + "task.enabled": "false", }, }, } { - tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() if coderVersion != "latest" && semver.Compare(coderVersion, tt.minVersion) < 0 { diff --git a/provider/ai_task.go b/provider/ai_task.go index 6ceda464..1bb6cf65 100644 --- a/provider/ai_task.go +++ b/provider/ai_task.go @@ -32,21 +32,16 @@ func aiTaskResource() *schema.Resource { CreateContext: func(c context.Context, resourceData *schema.ResourceData, i any) diag.Diagnostics { var diags diag.Diagnostics - if idStr := os.Getenv("CODER_TASK_ID"); idStr != "" { - resourceData.SetId(idStr) + if id, err := uuid.Parse(os.Getenv("CODER_TASK_ID")); err == nil && id != uuid.Nil { + resourceData.SetId(id.String()) + resourceData.Set("enabled", true) } else { resourceData.SetId(uuid.NewString()) - - diags = append(diags, diag.Diagnostic{ - Severity: diag.Warning, - Summary: "`CODER_TASK_ID` should be set. If you are seeing this message, the version of the Coder Terraform provider you are using is likely too new for your current Coder version.", - }) + resourceData.Set("enabled", false) } if prompt := os.Getenv("CODER_TASK_PROMPT"); prompt != "" { resourceData.Set("prompt", prompt) - } else { - resourceData.Set("prompt", "default") } var ( @@ -100,7 +95,7 @@ func aiTaskResource() *schema.Resource { }, "prompt": { Type: schema.TypeString, - Description: "The prompt text provided to the task by Coder.", + Description: "The prompt text provided to the task by Coder.\n\n -> The `prompt` field is only populated in Coder v2.28 and later.", Computed: true, }, "app_id": { @@ -112,6 +107,51 @@ func aiTaskResource() *schema.Resource { ValidateFunc: validation.IsUUID, ConflictsWith: []string{"sidebar_app"}, }, + "enabled": { + Type: schema.TypeBool, + Description: "True when executing in a Coder Task context, false when in a Coder Workspace context.\n\n -> The `enabled` field is only populated in Coder v2.28 and later.", + Computed: true, + }, + }, + } +} + +func taskDatasource() *schema.Resource { + return &schema.Resource{ + Description: "Use this data source to read information about Coder Tasks.", + ReadContext: func(ctx context.Context, rd *schema.ResourceData, i interface{}) diag.Diagnostics { + diags := diag.Diagnostics{} + + idStr := os.Getenv("CODER_TASK_ID") + if idStr == "" || idStr == uuid.Nil.String() { + rd.SetId(uuid.NewString()) + _ = rd.Set("enabled", false) + } else if _, err := uuid.Parse(idStr); err == nil { + rd.SetId(idStr) + _ = rd.Set("enabled", true) + } else { // invalid UUID + diags = append(diags, errorAsDiagnostics(err)...) + } + + _ = rd.Set("prompt", os.Getenv("CODER_TASK_PROMPT")) + return diags + }, + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Computed: true, + Description: "The UUID of the task, if executing in a Coder Task context. Empty in a Coder Workspace context.", + }, + "prompt": { + Type: schema.TypeString, + Computed: true, + Description: "The prompt text provided to the task by Coder, if executing in a Coder Task context. Empty in a Coder Workspace context.\n\n -> The `prompt` field is only populated in Coder v2.28 and later.", + }, + "enabled": { + Type: schema.TypeBool, + Computed: true, + Description: "True when executing in a Coder Task context, false when in a Coder Workspace context.\n\n -> The `enabled` field is only populated in Coder v2.28 and later.", + }, }, } } diff --git a/provider/ai_task_test.go b/provider/ai_task_test.go index d7357646..a67ba060 100644 --- a/provider/ai_task_test.go +++ b/provider/ai_task_test.go @@ -4,11 +4,68 @@ import ( "regexp" "testing" + "github.com/google/uuid" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" "github.com/stretchr/testify/require" ) +func TestAITask_Enabled(t *testing.T) { + t.Run("EnabledWhenTask", func(t *testing.T) { + t.Setenv("CODER_TASK_ID", "7d8d4c2e-fb57-44f9-a183-22509819c2e7") + + resource.Test(t, resource.TestCase{ + ProviderFactories: coderFactory(), + IsUnitTest: true, + Steps: []resource.TestStep{{ + Config: ` + provider "coder" { + } + 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) + + require.Equal(t, "true", resource.Primary.Attributes["enabled"]) + + return nil + }, + }}, + }) + }) + + t.Run("DisabledWhenWorkspace", func(t *testing.T) { + t.Setenv("CODER_TASK_ID", "") + + resource.Test(t, resource.TestCase{ + ProviderFactories: coderFactory(), + IsUnitTest: true, + Steps: []resource.TestStep{{ + Config: ` + provider "coder" { + } + 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) + + require.Equal(t, "false", resource.Primary.Attributes["enabled"]) + + return nil + }, + }}, + }) + }) +} + func TestAITask(t *testing.T) { t.Setenv("CODER_TASK_ID", "7d8d4c2e-fb57-44f9-a183-22509819c2e7") t.Setenv("CODER_TASK_PROMPT", "some task prompt") @@ -35,6 +92,7 @@ func TestAITask(t *testing.T) { "id", "prompt", "app_id", + "enabled", } { value := resource.Primary.Attributes[key] require.NotNil(t, value) @@ -97,6 +155,7 @@ func TestAITask(t *testing.T) { "id", "prompt", "app_id", + "enabled", } { value := resource.Primary.Attributes[key] require.NotNil(t, value) @@ -156,3 +215,83 @@ func TestAITask(t *testing.T) { }) }) } + +func TestTaskDatasource(t *testing.T) { + t.Run("Exists", func(t *testing.T) { + t.Setenv("CODER_TASK_ID", "7d8d4c2e-fb57-44f9-a183-22509819c2e7") + t.Setenv("CODER_TASK_PROMPT", "some task prompt") + resource.Test(t, resource.TestCase{ + ProviderFactories: coderFactory(), + IsUnitTest: true, + Steps: []resource.TestStep{{ + Config: ` + provider "coder" {} + data "coder_task" "me" {} + `, + Check: func(s *terraform.State) error { + require.Len(t, s.Modules, 1) + require.Len(t, s.Modules[0].Resources, 1) + resource := s.Modules[0].Resources["data.coder_task.me"] + require.NotNil(t, resource) + + taskID := resource.Primary.Attributes["id"] + require.Equal(t, "7d8d4c2e-fb57-44f9-a183-22509819c2e7", taskID) + + taskPromptValue := resource.Primary.Attributes["prompt"] + require.Equal(t, "some task prompt", taskPromptValue) + + enabledValue := resource.Primary.Attributes["enabled"] + require.Equal(t, "true", enabledValue) + return nil + }, + }}, + }) + }) + + t.Run("NotExists", func(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProviderFactories: coderFactory(), + IsUnitTest: true, + Steps: []resource.TestStep{{ + Config: ` + provider "coder" {} + data "coder_task" "me" {} + `, + Check: func(s *terraform.State) error { + require.Len(t, s.Modules, 1) + require.Len(t, s.Modules[0].Resources, 1) + resource := s.Modules[0].Resources["data.coder_task.me"] + require.NotNil(t, resource) + + taskID := resource.Primary.Attributes["id"] + require.NotEmpty(t, taskID) + require.NotEqual(t, uuid.Nil.String(), taskID) + _, err := uuid.Parse(taskID) + require.NoError(t, err) + + taskPromptValue := resource.Primary.Attributes["prompt"] + require.Empty(t, taskPromptValue) + + enabledValue := resource.Primary.Attributes["enabled"] + require.Equal(t, "false", enabledValue) + return nil + }, + }}, + }) + }) + + t.Run("InvalidTaskID", func(t *testing.T) { + t.Setenv("CODER_TASK_ID", "not a valid UUID") + resource.Test(t, resource.TestCase{ + ProviderFactories: coderFactory(), + IsUnitTest: true, + Steps: []resource.TestStep{{ + Config: ` + provider "coder" {} + data "coder_task" "me" {} + `, + ExpectError: regexp.MustCompile(`invalid UUID`), + }}, + }) + }) +} diff --git a/provider/helpers/validation.go b/provider/helpers/validation.go index 9cc21b89..e58a3b03 100644 --- a/provider/helpers/validation.go +++ b/provider/helpers/validation.go @@ -17,6 +17,6 @@ func ValidateURL(value any, label string) ([]string, []error) { if _, err := url.Parse(val); err != nil { return nil, []error{err} } - + return nil, nil } diff --git a/provider/provider.go b/provider/provider.go index 2b6409ba..7e4451b8 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -64,6 +64,7 @@ func New() *schema.Provider { "coder_external_auth": externalAuthDataSource(), "coder_workspace_owner": workspaceOwnerDataSource(), "coder_workspace_preset": workspacePresetDataSource(), + "coder_task": taskDatasource(), }, ResourcesMap: map[string]*schema.Resource{ "coder_agent": agentResource(), diff --git a/provider/provider_test.go b/provider/provider_test.go index 4bf98b32..606ae72b 100644 --- a/provider/provider_test.go +++ b/provider/provider_test.go @@ -37,7 +37,8 @@ func TestProviderEmpty(t *testing.T) { } data "coder_parameter" "param" { name = "hey" - }`, + } + data "coder_task" "me" {}`, Check: func(state *terraform.State) error { return nil }, diff --git a/provider/script_test.go b/provider/script_test.go index 64808372..8e6221f1 100644 --- a/provider/script_test.go +++ b/provider/script_test.go @@ -131,10 +131,10 @@ func TestValidateCronExpression(t *testing.T) { t.Parallel() tests := []struct { - name string - cronExpr string - expectWarnings bool - expectErrors bool + name string + cronExpr string + expectWarnings bool + expectErrors bool warningContains string }{ {