-
Notifications
You must be signed in to change notification settings - Fork 979
feat(cli): add exp task create command #19492
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
8c55100
14ee5c9
6271b22
9189ea6
27e86e3
873456e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,173 @@ | ||
package cli | ||
|
||
import ( | ||
"fmt" | ||
"slices" | ||
"strings" | ||
"time" | ||
|
||
"github.com/coder/coder/v2/cli/cliui" | ||
"github.com/coder/coder/v2/coderd/util/slice" | ||
"github.com/coder/coder/v2/codersdk" | ||
"github.com/coder/serpent" | ||
"github.com/google/uuid" | ||
"golang.org/x/xerrors" | ||
) | ||
|
||
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 [task]", | ||
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_TEMPLATE_NAME", | ||
DanielleMaywood marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Value: serpent.StringOf(&templateName), | ||
}, | ||
{ | ||
Env: "CODER_TEMPLATE_VERSION", | ||
DanielleMaywood marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Value: serpent.StringOf(&templateVersionName), | ||
}, | ||
{ | ||
Flag: "preset", | ||
Env: "CODER_PRESET_NAME", | ||
DanielleMaywood marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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 == "" { | ||
templates, err := client.Templates(ctx, codersdk.TemplateFilter{SearchQuery: "has-ai-task:true"}) | ||
if err != nil { | ||
return xerrors.Errorf("get templates: %w", err) | ||
} | ||
|
||
slices.SortFunc(templates, func(a, b codersdk.Template) int { | ||
return slice.Descending(a.ActiveUserCount, b.ActiveUserCount) | ||
}) | ||
|
||
templateNames := make([]string, 0, len(templates)) | ||
templateByName := make(map[string]codersdk.Template, len(templates)) | ||
|
||
// If more than 1 organization exists in the list of templates, | ||
// then include the organization name in the select options. | ||
uniqueOrganizations := make(map[uuid.UUID]bool) | ||
for _, template := range templates { | ||
uniqueOrganizations[template.OrganizationID] = true | ||
} | ||
|
||
for _, template := range templates { | ||
templateName := template.Name | ||
if len(uniqueOrganizations) > 1 { | ||
templateName += cliui.Placeholder( | ||
fmt.Sprintf( | ||
" (%s)", | ||
template.OrganizationName, | ||
), | ||
) | ||
} | ||
|
||
if template.ActiveUserCount > 0 { | ||
templateName += cliui.Placeholder( | ||
fmt.Sprintf( | ||
" used by %s", | ||
formatActiveDevelopers(template.ActiveUserCount), | ||
), | ||
) | ||
} | ||
|
||
templateNames = append(templateNames, templateName) | ||
templateByName[templateName] = template | ||
} | ||
|
||
option, err := cliui.Select(inv, cliui.SelectOptions{ | ||
Options: templateNames, | ||
HideSearch: true, | ||
}) | ||
|
||
templateName = templateByName[option].Name | ||
} | ||
|
||
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) | ||
} | ||
|
||
templateVersionID = preset.ID | ||
} | ||
|
||
workspace, err := expClient.CreateTask(ctx, codersdk.Me, codersdk.CreateTaskRequest{ | ||
TemplateVersionID: templateVersionID, | ||
TemplateVersionPresetID: templateVersionPresetID, | ||
Prompt: taskInput, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggestion: Not sure if we're too far gone to do this, but potential for rename: Prompt -> Input depending on how we want to standardize this. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As it is all experimental we technically still have time |
||
}) | ||
|
||
_, _ = fmt.Fprintf( | ||
inv.Stdout, | ||
"The task %s has been created at %s!\n", | ||
cliui.Keyword(workspace.Name), | ||
cliui.Timestamp(time.Now()), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The way I've implemented it is how we currently do it for workspace creation but I'm happy to create a quick follow up PR to address this? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Up to you tbh, I'm good with either just thought it made sense so threw it out there. 👍🏻 |
||
) | ||
|
||
return nil | ||
}, | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
package cli_test | ||
|
||
import ( | ||
"fmt" | ||
"testing" | ||
|
||
"github.com/coder/coder/v2/cli/clitest" | ||
"github.com/coder/coder/v2/coderd/coderdtest" | ||
"github.com/coder/coder/v2/codersdk" | ||
"github.com/coder/coder/v2/provisioner/echo" | ||
"github.com/coder/coder/v2/provisionersdk/proto" | ||
"github.com/coder/coder/v2/testutil" | ||
"github.com/google/uuid" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func TestTaskCreate(t *testing.T) { | ||
t.Parallel() | ||
|
||
createAITemplate := func(t *testing.T, client *codersdk.Client, user codersdk.CreateFirstUserResponse) (codersdk.TemplateVersion, codersdk.Template) { | ||
t.Helper() | ||
|
||
taskAppID := uuid.New() | ||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ | ||
Parse: echo.ParseComplete, | ||
ProvisionPlan: []*proto.Response{ | ||
{ | ||
Type: &proto.Response_Plan{ | ||
Plan: &proto.PlanComplete{ | ||
Parameters: []*proto.RichParameter{{Name: codersdk.AITaskPromptParameterName, Type: "string"}}, | ||
HasAiTasks: true, | ||
AiTasks: []*proto.AITask{}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
ProvisionApply: []*proto.Response{ | ||
{ | ||
Type: &proto.Response_Apply{ | ||
Apply: &proto.ApplyComplete{ | ||
Resources: []*proto.Resource{{ | ||
Name: "example", | ||
Type: "aws_instance", | ||
Agents: []*proto.Agent{{ | ||
Id: uuid.NewString(), | ||
Name: "example", | ||
Apps: []*proto.App{ | ||
{ | ||
Id: taskAppID.String(), | ||
Slug: "task-sidebar", | ||
DisplayName: "Task Sidebar", | ||
}, | ||
}, | ||
}}, | ||
}}, | ||
Parameters: []*proto.RichParameter{{Name: codersdk.AITaskPromptParameterName, Type: "string"}}, | ||
AiTasks: []*proto.AITask{{ | ||
SidebarApp: &proto.AITaskSidebarApp{ | ||
Id: taskAppID.String(), | ||
}, | ||
}}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
}) | ||
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) | ||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) | ||
|
||
return version, template | ||
} | ||
|
||
t.Run("CreateWithTemplateNameAndVersion", func(t *testing.T) { | ||
t.Parallel() | ||
|
||
var ( | ||
ctx = testutil.Context(t, testutil.WaitShort) | ||
|
||
prompt = "Task prompt" | ||
) | ||
|
||
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) | ||
owner := coderdtest.CreateFirstUser(t, client) | ||
templateVersion, template := createAITemplate(t, client, owner) | ||
|
||
member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID) | ||
expMember := codersdk.NewExperimentalClient(member) | ||
|
||
tasks, err := expMember.Tasks(ctx, nil) | ||
require.NoError(t, err) | ||
require.Empty(t, tasks) | ||
|
||
args := []string{ | ||
"exp", | ||
"task", | ||
"create", | ||
fmt.Sprintf("%s@%s", template.Name, templateVersion.Name), | ||
"--input", prompt, | ||
} | ||
|
||
inv, root := clitest.New(t, args...) | ||
clitest.SetupConfig(t, member, root) | ||
|
||
err = inv.Run() | ||
require.NoError(t, err) | ||
|
||
workspaces, err := member.Workspaces(ctx, codersdk.WorkspaceFilter{FilterQuery: "has-ai-task:true"}) | ||
require.NoError(t, err) | ||
require.Len(t, workspaces.Workspaces, 1) | ||
|
||
coderdtest.AwaitWorkspaceBuildJobCompleted(t, member, workspaces.Workspaces[0].LatestBuild.ID) | ||
|
||
tasks, err = expMember.Tasks(ctx, nil) | ||
require.NoError(t, err) | ||
require.Len(t, tasks, 1) | ||
|
||
require.Equal(t, prompt, tasks[0].InitialPrompt) | ||
}) | ||
} |
Uh oh!
There was an error while loading. Please reload this page.