Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 63c1325

Browse files
feat(cli): add exp task create command (#19492)
Partially implements coder/internal#893 This isn't the full implementation of `coder exp tasks create` as defined in the issue, but it is the minimum required to create a task.
1 parent 73544a1 commit 63c1325

File tree

3 files changed

+355
-0
lines changed

3 files changed

+355
-0
lines changed

cli/exp_task.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ func (r *RootCmd) tasksCommand() *serpent.Command {
1414
},
1515
Children: []*serpent.Command{
1616
r.taskList(),
17+
r.taskCreate(),
1718
},
1819
}
1920
return cmd

cli/exp_taskcreate.go

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
package cli
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
"time"
7+
8+
"github.com/google/uuid"
9+
"golang.org/x/xerrors"
10+
11+
"github.com/coder/coder/v2/cli/cliui"
12+
"github.com/coder/coder/v2/codersdk"
13+
"github.com/coder/serpent"
14+
)
15+
16+
func (r *RootCmd) taskCreate() *serpent.Command {
17+
var (
18+
orgContext = NewOrganizationContext()
19+
client = new(codersdk.Client)
20+
21+
templateName string
22+
templateVersionName string
23+
presetName string
24+
taskInput string
25+
)
26+
27+
return &serpent.Command{
28+
Use: "create [template]",
29+
Short: "Create an experimental task",
30+
Middleware: serpent.Chain(
31+
serpent.RequireRangeArgs(0, 1),
32+
r.InitClient(client),
33+
),
34+
Options: serpent.OptionSet{
35+
{
36+
Flag: "input",
37+
Env: "CODER_TASK_INPUT",
38+
Value: serpent.StringOf(&taskInput),
39+
Required: true,
40+
},
41+
{
42+
Env: "CODER_TASK_TEMPLATE_NAME",
43+
Value: serpent.StringOf(&templateName),
44+
},
45+
{
46+
Env: "CODER_TASK_TEMPLATE_VERSION",
47+
Value: serpent.StringOf(&templateVersionName),
48+
},
49+
{
50+
Flag: "preset",
51+
Env: "CODER_TASK_PRESET_NAME",
52+
Value: serpent.StringOf(&presetName),
53+
Default: PresetNone,
54+
},
55+
},
56+
Handler: func(inv *serpent.Invocation) error {
57+
var (
58+
ctx = inv.Context()
59+
expClient = codersdk.NewExperimentalClient(client)
60+
61+
templateVersionID uuid.UUID
62+
templateVersionPresetID uuid.UUID
63+
)
64+
65+
organization, err := orgContext.Selected(inv, client)
66+
if err != nil {
67+
return xerrors.Errorf("get current organization: %w", err)
68+
}
69+
70+
if len(inv.Args) > 0 {
71+
templateName, templateVersionName, _ = strings.Cut(inv.Args[0], "@")
72+
}
73+
74+
if templateName == "" {
75+
return xerrors.Errorf("template name not provided")
76+
}
77+
78+
if templateVersionName != "" {
79+
templateVersion, err := client.TemplateVersionByOrganizationAndName(ctx, organization.ID, templateName, templateVersionName)
80+
if err != nil {
81+
return xerrors.Errorf("get template version: %w", err)
82+
}
83+
84+
templateVersionID = templateVersion.ID
85+
} else {
86+
template, err := client.TemplateByName(ctx, organization.ID, templateName)
87+
if err != nil {
88+
return xerrors.Errorf("get template: %w", err)
89+
}
90+
91+
templateVersionID = template.ActiveVersionID
92+
}
93+
94+
if presetName != PresetNone {
95+
templatePresets, err := client.TemplateVersionPresets(ctx, templateVersionID)
96+
if err != nil {
97+
return xerrors.Errorf("get template presets: %w", err)
98+
}
99+
100+
preset, err := resolvePreset(templatePresets, presetName)
101+
if err != nil {
102+
return xerrors.Errorf("resolve preset: %w", err)
103+
}
104+
105+
templateVersionPresetID = preset.ID
106+
}
107+
108+
workspace, err := expClient.CreateTask(ctx, codersdk.Me, codersdk.CreateTaskRequest{
109+
TemplateVersionID: templateVersionID,
110+
TemplateVersionPresetID: templateVersionPresetID,
111+
Prompt: taskInput,
112+
})
113+
if err != nil {
114+
return xerrors.Errorf("create task: %w", err)
115+
}
116+
117+
_, _ = fmt.Fprintf(
118+
inv.Stdout,
119+
"The task %s has been created at %s!\n",
120+
cliui.Keyword(workspace.Name),
121+
cliui.Timestamp(time.Now()),
122+
)
123+
124+
return nil
125+
},
126+
}
127+
}

cli/exp_taskcreate_test.go

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
package cli_test
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net/http"
7+
"net/http/httptest"
8+
"net/url"
9+
"strings"
10+
"testing"
11+
12+
"github.com/google/uuid"
13+
"github.com/stretchr/testify/assert"
14+
"github.com/stretchr/testify/require"
15+
16+
"github.com/coder/coder/v2/cli/clitest"
17+
"github.com/coder/coder/v2/cli/cliui"
18+
"github.com/coder/coder/v2/coderd/httpapi"
19+
"github.com/coder/coder/v2/codersdk"
20+
"github.com/coder/coder/v2/testutil"
21+
"github.com/coder/serpent"
22+
)
23+
24+
func TestTaskCreate(t *testing.T) {
25+
t.Parallel()
26+
27+
var (
28+
organizationID = uuid.New()
29+
templateID = uuid.New()
30+
templateVersionID = uuid.New()
31+
templateVersionPresetID = uuid.New()
32+
)
33+
34+
templateAndVersionFoundHandler := func(t *testing.T, ctx context.Context, templateName, templateVersionName, presetName, prompt string) http.HandlerFunc {
35+
t.Helper()
36+
37+
return func(w http.ResponseWriter, r *http.Request) {
38+
switch r.URL.Path {
39+
case "/api/v2/users/me/organizations":
40+
httpapi.Write(ctx, w, http.StatusOK, []codersdk.Organization{
41+
{MinimalOrganization: codersdk.MinimalOrganization{
42+
ID: organizationID,
43+
}},
44+
})
45+
case fmt.Sprintf("/api/v2/organizations/%s/templates/my-template/versions/my-template-version", organizationID):
46+
httpapi.Write(ctx, w, http.StatusOK, codersdk.TemplateVersion{
47+
ID: templateVersionID,
48+
})
49+
case fmt.Sprintf("/api/v2/organizations/%s/templates/my-template", organizationID):
50+
httpapi.Write(ctx, w, http.StatusOK, codersdk.Template{
51+
ID: templateID,
52+
ActiveVersionID: templateVersionID,
53+
})
54+
case fmt.Sprintf("/api/v2/templateversions/%s/presets", templateVersionID):
55+
httpapi.Write(ctx, w, http.StatusOK, []codersdk.Preset{
56+
{
57+
ID: templateVersionPresetID,
58+
Name: presetName,
59+
},
60+
})
61+
case "/api/experimental/tasks/me":
62+
var req codersdk.CreateTaskRequest
63+
if !httpapi.Read(ctx, w, r, &req) {
64+
return
65+
}
66+
67+
assert.Equal(t, prompt, req.Prompt, "prompt mismatch")
68+
assert.Equal(t, templateVersionID, req.TemplateVersionID, "template version mismatch")
69+
70+
if presetName == "" {
71+
assert.Equal(t, uuid.Nil, req.TemplateVersionPresetID, "expected no template preset id")
72+
} else {
73+
assert.Equal(t, templateVersionPresetID, req.TemplateVersionPresetID, "template version preset id mismatch")
74+
}
75+
76+
httpapi.Write(ctx, w, http.StatusCreated, codersdk.Workspace{
77+
Name: "task-wild-goldfish-27",
78+
})
79+
default:
80+
t.Errorf("unexpected path: %s", r.URL.Path)
81+
}
82+
}
83+
}
84+
85+
tests := []struct {
86+
args []string
87+
env []string
88+
expectError string
89+
expectOutput string
90+
handler func(t *testing.T, ctx context.Context) http.HandlerFunc
91+
}{
92+
{
93+
args: []string{"my-template@my-template-version", "--input", "my custom prompt"},
94+
expectOutput: fmt.Sprintf("The task %s has been created", cliui.Keyword("task-wild-goldfish-27")),
95+
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
96+
return templateAndVersionFoundHandler(t, ctx, "my-template", "my-template-version", "", "my custom prompt")
97+
},
98+
},
99+
{
100+
args: []string{"my-template", "--input", "my custom prompt"},
101+
env: []string{"CODER_TASK_TEMPLATE_VERSION=my-template-version"},
102+
expectOutput: fmt.Sprintf("The task %s has been created", cliui.Keyword("task-wild-goldfish-27")),
103+
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
104+
return templateAndVersionFoundHandler(t, ctx, "my-template", "my-template-version", "", "my custom prompt")
105+
},
106+
},
107+
{
108+
args: []string{"--input", "my custom prompt"},
109+
env: []string{"CODER_TASK_TEMPLATE_NAME=my-template", "CODER_TASK_TEMPLATE_VERSION=my-template-version"},
110+
expectOutput: fmt.Sprintf("The task %s has been created", cliui.Keyword("task-wild-goldfish-27")),
111+
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
112+
return templateAndVersionFoundHandler(t, ctx, "my-template", "my-template-version", "", "my custom prompt")
113+
},
114+
},
115+
{
116+
env: []string{"CODER_TASK_TEMPLATE_NAME=my-template", "CODER_TASK_TEMPLATE_VERSION=my-template-version", "CODER_TASK_INPUT=my custom prompt"},
117+
expectOutput: fmt.Sprintf("The task %s has been created", cliui.Keyword("task-wild-goldfish-27")),
118+
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
119+
return templateAndVersionFoundHandler(t, ctx, "my-template", "my-template-version", "", "my custom prompt")
120+
},
121+
},
122+
{
123+
args: []string{"my-template", "--input", "my custom prompt"},
124+
expectOutput: fmt.Sprintf("The task %s has been created", cliui.Keyword("task-wild-goldfish-27")),
125+
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
126+
return templateAndVersionFoundHandler(t, ctx, "my-template", "", "", "my custom prompt")
127+
},
128+
},
129+
{
130+
args: []string{"my-template", "--input", "my custom prompt", "--preset", "my-preset"},
131+
expectOutput: fmt.Sprintf("The task %s has been created", cliui.Keyword("task-wild-goldfish-27")),
132+
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
133+
return templateAndVersionFoundHandler(t, ctx, "my-template", "", "my-preset", "my custom prompt")
134+
},
135+
},
136+
{
137+
args: []string{"my-template", "--input", "my custom prompt"},
138+
env: []string{"CODER_TASK_PRESET_NAME=my-preset"},
139+
expectOutput: fmt.Sprintf("The task %s has been created", cliui.Keyword("task-wild-goldfish-27")),
140+
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
141+
return templateAndVersionFoundHandler(t, ctx, "my-template", "", "my-preset", "my custom prompt")
142+
},
143+
},
144+
{
145+
args: []string{"my-template", "--input", "my custom prompt", "--preset", "not-real-preset"},
146+
expectError: `preset "not-real-preset" not found`,
147+
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
148+
return templateAndVersionFoundHandler(t, ctx, "my-template", "", "my-preset", "my custom prompt")
149+
},
150+
},
151+
{
152+
args: []string{"my-template@not-real-template-version", "--input", "my custom prompt"},
153+
expectError: httpapi.ResourceNotFoundResponse.Message,
154+
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
155+
return func(w http.ResponseWriter, r *http.Request) {
156+
switch r.URL.Path {
157+
case "/api/v2/users/me/organizations":
158+
httpapi.Write(ctx, w, http.StatusOK, []codersdk.Organization{
159+
{MinimalOrganization: codersdk.MinimalOrganization{
160+
ID: organizationID,
161+
}},
162+
})
163+
case fmt.Sprintf("/api/v2/organizations/%s/templates/my-template/versions/not-real-template-version", organizationID):
164+
httpapi.ResourceNotFound(w)
165+
default:
166+
t.Errorf("unexpected path: %s", r.URL.Path)
167+
}
168+
}
169+
},
170+
},
171+
{
172+
args: []string{"not-real-template", "--input", "my custom prompt"},
173+
expectError: httpapi.ResourceNotFoundResponse.Message,
174+
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
175+
return func(w http.ResponseWriter, r *http.Request) {
176+
switch r.URL.Path {
177+
case "/api/v2/users/me/organizations":
178+
httpapi.Write(ctx, w, http.StatusOK, []codersdk.Organization{
179+
{MinimalOrganization: codersdk.MinimalOrganization{
180+
ID: organizationID,
181+
}},
182+
})
183+
case fmt.Sprintf("/api/v2/organizations/%s/templates/not-real-template", organizationID):
184+
httpapi.ResourceNotFound(w)
185+
default:
186+
t.Errorf("unexpected path: %s", r.URL.Path)
187+
}
188+
}
189+
},
190+
},
191+
}
192+
193+
for _, tt := range tests {
194+
t.Run(strings.Join(tt.args, ","), func(t *testing.T) {
195+
t.Parallel()
196+
197+
var (
198+
ctx = testutil.Context(t, testutil.WaitShort)
199+
srv = httptest.NewServer(tt.handler(t, ctx))
200+
client = new(codersdk.Client)
201+
args = []string{"exp", "task", "create"}
202+
sb strings.Builder
203+
err error
204+
)
205+
206+
t.Cleanup(srv.Close)
207+
208+
client.URL, err = url.Parse(srv.URL)
209+
require.NoError(t, err)
210+
211+
inv, root := clitest.New(t, append(args, tt.args...)...)
212+
inv.Environ = serpent.ParseEnviron(tt.env, "")
213+
inv.Stdout = &sb
214+
inv.Stderr = &sb
215+
clitest.SetupConfig(t, client, root)
216+
217+
err = inv.WithContext(ctx).Run()
218+
if tt.expectError == "" {
219+
assert.NoError(t, err)
220+
} else {
221+
assert.ErrorContains(t, err, tt.expectError)
222+
}
223+
224+
assert.Contains(t, sb.String(), tt.expectOutput)
225+
})
226+
}
227+
}

0 commit comments

Comments
 (0)