mirror of
https://github.com/coder/coder.git
synced 2026-09-22 05:05:20 +08:00
feat(cli): add exp task create command (#19492)
Partially implements https://github.com/coder/internal/issues/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.
This commit is contained in:
@@ -14,6 +14,7 @@ func (r *RootCmd) tasksCommand() *serpent.Command {
|
||||
},
|
||||
Children: []*serpent.Command{
|
||||
r.taskList(),
|
||||
r.taskCreate(),
|
||||
},
|
||||
}
|
||||
return cmd
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/v2/cli/cliui"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/serpent"
|
||||
)
|
||||
|
||||
func (r *RootCmd) taskCreate() *serpent.Command {
|
||||
var (
|
||||
orgContext = NewOrganizationContext()
|
||||
client = new(codersdk.Client)
|
||||
|
||||
templateName string
|
||||
templateVersionName string
|
||||
presetName string
|
||||
taskInput string
|
||||
)
|
||||
|
||||
return &serpent.Command{
|
||||
Use: "create [template]",
|
||||
Short: "Create an experimental task",
|
||||
Middleware: serpent.Chain(
|
||||
serpent.RequireRangeArgs(0, 1),
|
||||
r.InitClient(client),
|
||||
),
|
||||
Options: serpent.OptionSet{
|
||||
{
|
||||
Flag: "input",
|
||||
Env: "CODER_TASK_INPUT",
|
||||
Value: serpent.StringOf(&taskInput),
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Env: "CODER_TASK_TEMPLATE_NAME",
|
||||
Value: serpent.StringOf(&templateName),
|
||||
},
|
||||
{
|
||||
Env: "CODER_TASK_TEMPLATE_VERSION",
|
||||
Value: serpent.StringOf(&templateVersionName),
|
||||
},
|
||||
{
|
||||
Flag: "preset",
|
||||
Env: "CODER_TASK_PRESET_NAME",
|
||||
Value: serpent.StringOf(&presetName),
|
||||
Default: PresetNone,
|
||||
},
|
||||
},
|
||||
Handler: func(inv *serpent.Invocation) error {
|
||||
var (
|
||||
ctx = inv.Context()
|
||||
expClient = codersdk.NewExperimentalClient(client)
|
||||
|
||||
templateVersionID uuid.UUID
|
||||
templateVersionPresetID uuid.UUID
|
||||
)
|
||||
|
||||
organization, err := orgContext.Selected(inv, client)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get current organization: %w", err)
|
||||
}
|
||||
|
||||
if len(inv.Args) > 0 {
|
||||
templateName, templateVersionName, _ = strings.Cut(inv.Args[0], "@")
|
||||
}
|
||||
|
||||
if templateName == "" {
|
||||
return xerrors.Errorf("template name not provided")
|
||||
}
|
||||
|
||||
if templateVersionName != "" {
|
||||
templateVersion, err := client.TemplateVersionByOrganizationAndName(ctx, organization.ID, templateName, templateVersionName)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get template version: %w", err)
|
||||
}
|
||||
|
||||
templateVersionID = templateVersion.ID
|
||||
} else {
|
||||
template, err := client.TemplateByName(ctx, organization.ID, templateName)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get template: %w", err)
|
||||
}
|
||||
|
||||
templateVersionID = template.ActiveVersionID
|
||||
}
|
||||
|
||||
if presetName != PresetNone {
|
||||
templatePresets, err := client.TemplateVersionPresets(ctx, templateVersionID)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get template presets: %w", err)
|
||||
}
|
||||
|
||||
preset, err := resolvePreset(templatePresets, presetName)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("resolve preset: %w", err)
|
||||
}
|
||||
|
||||
templateVersionPresetID = preset.ID
|
||||
}
|
||||
|
||||
workspace, err := expClient.CreateTask(ctx, codersdk.Me, codersdk.CreateTaskRequest{
|
||||
TemplateVersionID: templateVersionID,
|
||||
TemplateVersionPresetID: templateVersionPresetID,
|
||||
Prompt: taskInput,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("create task: %w", err)
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintf(
|
||||
inv.Stdout,
|
||||
"The task %s has been created at %s!\n",
|
||||
cliui.Keyword(workspace.Name),
|
||||
cliui.Timestamp(time.Now()),
|
||||
)
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/v2/cli/clitest"
|
||||
"github.com/coder/coder/v2/cli/cliui"
|
||||
"github.com/coder/coder/v2/coderd/httpapi"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
"github.com/coder/serpent"
|
||||
)
|
||||
|
||||
func TestTaskCreate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
organizationID = uuid.New()
|
||||
templateID = uuid.New()
|
||||
templateVersionID = uuid.New()
|
||||
templateVersionPresetID = uuid.New()
|
||||
)
|
||||
|
||||
templateAndVersionFoundHandler := func(t *testing.T, ctx context.Context, templateName, templateVersionName, presetName, prompt string) http.HandlerFunc {
|
||||
t.Helper()
|
||||
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/api/v2/users/me/organizations":
|
||||
httpapi.Write(ctx, w, http.StatusOK, []codersdk.Organization{
|
||||
{MinimalOrganization: codersdk.MinimalOrganization{
|
||||
ID: organizationID,
|
||||
}},
|
||||
})
|
||||
case fmt.Sprintf("/api/v2/organizations/%s/templates/my-template/versions/my-template-version", organizationID):
|
||||
httpapi.Write(ctx, w, http.StatusOK, codersdk.TemplateVersion{
|
||||
ID: templateVersionID,
|
||||
})
|
||||
case fmt.Sprintf("/api/v2/organizations/%s/templates/my-template", organizationID):
|
||||
httpapi.Write(ctx, w, http.StatusOK, codersdk.Template{
|
||||
ID: templateID,
|
||||
ActiveVersionID: templateVersionID,
|
||||
})
|
||||
case fmt.Sprintf("/api/v2/templateversions/%s/presets", templateVersionID):
|
||||
httpapi.Write(ctx, w, http.StatusOK, []codersdk.Preset{
|
||||
{
|
||||
ID: templateVersionPresetID,
|
||||
Name: presetName,
|
||||
},
|
||||
})
|
||||
case "/api/experimental/tasks/me":
|
||||
var req codersdk.CreateTaskRequest
|
||||
if !httpapi.Read(ctx, w, r, &req) {
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(t, prompt, req.Prompt, "prompt mismatch")
|
||||
assert.Equal(t, templateVersionID, req.TemplateVersionID, "template version mismatch")
|
||||
|
||||
if presetName == "" {
|
||||
assert.Equal(t, uuid.Nil, req.TemplateVersionPresetID, "expected no template preset id")
|
||||
} else {
|
||||
assert.Equal(t, templateVersionPresetID, req.TemplateVersionPresetID, "template version preset id mismatch")
|
||||
}
|
||||
|
||||
httpapi.Write(ctx, w, http.StatusCreated, codersdk.Workspace{
|
||||
Name: "task-wild-goldfish-27",
|
||||
})
|
||||
default:
|
||||
t.Errorf("unexpected path: %s", r.URL.Path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
args []string
|
||||
env []string
|
||||
expectError string
|
||||
expectOutput string
|
||||
handler func(t *testing.T, ctx context.Context) http.HandlerFunc
|
||||
}{
|
||||
{
|
||||
args: []string{"my-template@my-template-version", "--input", "my custom prompt"},
|
||||
expectOutput: fmt.Sprintf("The task %s has been created", cliui.Keyword("task-wild-goldfish-27")),
|
||||
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
|
||||
return templateAndVersionFoundHandler(t, ctx, "my-template", "my-template-version", "", "my custom prompt")
|
||||
},
|
||||
},
|
||||
{
|
||||
args: []string{"my-template", "--input", "my custom prompt"},
|
||||
env: []string{"CODER_TASK_TEMPLATE_VERSION=my-template-version"},
|
||||
expectOutput: fmt.Sprintf("The task %s has been created", cliui.Keyword("task-wild-goldfish-27")),
|
||||
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
|
||||
return templateAndVersionFoundHandler(t, ctx, "my-template", "my-template-version", "", "my custom prompt")
|
||||
},
|
||||
},
|
||||
{
|
||||
args: []string{"--input", "my custom prompt"},
|
||||
env: []string{"CODER_TASK_TEMPLATE_NAME=my-template", "CODER_TASK_TEMPLATE_VERSION=my-template-version"},
|
||||
expectOutput: fmt.Sprintf("The task %s has been created", cliui.Keyword("task-wild-goldfish-27")),
|
||||
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
|
||||
return templateAndVersionFoundHandler(t, ctx, "my-template", "my-template-version", "", "my custom prompt")
|
||||
},
|
||||
},
|
||||
{
|
||||
env: []string{"CODER_TASK_TEMPLATE_NAME=my-template", "CODER_TASK_TEMPLATE_VERSION=my-template-version", "CODER_TASK_INPUT=my custom prompt"},
|
||||
expectOutput: fmt.Sprintf("The task %s has been created", cliui.Keyword("task-wild-goldfish-27")),
|
||||
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
|
||||
return templateAndVersionFoundHandler(t, ctx, "my-template", "my-template-version", "", "my custom prompt")
|
||||
},
|
||||
},
|
||||
{
|
||||
args: []string{"my-template", "--input", "my custom prompt"},
|
||||
expectOutput: fmt.Sprintf("The task %s has been created", cliui.Keyword("task-wild-goldfish-27")),
|
||||
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
|
||||
return templateAndVersionFoundHandler(t, ctx, "my-template", "", "", "my custom prompt")
|
||||
},
|
||||
},
|
||||
{
|
||||
args: []string{"my-template", "--input", "my custom prompt", "--preset", "my-preset"},
|
||||
expectOutput: fmt.Sprintf("The task %s has been created", cliui.Keyword("task-wild-goldfish-27")),
|
||||
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
|
||||
return templateAndVersionFoundHandler(t, ctx, "my-template", "", "my-preset", "my custom prompt")
|
||||
},
|
||||
},
|
||||
{
|
||||
args: []string{"my-template", "--input", "my custom prompt"},
|
||||
env: []string{"CODER_TASK_PRESET_NAME=my-preset"},
|
||||
expectOutput: fmt.Sprintf("The task %s has been created", cliui.Keyword("task-wild-goldfish-27")),
|
||||
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
|
||||
return templateAndVersionFoundHandler(t, ctx, "my-template", "", "my-preset", "my custom prompt")
|
||||
},
|
||||
},
|
||||
{
|
||||
args: []string{"my-template", "--input", "my custom prompt", "--preset", "not-real-preset"},
|
||||
expectError: `preset "not-real-preset" not found`,
|
||||
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
|
||||
return templateAndVersionFoundHandler(t, ctx, "my-template", "", "my-preset", "my custom prompt")
|
||||
},
|
||||
},
|
||||
{
|
||||
args: []string{"my-template@not-real-template-version", "--input", "my custom prompt"},
|
||||
expectError: httpapi.ResourceNotFoundResponse.Message,
|
||||
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/api/v2/users/me/organizations":
|
||||
httpapi.Write(ctx, w, http.StatusOK, []codersdk.Organization{
|
||||
{MinimalOrganization: codersdk.MinimalOrganization{
|
||||
ID: organizationID,
|
||||
}},
|
||||
})
|
||||
case fmt.Sprintf("/api/v2/organizations/%s/templates/my-template/versions/not-real-template-version", organizationID):
|
||||
httpapi.ResourceNotFound(w)
|
||||
default:
|
||||
t.Errorf("unexpected path: %s", r.URL.Path)
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
args: []string{"not-real-template", "--input", "my custom prompt"},
|
||||
expectError: httpapi.ResourceNotFoundResponse.Message,
|
||||
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/api/v2/users/me/organizations":
|
||||
httpapi.Write(ctx, w, http.StatusOK, []codersdk.Organization{
|
||||
{MinimalOrganization: codersdk.MinimalOrganization{
|
||||
ID: organizationID,
|
||||
}},
|
||||
})
|
||||
case fmt.Sprintf("/api/v2/organizations/%s/templates/not-real-template", organizationID):
|
||||
httpapi.ResourceNotFound(w)
|
||||
default:
|
||||
t.Errorf("unexpected path: %s", r.URL.Path)
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(strings.Join(tt.args, ","), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
ctx = testutil.Context(t, testutil.WaitShort)
|
||||
srv = httptest.NewServer(tt.handler(t, ctx))
|
||||
client = new(codersdk.Client)
|
||||
args = []string{"exp", "task", "create"}
|
||||
sb strings.Builder
|
||||
err error
|
||||
)
|
||||
|
||||
t.Cleanup(srv.Close)
|
||||
|
||||
client.URL, err = url.Parse(srv.URL)
|
||||
require.NoError(t, err)
|
||||
|
||||
inv, root := clitest.New(t, append(args, tt.args...)...)
|
||||
inv.Environ = serpent.ParseEnviron(tt.env, "")
|
||||
inv.Stdout = &sb
|
||||
inv.Stderr = &sb
|
||||
clitest.SetupConfig(t, client, root)
|
||||
|
||||
err = inv.WithContext(ctx).Run()
|
||||
if tt.expectError == "" {
|
||||
assert.NoError(t, err)
|
||||
} else {
|
||||
assert.ErrorContains(t, err, tt.expectError)
|
||||
}
|
||||
|
||||
assert.Contains(t, sb.String(), tt.expectOutput)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user