Skip to content

Commit c2c443b

Browse files
feat: add prompt and app_id fields to coder_ai_task (#445)
Closes coder/internal#977 Adds one required field `app_id`, one read-only computed field `prompt`, as well as deprecates the `sidebar_app` field, updates the `id` field to inherit the value from `CODER_TASK_ID` env variable.
1 parent 71c6d7d commit c2c443b

File tree

4 files changed

+151
-27
lines changed

4 files changed

+151
-27
lines changed

docs/resources/ai_task.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,15 @@ Use this resource to define Coder tasks.
1515
<!-- schema generated by tfplugindocs -->
1616
## Schema
1717

18-
### Required
18+
### Optional
1919

20-
- `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))
20+
- `app_id` (String) The ID of the `coder_app` resource that provides the AI interface for this task.
21+
- `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))
2122

2223
### Read-Only
2324

2425
- `id` (String) A unique identifier for this resource.
26+
- `prompt` (String) The prompt text provided to the task by Coder.
2527

2628
<a id="nestedblock--sidebar_app"></a>
2729
### Nested Schema for `sidebar_app`

provider/ai_task.go

Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ package provider
22

33
import (
44
"context"
5+
"os"
56

6-
"github.com/google/uuid"
77
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
88
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
99
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
@@ -21,13 +21,43 @@ type AITaskSidebarApp struct {
2121
// TaskPromptParameterName is the name of the parameter which is *required* to be defined when a coder_ai_task is used.
2222
const TaskPromptParameterName = "AI Prompt"
2323

24-
func aiTask() *schema.Resource {
24+
func aiTaskResource() *schema.Resource {
2525
return &schema.Resource{
2626
SchemaVersion: 1,
2727

2828
Description: "Use this resource to define Coder tasks.",
2929
CreateContext: func(c context.Context, resourceData *schema.ResourceData, i any) diag.Diagnostics {
30-
resourceData.SetId(uuid.NewString())
30+
if idStr := os.Getenv("CODER_TASK_ID"); idStr != "" {
31+
resourceData.SetId(idStr)
32+
} else {
33+
return diag.Errorf("CODER_TASK_ID must be set")
34+
}
35+
36+
if prompt := os.Getenv("CODER_TASK_PROMPT"); prompt != "" {
37+
resourceData.Set("prompt", prompt)
38+
} else {
39+
resourceData.Set("prompt", "default")
40+
}
41+
42+
var (
43+
appID = resourceData.Get("app_id").(string)
44+
sidebarAppSet = resourceData.Get("sidebar_app").(*schema.Set)
45+
)
46+
47+
if appID == "" && sidebarAppSet.Len() > 0 {
48+
sidebarApps := sidebarAppSet.List()
49+
sidebarApp := sidebarApps[0].(map[string]any)
50+
51+
if id, ok := sidebarApp["id"].(string); ok && id != "" {
52+
appID = id
53+
resourceData.Set("app_id", id)
54+
}
55+
}
56+
57+
if appID == "" {
58+
return diag.Errorf("'app_id' must be set")
59+
}
60+
3161
return nil
3262
},
3363
ReadContext: schema.NoopContext,
@@ -39,11 +69,13 @@ func aiTask() *schema.Resource {
3969
Computed: true,
4070
},
4171
"sidebar_app": {
42-
Type: schema.TypeSet,
43-
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.",
44-
ForceNew: true,
45-
Required: true,
46-
MaxItems: 1,
72+
Type: schema.TypeSet,
73+
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.",
74+
Deprecated: "This field has been deprecated in favor of the `app_id` field.",
75+
ForceNew: true,
76+
Optional: true,
77+
MaxItems: 1,
78+
ConflictsWith: []string{"app_id"},
4779
Elem: &schema.Resource{
4880
Schema: map[string]*schema.Schema{
4981
"id": {
@@ -56,6 +88,20 @@ func aiTask() *schema.Resource {
5688
},
5789
},
5890
},
91+
"prompt": {
92+
Type: schema.TypeString,
93+
Description: "The prompt text provided to the task by Coder.",
94+
Computed: true,
95+
},
96+
"app_id": {
97+
Type: schema.TypeString,
98+
Description: "The ID of the `coder_app` resource that provides the AI interface for this task.",
99+
ForceNew: true,
100+
Optional: true,
101+
Computed: true,
102+
ValidateFunc: validation.IsUUID,
103+
ConflictsWith: []string{"sidebar_app"},
104+
},
59105
},
60106
}
61107
}

provider/ai_task_test.go

Lines changed: 92 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import (
1010
)
1111

1212
func TestAITask(t *testing.T) {
13-
t.Parallel()
13+
t.Setenv("CODER_TASK_ID", "7d8d4c2e-fb57-44f9-a183-22509819c2e7")
14+
t.Setenv("CODER_TASK_PROMPT", "some task prompt")
1415

1516
t.Run("OK", func(t *testing.T) {
1617
t.Parallel()
@@ -22,44 +23,100 @@ func TestAITask(t *testing.T) {
2223
Config: `
2324
provider "coder" {
2425
}
25-
resource "coder_agent" "dev" {
26-
os = "linux"
27-
arch = "amd64"
26+
resource "coder_ai_task" "test" {
27+
app_id = "9a3ff7b4-4b3f-48c6-8d3a-a8118ac921fc"
28+
}
29+
`,
30+
Check: func(state *terraform.State) error {
31+
require.Len(t, state.Modules, 1)
32+
resource := state.Modules[0].Resources["coder_ai_task.test"]
33+
require.NotNil(t, resource)
34+
for _, key := range []string{
35+
"id",
36+
"prompt",
37+
"app_id",
38+
} {
39+
value := resource.Primary.Attributes[key]
40+
require.NotNil(t, value)
41+
require.Greater(t, len(value), 0)
42+
}
43+
44+
taskID := resource.Primary.Attributes["id"]
45+
require.Equal(t, "7d8d4c2e-fb57-44f9-a183-22509819c2e7", taskID)
46+
47+
taskPrompt := resource.Primary.Attributes["prompt"]
48+
require.Equal(t, "some task prompt", taskPrompt)
49+
50+
return nil
51+
},
52+
}},
53+
})
54+
})
55+
56+
t.Run("InvalidAppID", func(t *testing.T) {
57+
t.Parallel()
58+
59+
resource.Test(t, resource.TestCase{
60+
ProviderFactories: coderFactory(),
61+
IsUnitTest: true,
62+
Steps: []resource.TestStep{{
63+
Config: `
64+
provider "coder" {
65+
}
66+
resource "coder_ai_task" "test" {
67+
app_id = "not-a-uuid"
2868
}
29-
resource "coder_app" "code-server" {
30-
agent_id = coder_agent.dev.id
31-
slug = "code-server"
32-
display_name = "code-server"
33-
icon = "builtin:vim"
34-
url = "http://localhost:13337"
35-
open_in = "slim-window"
69+
`,
70+
ExpectError: regexp.MustCompile(`expected "app_id" to be a valid UUID`),
71+
}},
72+
})
73+
})
74+
75+
t.Run("DeprecatedSidebarApp", func(t *testing.T) {
76+
t.Parallel()
77+
78+
resource.Test(t, resource.TestCase{
79+
ProviderFactories: coderFactory(),
80+
IsUnitTest: true,
81+
Steps: []resource.TestStep{{
82+
Config: `
83+
provider "coder" {
3684
}
3785
resource "coder_ai_task" "test" {
3886
sidebar_app {
39-
id = coder_app.code-server.id
87+
id = "9a3ff7b4-4b3f-48c6-8d3a-a8118ac921fc"
4088
}
4189
}
4290
`,
4391
Check: func(state *terraform.State) error {
4492
require.Len(t, state.Modules, 1)
4593
resource := state.Modules[0].Resources["coder_ai_task.test"]
4694
require.NotNil(t, resource)
95+
4796
for _, key := range []string{
4897
"id",
49-
"sidebar_app.#",
98+
"prompt",
99+
"app_id",
50100
} {
51101
value := resource.Primary.Attributes[key]
52102
require.NotNil(t, value)
53103
require.Greater(t, len(value), 0)
54104
}
105+
55106
require.Equal(t, "1", resource.Primary.Attributes["sidebar_app.#"])
107+
sidebarAppID := resource.Primary.Attributes["sidebar_app.0.id"]
108+
require.Equal(t, "9a3ff7b4-4b3f-48c6-8d3a-a8118ac921fc", sidebarAppID)
109+
110+
actualAppID := resource.Primary.Attributes["app_id"]
111+
require.Equal(t, "9a3ff7b4-4b3f-48c6-8d3a-a8118ac921fc", actualAppID)
112+
56113
return nil
57114
},
58115
}},
59116
})
60117
})
61118

62-
t.Run("InvalidSidebarAppID", func(t *testing.T) {
119+
t.Run("ConflictingFields", func(t *testing.T) {
63120
t.Parallel()
64121

65122
resource.Test(t, resource.TestCase{
@@ -70,12 +127,31 @@ func TestAITask(t *testing.T) {
70127
provider "coder" {
71128
}
72129
resource "coder_ai_task" "test" {
130+
app_id = "9a3ff7b4-4b3f-48c6-8d3a-a8118ac921fc"
73131
sidebar_app {
74-
id = "not-a-uuid"
132+
id = "9a3ff7b4-4b3f-48c6-8d3a-a8118ac921fc"
75133
}
76134
}
77135
`,
78-
ExpectError: regexp.MustCompile(`expected "sidebar_app.0.id" to be a valid UUID`),
136+
ExpectError: regexp.MustCompile(`"app_id": conflicts with sidebar_app`),
137+
}},
138+
})
139+
})
140+
141+
t.Run("NoAppID", func(t *testing.T) {
142+
t.Parallel()
143+
144+
resource.Test(t, resource.TestCase{
145+
ProviderFactories: coderFactory(),
146+
IsUnitTest: true,
147+
Steps: []resource.TestStep{{
148+
Config: `
149+
provider "coder" {
150+
}
151+
resource "coder_ai_task" "test" {
152+
}
153+
`,
154+
ExpectError: regexp.MustCompile(`'app_id' must be set`),
79155
}},
80156
})
81157
})

provider/provider.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ func New() *schema.Provider {
6868
ResourcesMap: map[string]*schema.Resource{
6969
"coder_agent": agentResource(),
7070
"coder_agent_instance": agentInstanceResource(),
71-
"coder_ai_task": aiTask(),
71+
"coder_ai_task": aiTaskResource(),
7272
"coder_app": appResource(),
7373
"coder_metadata": metadataResource(),
7474
"coder_script": scriptResource(),

0 commit comments

Comments
 (0)