- Notifications
You must be signed in to change notification settings - Fork1.1k
feat: coder-attach: add support for external workspaces#19178
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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Uh oh!
There was an error while loading.Please reload this page.
Changes fromall commits
3ea541e4818df14bfdb831044051fd2458b0c39f5023e555af9f5be1e281f0ed77522d451c806f9274fe00b6f26c019a317d07857c462a692d2dfec387fc04c2588ea33dd778c4134793c1d69473acd0f7cc6861da68c202e24741682ea6051967a5f060324ff6e8fae2a7182141bc54a75c1f422f2c00File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading.Please reload this page.
Jump to
Uh oh!
There was an error while loading.Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -29,7 +29,12 @@ const PresetNone = "none" | ||
| var ErrNoPresetFound = xerrors.New("no preset found") | ||
| type createOptions struct { | ||
| beforeCreate func(ctx context.Context, client *codersdk.Client, template codersdk.Template, templateVersionID uuid.UUID) error | ||
| afterCreate func(ctx context.Context, inv *serpent.Invocation, client *codersdk.Client, workspace codersdk.Workspace) error | ||
| } | ||
Comment on lines +32 to +35 Member 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. 👍 nice abstraction! | ||
| func (r *RootCmd) create(opts createOptions) *serpent.Command { | ||
| var ( | ||
| templateName string | ||
| templateVersion string | ||
| @@ -305,6 +310,13 @@ func (r *RootCmd) create() *serpent.Command { | ||
| _, _ = fmt.Fprintf(inv.Stdout, "%s", cliui.Bold("No preset applied.")) | ||
| } | ||
| if opts.beforeCreate != nil { | ||
| err = opts.beforeCreate(inv.Context(), client, template, templateVersionID) | ||
| if err != nil { | ||
| return xerrors.Errorf("before create: %w", err) | ||
| } | ||
| } | ||
| richParameters, err := prepWorkspaceBuild(inv, client, prepWorkspaceBuildArgs{ | ||
| Action: WorkspaceCreate, | ||
| TemplateVersionID: templateVersionID, | ||
| @@ -366,6 +378,14 @@ func (r *RootCmd) create() *serpent.Command { | ||
| cliui.Keyword(workspace.Name), | ||
| cliui.Timestamp(time.Now()), | ||
| ) | ||
| if opts.afterCreate != nil { | ||
| err = opts.afterCreate(inv.Context(), inv, client, workspace) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| } | ||
| return nil | ||
| }, | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,271 @@ | ||
| package cli | ||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "strings" | ||
| "github.com/google/uuid" | ||
| "golang.org/x/xerrors" | ||
| "github.com/coder/coder/v2/cli/cliui" | ||
| "github.com/coder/coder/v2/codersdk" | ||
| "github.com/coder/pretty" | ||
| "github.com/coder/serpent" | ||
| ) | ||
| typeexternalAgentstruct { | ||
| WorkspaceNamestring`json:"-"` | ||
| AgentNamestring`json:"-"` | ||
| AuthTypestring`json:"auth_type"` | ||
| AuthTokenstring`json:"auth_token"` | ||
| InitScriptstring`json:"init_script"` | ||
| } | ||
| func (r*RootCmd)externalWorkspaces()*serpent.Command { | ||
| orgContext:=NewOrganizationContext() | ||
| cmd:=&serpent.Command{ | ||
| Use:"external-workspaces [subcommand]", | ||
| Short:"Create or manage external workspaces", | ||
| Handler:func(inv*serpent.Invocation)error { | ||
| returninv.Command.HelpHandler(inv) | ||
| }, | ||
| Children: []*serpent.Command{ | ||
| r.externalWorkspaceCreate(), | ||
| r.externalWorkspaceAgentInstructions(), | ||
| r.externalWorkspaceList(), | ||
| }, | ||
| } | ||
| orgContext.AttachOptions(cmd) | ||
| returncmd | ||
| } | ||
Comment on lines +25 to +43 Member 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. Is this functionality available in OSS as well? | ||
| // externalWorkspaceCreate extends `coder create` to create an external workspace. | ||
| func (r*RootCmd)externalWorkspaceCreate()*serpent.Command { | ||
| opts:=createOptions{ | ||
| beforeCreate:func(ctx context.Context,client*codersdk.Client,_ codersdk.Template,templateVersionID uuid.UUID)error { | ||
| resources,err:=client.TemplateVersionResources(ctx,templateVersionID) | ||
| iferr!=nil { | ||
| returnxerrors.Errorf("get template version resources: %w",err) | ||
| } | ||
| iflen(resources)==0 { | ||
| returnxerrors.Errorf("no resources found for template version %q",templateVersionID) | ||
| } | ||
| varhasExternalAgentbool | ||
| for_,resource:=rangeresources { | ||
| ifresource.Type=="coder_external_agent" { | ||
| hasExternalAgent=true | ||
| break | ||
| } | ||
| } | ||
| if!hasExternalAgent { | ||
| returnxerrors.Errorf("template version %q does not have an external agent. Only templates with external agents can be used for external workspace creation",templateVersionID) | ||
| } | ||
| returnnil | ||
| }, | ||
| afterCreate:func(ctx context.Context,inv*serpent.Invocation,client*codersdk.Client,workspace codersdk.Workspace)error { | ||
| workspace,err:=client.WorkspaceByOwnerAndName(ctx,codersdk.Me,workspace.Name, codersdk.WorkspaceOptions{}) | ||
| iferr!=nil { | ||
| returnxerrors.Errorf("get workspace by name: %w",err) | ||
| } | ||
| externalAgents,err:=fetchExternalAgents(inv,client,workspace,workspace.LatestBuild.Resources) | ||
| iferr!=nil { | ||
| returnxerrors.Errorf("fetch external agents: %w",err) | ||
| } | ||
| formatted:=formatExternalAgent(workspace.Name,externalAgents) | ||
| _,err=fmt.Fprintln(inv.Stdout,formatted) | ||
| returnerr | ||
| }, | ||
| } | ||
| cmd:=r.create(opts) | ||
| cmd.Use="create [workspace]" | ||
| cmd.Short="Create a new external workspace" | ||
| cmd.Middleware=serpent.Chain( | ||
| cmd.Middleware, | ||
| serpent.RequireNArgs(1), | ||
| ) | ||
| fori:=rangecmd.Options { | ||
| ifcmd.Options[i].Flag=="template" { | ||
| cmd.Options[i].Required=true | ||
| } | ||
| } | ||
| returncmd | ||
| } | ||
| // externalWorkspaceAgentInstructions prints the instructions for an external agent. | ||
| func (r*RootCmd)externalWorkspaceAgentInstructions()*serpent.Command { | ||
| client:=new(codersdk.Client) | ||
| formatter:=cliui.NewOutputFormatter( | ||
| cliui.ChangeFormatterData(cliui.TextFormat(),func(dataany) (any,error) { | ||
| agent,ok:=data.(externalAgent) | ||
| if!ok { | ||
| return"",xerrors.Errorf("expected externalAgent, got %T",data) | ||
| } | ||
| returnformatExternalAgent(agent.WorkspaceName, []externalAgent{agent}),nil | ||
| }), | ||
| cliui.JSONFormat(), | ||
| ) | ||
| cmd:=&serpent.Command{ | ||
| Use:"agent-instructions [user/]workspace[.agent]", | ||
| Short:"Get the instructions for an external agent", | ||
| Middleware:serpent.Chain(r.InitClient(client),serpent.RequireNArgs(1)), | ||
| Handler:func(inv*serpent.Invocation)error { | ||
| workspace,workspaceAgent,_,err:=getWorkspaceAndAgent(inv.Context(),inv,client,false,inv.Args[0]) | ||
| iferr!=nil { | ||
| returnxerrors.Errorf("find workspace and agent: %w",err) | ||
| } | ||
| credentials,err:=client.WorkspaceExternalAgentCredentials(inv.Context(),workspace.ID,workspaceAgent.Name) | ||
| iferr!=nil { | ||
| returnxerrors.Errorf("get external agent token for agent %q: %w",workspaceAgent.Name,err) | ||
| } | ||
| agentInfo:=externalAgent{ | ||
| WorkspaceName:workspace.Name, | ||
| AgentName:workspaceAgent.Name, | ||
| AuthType:"token", | ||
| AuthToken:credentials.AgentToken, | ||
| InitScript:credentials.Command, | ||
| } | ||
| out,err:=formatter.Format(inv.Context(),agentInfo) | ||
| iferr!=nil { | ||
| returnerr | ||
| } | ||
| _,err=fmt.Fprintln(inv.Stdout,out) | ||
| returnerr | ||
| }, | ||
| } | ||
| formatter.AttachOptions(&cmd.Options) | ||
| returncmd | ||
| } | ||
| func (r*RootCmd)externalWorkspaceList()*serpent.Command { | ||
| var ( | ||
| filter cliui.WorkspaceFilter | ||
| formatter=cliui.NewOutputFormatter( | ||
| cliui.TableFormat( | ||
| []workspaceListRow{}, | ||
| []string{ | ||
| "workspace", | ||
| "template", | ||
| "status", | ||
| "healthy", | ||
| "last built", | ||
| "current version", | ||
| "outdated", | ||
| }, | ||
| ), | ||
| cliui.JSONFormat(), | ||
| ) | ||
| ) | ||
| client:=new(codersdk.Client) | ||
| cmd:=&serpent.Command{ | ||
| Annotations:workspaceCommand, | ||
| Use:"list", | ||
| Short:"List external workspaces", | ||
| Aliases: []string{"ls"}, | ||
| Middleware:serpent.Chain( | ||
| serpent.RequireNArgs(0), | ||
| r.InitClient(client), | ||
| ), | ||
| Handler:func(inv*serpent.Invocation)error { | ||
| baseFilter:=filter.Filter() | ||
| ifbaseFilter.FilterQuery=="" { | ||
| baseFilter.FilterQuery="has-external-agent:true" | ||
| }else { | ||
| baseFilter.FilterQuery+=" has-external-agent:true" | ||
| } | ||
| res,err:=queryConvertWorkspaces(inv.Context(),client,baseFilter,workspaceListRowFromWorkspace) | ||
| iferr!=nil { | ||
| returnerr | ||
| } | ||
| iflen(res)==0&&formatter.FormatID()!=cliui.JSONFormat().ID() { | ||
| pretty.Fprintf(inv.Stderr,cliui.DefaultStyles.Prompt,"No workspaces found! Create one:\n") | ||
| _,_=fmt.Fprintln(inv.Stderr) | ||
| _,_=fmt.Fprintln(inv.Stderr," "+pretty.Sprint(cliui.DefaultStyles.Code,"coder external-workspaces create <name>")) | ||
| _,_=fmt.Fprintln(inv.Stderr) | ||
| returnnil | ||
| } | ||
| out,err:=formatter.Format(inv.Context(),res) | ||
| iferr!=nil { | ||
| returnerr | ||
| } | ||
| _,err=fmt.Fprintln(inv.Stdout,out) | ||
| returnerr | ||
| }, | ||
| } | ||
| filter.AttachOptions(&cmd.Options) | ||
| formatter.AttachOptions(&cmd.Options) | ||
| returncmd | ||
| } | ||
| // fetchExternalAgents fetches the external agents for a workspace. | ||
| funcfetchExternalAgents(inv*serpent.Invocation,client*codersdk.Client,workspace codersdk.Workspace,resources []codersdk.WorkspaceResource) ([]externalAgent,error) { | ||
| iflen(resources)==0 { | ||
| returnnil,xerrors.Errorf("no resources found for workspace") | ||
| } | ||
| varexternalAgents []externalAgent | ||
| for_,resource:=rangeresources { | ||
| ifresource.Type!="coder_external_agent"||len(resource.Agents)==0 { | ||
| continue | ||
| } | ||
| agent:=resource.Agents[0] | ||
| credentials,err:=client.WorkspaceExternalAgentCredentials(inv.Context(),workspace.ID,agent.Name) | ||
| iferr!=nil { | ||
| returnnil,xerrors.Errorf("get external agent token for agent %q: %w",agent.Name,err) | ||
| } | ||
| externalAgents=append(externalAgents,externalAgent{ | ||
| AgentName:agent.Name, | ||
| AuthType:"token", | ||
| AuthToken:credentials.AgentToken, | ||
| InitScript:credentials.Command, | ||
| }) | ||
| } | ||
| returnexternalAgents,nil | ||
| } | ||
| // formatExternalAgent formats the instructions for an external agent. | ||
| funcformatExternalAgent(workspaceNamestring,externalAgents []externalAgent)string { | ||
| varoutput strings.Builder | ||
| _,_=output.WriteString(fmt.Sprintf("\nPlease run the following commands to attach external agent to the workspace %s:\n\n",cliui.Keyword(workspaceName))) | ||
| fori,agent:=rangeexternalAgents { | ||
| iflen(externalAgents)>1 { | ||
| _,_=output.WriteString(fmt.Sprintf("For agent %s:\n",cliui.Keyword(agent.AgentName))) | ||
| } | ||
| _,_=output.WriteString(fmt.Sprintf("%s\n",pretty.Sprint(cliui.DefaultStyles.Code,fmt.Sprintf("export CODER_AGENT_TOKEN=%s",agent.AuthToken)))) | ||
| _,_=output.WriteString(fmt.Sprintf("%s\n",pretty.Sprint(cliui.DefaultStyles.Code,fmt.Sprintf("curl -fsSL %s | sh",agent.InitScript)))) | ||
| ifi<len(externalAgents)-1 { | ||
| _,_=output.WriteString("\n") | ||
| } | ||
| } | ||
| returnoutput.String() | ||
| } | ||
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.