|
| 1 | +package cli |
| 2 | + |
| 3 | +import ( |
| 4 | +"context" |
| 5 | +"fmt" |
| 6 | +"strings" |
| 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/pretty" |
| 14 | +"github.com/coder/serpent" |
| 15 | +) |
| 16 | + |
| 17 | +typeexternalAgentstruct { |
| 18 | +WorkspaceNamestring`json:"-"` |
| 19 | +AgentNamestring`json:"-"` |
| 20 | +AuthTypestring`json:"auth_type"` |
| 21 | +AuthTokenstring`json:"auth_token"` |
| 22 | +InitScriptstring`json:"init_script"` |
| 23 | +} |
| 24 | + |
| 25 | +func (r*RootCmd)externalWorkspaces()*serpent.Command { |
| 26 | +orgContext:=NewOrganizationContext() |
| 27 | + |
| 28 | +cmd:=&serpent.Command{ |
| 29 | +Use:"external-workspaces [subcommand]", |
| 30 | +Short:"Create or manage external workspaces", |
| 31 | +Handler:func(inv*serpent.Invocation)error { |
| 32 | +returninv.Command.HelpHandler(inv) |
| 33 | +}, |
| 34 | +Children: []*serpent.Command{ |
| 35 | +r.externalWorkspaceCreate(), |
| 36 | +r.externalWorkspaceAgentInstructions(), |
| 37 | +r.externalWorkspaceList(), |
| 38 | +}, |
| 39 | +} |
| 40 | + |
| 41 | +orgContext.AttachOptions(cmd) |
| 42 | +returncmd |
| 43 | +} |
| 44 | + |
| 45 | +// externalWorkspaceCreate extends `coder create` to create an external workspace. |
| 46 | +func (r*RootCmd)externalWorkspaceCreate()*serpent.Command { |
| 47 | +opts:=createOptions{ |
| 48 | +beforeCreate:func(ctx context.Context,client*codersdk.Client,_ codersdk.Template,templateVersionID uuid.UUID)error { |
| 49 | +resources,err:=client.TemplateVersionResources(ctx,templateVersionID) |
| 50 | +iferr!=nil { |
| 51 | +returnxerrors.Errorf("get template version resources: %w",err) |
| 52 | +} |
| 53 | +iflen(resources)==0 { |
| 54 | +returnxerrors.Errorf("no resources found for template version %q",templateVersionID) |
| 55 | +} |
| 56 | + |
| 57 | +varhasExternalAgentbool |
| 58 | +for_,resource:=rangeresources { |
| 59 | +ifresource.Type=="coder_external_agent" { |
| 60 | +hasExternalAgent=true |
| 61 | +break |
| 62 | +} |
| 63 | +} |
| 64 | + |
| 65 | +if!hasExternalAgent { |
| 66 | +returnxerrors.Errorf("template version %q does not have an external agent. Only templates with external agents can be used for external workspace creation",templateVersionID) |
| 67 | +} |
| 68 | + |
| 69 | +returnnil |
| 70 | +}, |
| 71 | +afterCreate:func(ctx context.Context,inv*serpent.Invocation,client*codersdk.Client,workspace codersdk.Workspace)error { |
| 72 | +workspace,err:=client.WorkspaceByOwnerAndName(ctx,codersdk.Me,workspace.Name, codersdk.WorkspaceOptions{}) |
| 73 | +iferr!=nil { |
| 74 | +returnxerrors.Errorf("get workspace by name: %w",err) |
| 75 | +} |
| 76 | + |
| 77 | +externalAgents,err:=fetchExternalAgents(inv,client,workspace,workspace.LatestBuild.Resources) |
| 78 | +iferr!=nil { |
| 79 | +returnxerrors.Errorf("fetch external agents: %w",err) |
| 80 | +} |
| 81 | + |
| 82 | +formatted:=formatExternalAgent(workspace.Name,externalAgents) |
| 83 | +_,err=fmt.Fprintln(inv.Stdout,formatted) |
| 84 | +returnerr |
| 85 | +}, |
| 86 | +} |
| 87 | + |
| 88 | +cmd:=r.create(opts) |
| 89 | +cmd.Use="create [workspace]" |
| 90 | +cmd.Short="Create a new external workspace" |
| 91 | +cmd.Middleware=serpent.Chain( |
| 92 | +cmd.Middleware, |
| 93 | +serpent.RequireNArgs(1), |
| 94 | +) |
| 95 | + |
| 96 | +fori:=rangecmd.Options { |
| 97 | +ifcmd.Options[i].Flag=="template" { |
| 98 | +cmd.Options[i].Required=true |
| 99 | +} |
| 100 | +} |
| 101 | + |
| 102 | +returncmd |
| 103 | +} |
| 104 | + |
| 105 | +// externalWorkspaceAgentInstructions prints the instructions for an external agent. |
| 106 | +func (r*RootCmd)externalWorkspaceAgentInstructions()*serpent.Command { |
| 107 | +client:=new(codersdk.Client) |
| 108 | +formatter:=cliui.NewOutputFormatter( |
| 109 | +cliui.ChangeFormatterData(cliui.TextFormat(),func(dataany) (any,error) { |
| 110 | +agent,ok:=data.(externalAgent) |
| 111 | +if!ok { |
| 112 | +return"",xerrors.Errorf("expected externalAgent, got %T",data) |
| 113 | +} |
| 114 | + |
| 115 | +returnformatExternalAgent(agent.WorkspaceName, []externalAgent{agent}),nil |
| 116 | +}), |
| 117 | +cliui.JSONFormat(), |
| 118 | +) |
| 119 | + |
| 120 | +cmd:=&serpent.Command{ |
| 121 | +Use:"agent-instructions [user/]workspace[.agent]", |
| 122 | +Short:"Get the instructions for an external agent", |
| 123 | +Middleware:serpent.Chain(r.InitClient(client),serpent.RequireNArgs(1)), |
| 124 | +Handler:func(inv*serpent.Invocation)error { |
| 125 | +workspace,workspaceAgent,_,err:=getWorkspaceAndAgent(inv.Context(),inv,client,false,inv.Args[0]) |
| 126 | +iferr!=nil { |
| 127 | +returnxerrors.Errorf("find workspace and agent: %w",err) |
| 128 | +} |
| 129 | + |
| 130 | +credentials,err:=client.WorkspaceExternalAgentCredentials(inv.Context(),workspace.ID,workspaceAgent.Name) |
| 131 | +iferr!=nil { |
| 132 | +returnxerrors.Errorf("get external agent token for agent %q: %w",workspaceAgent.Name,err) |
| 133 | +} |
| 134 | + |
| 135 | +agentInfo:=externalAgent{ |
| 136 | +WorkspaceName:workspace.Name, |
| 137 | +AgentName:workspaceAgent.Name, |
| 138 | +AuthType:"token", |
| 139 | +AuthToken:credentials.AgentToken, |
| 140 | +InitScript:credentials.Command, |
| 141 | +} |
| 142 | + |
| 143 | +out,err:=formatter.Format(inv.Context(),agentInfo) |
| 144 | +iferr!=nil { |
| 145 | +returnerr |
| 146 | +} |
| 147 | + |
| 148 | +_,err=fmt.Fprintln(inv.Stdout,out) |
| 149 | +returnerr |
| 150 | +}, |
| 151 | +} |
| 152 | + |
| 153 | +formatter.AttachOptions(&cmd.Options) |
| 154 | +returncmd |
| 155 | +} |
| 156 | + |
| 157 | +func (r*RootCmd)externalWorkspaceList()*serpent.Command { |
| 158 | +var ( |
| 159 | +filter cliui.WorkspaceFilter |
| 160 | +formatter=cliui.NewOutputFormatter( |
| 161 | +cliui.TableFormat( |
| 162 | +[]workspaceListRow{}, |
| 163 | +[]string{ |
| 164 | +"workspace", |
| 165 | +"template", |
| 166 | +"status", |
| 167 | +"healthy", |
| 168 | +"last built", |
| 169 | +"current version", |
| 170 | +"outdated", |
| 171 | +}, |
| 172 | +), |
| 173 | +cliui.JSONFormat(), |
| 174 | +) |
| 175 | +) |
| 176 | +client:=new(codersdk.Client) |
| 177 | +cmd:=&serpent.Command{ |
| 178 | +Annotations:workspaceCommand, |
| 179 | +Use:"list", |
| 180 | +Short:"List external workspaces", |
| 181 | +Aliases: []string{"ls"}, |
| 182 | +Middleware:serpent.Chain( |
| 183 | +serpent.RequireNArgs(0), |
| 184 | +r.InitClient(client), |
| 185 | +), |
| 186 | +Handler:func(inv*serpent.Invocation)error { |
| 187 | +baseFilter:=filter.Filter() |
| 188 | + |
| 189 | +ifbaseFilter.FilterQuery=="" { |
| 190 | +baseFilter.FilterQuery="has-external-agent:true" |
| 191 | +}else { |
| 192 | +baseFilter.FilterQuery+=" has-external-agent:true" |
| 193 | +} |
| 194 | + |
| 195 | +res,err:=queryConvertWorkspaces(inv.Context(),client,baseFilter,workspaceListRowFromWorkspace) |
| 196 | +iferr!=nil { |
| 197 | +returnerr |
| 198 | +} |
| 199 | + |
| 200 | +iflen(res)==0&&formatter.FormatID()!=cliui.JSONFormat().ID() { |
| 201 | +pretty.Fprintf(inv.Stderr,cliui.DefaultStyles.Prompt,"No workspaces found! Create one:\n") |
| 202 | +_,_=fmt.Fprintln(inv.Stderr) |
| 203 | +_,_=fmt.Fprintln(inv.Stderr," "+pretty.Sprint(cliui.DefaultStyles.Code,"coder external-workspaces create <name>")) |
| 204 | +_,_=fmt.Fprintln(inv.Stderr) |
| 205 | +returnnil |
| 206 | +} |
| 207 | + |
| 208 | +out,err:=formatter.Format(inv.Context(),res) |
| 209 | +iferr!=nil { |
| 210 | +returnerr |
| 211 | +} |
| 212 | + |
| 213 | +_,err=fmt.Fprintln(inv.Stdout,out) |
| 214 | +returnerr |
| 215 | +}, |
| 216 | +} |
| 217 | +filter.AttachOptions(&cmd.Options) |
| 218 | +formatter.AttachOptions(&cmd.Options) |
| 219 | +returncmd |
| 220 | +} |
| 221 | + |
| 222 | +// fetchExternalAgents fetches the external agents for a workspace. |
| 223 | +funcfetchExternalAgents(inv*serpent.Invocation,client*codersdk.Client,workspace codersdk.Workspace,resources []codersdk.WorkspaceResource) ([]externalAgent,error) { |
| 224 | +iflen(resources)==0 { |
| 225 | +returnnil,xerrors.Errorf("no resources found for workspace") |
| 226 | +} |
| 227 | + |
| 228 | +varexternalAgents []externalAgent |
| 229 | + |
| 230 | +for_,resource:=rangeresources { |
| 231 | +ifresource.Type!="coder_external_agent"||len(resource.Agents)==0 { |
| 232 | +continue |
| 233 | +} |
| 234 | + |
| 235 | +agent:=resource.Agents[0] |
| 236 | +credentials,err:=client.WorkspaceExternalAgentCredentials(inv.Context(),workspace.ID,agent.Name) |
| 237 | +iferr!=nil { |
| 238 | +returnnil,xerrors.Errorf("get external agent token for agent %q: %w",agent.Name,err) |
| 239 | +} |
| 240 | + |
| 241 | +externalAgents=append(externalAgents,externalAgent{ |
| 242 | +AgentName:agent.Name, |
| 243 | +AuthType:"token", |
| 244 | +AuthToken:credentials.AgentToken, |
| 245 | +InitScript:credentials.Command, |
| 246 | +}) |
| 247 | +} |
| 248 | + |
| 249 | +returnexternalAgents,nil |
| 250 | +} |
| 251 | + |
| 252 | +// formatExternalAgent formats the instructions for an external agent. |
| 253 | +funcformatExternalAgent(workspaceNamestring,externalAgents []externalAgent)string { |
| 254 | +varoutput strings.Builder |
| 255 | +_,_=output.WriteString(fmt.Sprintf("\nPlease run the following commands to attach external agent to the workspace %s:\n\n",cliui.Keyword(workspaceName))) |
| 256 | + |
| 257 | +fori,agent:=rangeexternalAgents { |
| 258 | +iflen(externalAgents)>1 { |
| 259 | +_,_=output.WriteString(fmt.Sprintf("For agent %s:\n",cliui.Keyword(agent.AgentName))) |
| 260 | +} |
| 261 | + |
| 262 | +_,_=output.WriteString(fmt.Sprintf("%s\n",pretty.Sprint(cliui.DefaultStyles.Code,fmt.Sprintf("export CODER_AGENT_TOKEN=%s",agent.AuthToken)))) |
| 263 | +_,_=output.WriteString(fmt.Sprintf("%s\n",pretty.Sprint(cliui.DefaultStyles.Code,fmt.Sprintf("curl -fsSL %s | sh",agent.InitScript)))) |
| 264 | + |
| 265 | +ifi<len(externalAgents)-1 { |
| 266 | +_,_=output.WriteString("\n") |
| 267 | +} |
| 268 | +} |
| 269 | + |
| 270 | +returnoutput.String() |
| 271 | +} |