Skip to content
This repository was archived by the owner on Aug 30, 2024. It is now read-only.

Commit 74d0294

Browse files
authored
feat: Add edit-from-config command to switch templates (#332)
- Add edit-from-config command to switch templates. Only works for local templates
1 parent 188450b commit 74d0294

File tree

5 files changed

+211
-105
lines changed

5 files changed

+211
-105
lines changed

coder-sdk/workspace.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -205,12 +205,13 @@ func (c *DefaultClient) StopWorkspace(ctx context.Context, workspaceID string) e
205205
// UpdateWorkspaceReq defines the update operation, only setting
206206
// nil-fields.
207207
type UpdateWorkspaceReq struct {
208-
ImageID *string `json:"image_id"`
209-
ImageTag *string `json:"image_tag"`
210-
CPUCores *float32 `json:"cpu_cores"`
211-
MemoryGB *float32 `json:"memory_gb"`
212-
DiskGB *int `json:"disk_gb"`
213-
GPUs *int `json:"gpus"`
208+
ImageID *string `json:"image_id"`
209+
ImageTag *string `json:"image_tag"`
210+
CPUCores *float32 `json:"cpu_cores"`
211+
MemoryGB *float32 `json:"memory_gb"`
212+
DiskGB *int `json:"disk_gb"`
213+
GPUs *int `json:"gpus"`
214+
TemplateID *string `json:"template_id"`
214215
}
215216

216217
// RebuildWorkspace requests that the given workspaceID is rebuilt with no changes to its specification.

docs/coder_workspaces.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ Perform operations on the Coder workspaces owned by the active user.
2424
* [coder workspaces create](coder_workspaces_create.md) - create a new workspace.
2525
* [coder workspaces create-from-config](coder_workspaces_create-from-config.md) - create a new workspace from a template
2626
* [coder workspaces edit](coder_workspaces_edit.md) - edit an existing workspace and initiate a rebuild.
27+
* [coder workspaces edit-from-config](coder_workspaces_edit-from-config.md) - change the template a workspace is tracking
2728
* [coder workspaces ls](coder_workspaces_ls.md) - list all workspaces owned by the active user
2829
* [coder workspaces rebuild](coder_workspaces_rebuild.md) - rebuild a Coder workspace
2930
* [coder workspaces rm](coder_workspaces_rm.md) - remove Coder workspaces by name

docs/coder_workspaces_create-from-config.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ coder workspaces create-from-config [flags]
1414

1515
```
1616
# create a new workspace from git repository
17-
coder workspaces create-from-config --name="dev-workspace" --repo-url https://github.com/cdr/m --ref my-branch
18-
coder workspaces create-from-config --name="dev-workspace" -f coder.yaml
17+
coder envs create-from-config --name="dev-env" --repo-url https://github.com/cdr/m --ref my-branch
18+
coder envs create-from-config --name="dev-env" -f coder.yaml
1919
```
2020

2121
### Options
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
## coder workspaces edit-from-config
2+
3+
change the template a workspace is tracking
4+
5+
### Synopsis
6+
7+
Edit an existing Coder workspace using a Workspaces As Code template.
8+
9+
```
10+
coder workspaces edit-from-config [flags]
11+
```
12+
13+
### Examples
14+
15+
```
16+
# edit a new workspace from git repository
17+
coder envs edit-from-config dev-env --repo-url https://github.com/cdr/m --ref my-branch
18+
coder envs edit-from-config dev-env -f coder.yaml
19+
```
20+
21+
### Options
22+
23+
```
24+
-f, --filepath string path to local template file.
25+
--follow follow buildlog after initiating rebuild
26+
-h, --help help for edit-from-config
27+
```
28+
29+
### Options inherited from parent commands
30+
31+
```
32+
-v, --verbose show verbose output
33+
```
34+
35+
### SEE ALSO
36+
37+
* [coder workspaces](coder_workspaces.md) - Interact with Coder workspaces
38+

internal/cmd/workspaces.go

Lines changed: 163 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ func workspacesCmd() *cobra.Command {
4444
watchBuildLogCommand(),
4545
rebuildWorkspaceCommand(),
4646
createWorkspaceCmd(),
47-
createWorkspaceFromConfigCmd(),
47+
workspaceFromConfigCmd(true),
48+
workspaceFromConfigCmd(false),
4849
editWorkspaceCmd(),
4950
)
5051
return cmd
@@ -296,130 +297,195 @@ coder workspaces create my-new-powerful-workspace --cpu 12 --disk 100 --memory 1
296297
return cmd
297298
}
298299

299-
func createWorkspaceFromConfigCmd() *cobra.Command {
300+
// selectOrg finds the organization in the list or returns the default organization
301+
// if the needle isn't found.
302+
func selectOrg(needle string, haystack []coder.Organization) (*coder.Organization, error) {
303+
var userOrg *coder.Organization
304+
for i := range haystack {
305+
// Look for org by name
306+
if haystack[i].Name == needle {
307+
userOrg = &haystack[i]
308+
break
309+
}
310+
// Or use default if the provided is blank
311+
if needle == "" && haystack[i].Default {
312+
userOrg = &haystack[i]
313+
break
314+
}
315+
}
316+
317+
if userOrg == nil {
318+
if needle != "" {
319+
return nil, xerrors.Errorf("Unable to locate org '%s'", needle)
320+
}
321+
return nil, xerrors.Errorf("Unable to locate a default organization for the user")
322+
}
323+
return userOrg, nil
324+
}
325+
326+
// workspaceFromConfigCmd will return a create or an update workspace for a template'd workspace.
327+
// The code for create/update is nearly identical.
328+
// If `update` is true, the update command is returned. If false, the create command.
329+
func workspaceFromConfigCmd(update bool) *cobra.Command {
300330
var (
301-
ref string
302-
repo string
303-
follow bool
304-
filepath string
305-
org string
306-
providerName string
307-
workspaceName string
331+
ref string
332+
repo string
333+
follow bool
334+
filepath string
335+
org string
336+
providerName string
337+
envName string
308338
)
309339

310-
cmd := &cobra.Command{
311-
Use: "create-from-config",
312-
Short: "create a new workspace from a template",
313-
Long: "Create a new Coder workspace using a Workspaces As Code template.",
314-
Example: `# create a new workspace from git repository
315-
coder workspaces create-from-config --name="dev-workspace" --repo-url https://github.com/cdr/m --ref my-branch
316-
coder workspaces create-from-config --name="dev-workspace" -f coder.yaml`,
317-
RunE: func(cmd *cobra.Command, args []string) error {
318-
ctx := cmd.Context()
340+
run := func(cmd *cobra.Command, args []string) error {
341+
ctx := cmd.Context()
319342

320-
if workspaceName == "" {
321-
return clog.Error("Must provide a workspace name.",
322-
clog.BlankLine,
323-
clog.Tipf("Use --name=<workspace-name> to name your workspace"),
324-
)
325-
}
343+
// Update requires the env name, and the name should be the first argument.
344+
if update {
345+
envName = args[0]
346+
} else if envName == "" {
347+
// Create takes the name as a flag, and it must be set
348+
return clog.Error("Must provide a workspace name.",
349+
clog.BlankLine,
350+
clog.Tipf("Use --name=<workspace-name> to name your workspace"),
351+
)
352+
}
326353

327-
client, err := newClient(ctx, true)
354+
client, err := newClient(ctx, true)
355+
if err != nil {
356+
return err
357+
}
358+
359+
orgs, err := getUserOrgs(ctx, client, coder.Me)
360+
if err != nil {
361+
return err
362+
}
363+
364+
multiOrgMember := len(orgs) > 1
365+
if multiOrgMember && org == "" {
366+
return xerrors.New("org is required for multi-org members")
367+
}
368+
369+
// This is the env to be updated/created
370+
var env *coder.Workspace
371+
372+
// OrgID is the org where the template and env should be created.
373+
// If we are updating an env, use the orgID from the workspace.
374+
var orgID string
375+
if update {
376+
env, err = findWorkspace(ctx, client, envName, coder.Me)
328377
if err != nil {
329-
return err
378+
return handleAPIError(err)
330379
}
331-
332-
orgs, err := getUserOrgs(ctx, client, coder.Me)
380+
orgID = env.OrganizationID
381+
} else {
382+
var userOrg *coder.Organization
383+
// Select org in list or use default
384+
userOrg, err := selectOrg(org, orgs)
333385
if err != nil {
334386
return err
335387
}
336388

337-
multiOrgMember := len(orgs) > 1
338-
if multiOrgMember && org == "" {
339-
return xerrors.New("org is required for multi-org members")
340-
}
341-
342-
var userOrg *coder.Organization
343-
for i := range orgs {
344-
// Look for org by name
345-
if orgs[i].Name == org {
346-
userOrg = &orgs[i]
347-
break
348-
}
349-
// Or use default if the provided is blank
350-
if org == "" && orgs[i].Default {
351-
userOrg = &orgs[i]
352-
break
353-
}
354-
}
389+
orgID = userOrg.ID
390+
}
355391

356-
if userOrg == nil {
357-
if org != "" {
358-
return xerrors.Errorf("Unable to locate org '%s'", org)
359-
}
360-
return xerrors.Errorf("Unable to locate a default organization for the user")
361-
}
392+
if filepath == "" && ref == "" && repo == "" {
393+
return clog.Error("Must specify a configuration source",
394+
"A template source is either sourced from a local file (-f) or from a git repository (--repo-url and --ref)",
395+
)
396+
}
362397

363-
var rd io.Reader
364-
if filepath != "" {
365-
b, err := ioutil.ReadFile(filepath)
366-
if err != nil {
367-
return xerrors.Errorf("read local file: %w", err)
368-
}
369-
rd = bytes.NewReader(b)
398+
var rd io.Reader
399+
if filepath != "" {
400+
b, err := ioutil.ReadFile(filepath)
401+
if err != nil {
402+
return xerrors.Errorf("read local file: %w", err)
370403
}
404+
rd = bytes.NewReader(b)
405+
}
371406

372-
req := coder.ParseTemplateRequest{
373-
RepoURL: repo,
374-
Ref: ref,
375-
Local: rd,
376-
OrgID: userOrg.ID,
377-
Filepath: ".coder/coder.yaml",
378-
}
407+
req := coder.ParseTemplateRequest{
408+
RepoURL: repo,
409+
Ref: ref,
410+
Local: rd,
411+
OrgID: orgID,
412+
Filepath: ".coder/coder.yaml",
413+
}
379414

380-
version, err := client.ParseTemplate(ctx, req)
381-
if err != nil {
382-
return handleAPIError(err)
383-
}
415+
version, err := client.ParseTemplate(ctx, req)
416+
if err != nil {
417+
return handleAPIError(err)
418+
}
384419

385-
provider, err := coderutil.DefaultWorkspaceProvider(ctx, client)
386-
if err != nil {
387-
return xerrors.Errorf("default workspace provider: %w", err)
388-
}
420+
provider, err := coderutil.DefaultWorkspaceProvider(ctx, client)
421+
if err != nil {
422+
return xerrors.Errorf("default workspace provider: %w", err)
423+
}
389424

390-
workspace, err := client.CreateWorkspace(ctx, coder.CreateWorkspaceRequest{
391-
OrgID: userOrg.ID,
425+
if update {
426+
err = client.EditWorkspace(ctx, env.ID, coder.UpdateWorkspaceReq{
427+
TemplateID: &version.TemplateID,
428+
})
429+
} else {
430+
env, err = client.CreateWorkspace(ctx, coder.CreateWorkspaceRequest{
431+
OrgID: orgID,
392432
TemplateID: version.TemplateID,
393433
ResourcePoolID: provider.ID,
394434
Namespace: provider.DefaultNamespace,
395-
Name: workspaceName,
435+
Name: envName,
396436
})
397-
if err != nil {
398-
return handleAPIError(err)
399-
}
437+
}
438+
if err != nil {
439+
return handleAPIError(err)
440+
}
400441

401-
if follow {
402-
clog.LogSuccess("creating workspace...")
403-
if err := trailBuildLogs(ctx, client, workspace.ID); err != nil {
404-
return err
405-
}
406-
return nil
442+
if follow {
443+
clog.LogSuccess("creating workspace...")
444+
if err := trailBuildLogs(ctx, client, env.ID); err != nil {
445+
return err
407446
}
408-
409-
clog.LogSuccess("creating workspace...",
410-
clog.BlankLine,
411-
clog.Tipf(`run "coder workspaces watch-build %s" to trail the build logs`, workspace.Name),
412-
)
413447
return nil
414-
},
448+
}
449+
450+
clog.LogSuccess("creating workspace...",
451+
clog.BlankLine,
452+
clog.Tipf(`run "coder envs watch-build %s" to trail the build logs`, env.Name),
453+
)
454+
return nil
415455
}
416-
cmd.Flags().StringVarP(&org, "org", "o", "", "name of the organization the workspace should be created under.")
456+
457+
var cmd *cobra.Command
458+
if update {
459+
cmd = &cobra.Command{
460+
Use: "edit-from-config",
461+
Short: "change the template a workspace is tracking",
462+
Long: "Edit an existing Coder workspace using a Workspaces As Code template.",
463+
Args: cobra.ExactArgs(1),
464+
Example: `# edit a new workspace from git repository
465+
coder envs edit-from-config dev-env --repo-url https://github.com/cdr/m --ref my-branch
466+
coder envs edit-from-config dev-env -f coder.yaml`,
467+
RunE: run,
468+
}
469+
} else {
470+
cmd = &cobra.Command{
471+
Use: "create-from-config",
472+
Short: "create a new workspace from a template",
473+
Long: "Create a new Coder workspace using a Workspaces As Code template.",
474+
Example: `# create a new workspace from git repository
475+
coder envs create-from-config --name="dev-env" --repo-url https://github.com/cdr/m --ref my-branch
476+
coder envs create-from-config --name="dev-env" -f coder.yaml`,
477+
RunE: run,
478+
}
479+
cmd.Flags().StringVar(&providerName, "provider", "", "name of Workspace Provider with which to create the workspace")
480+
cmd.Flags().StringVar(&envName, "name", "", "name of the workspace to be created")
481+
cmd.Flags().StringVarP(&org, "org", "o", "", "name of the organization the workspace should be created under.")
482+
// Ref and repo-url can only be used for create
483+
cmd.Flags().StringVarP(&ref, "ref", "", "master", "git reference to pull template from. May be a branch, tag, or commit hash.")
484+
cmd.Flags().StringVarP(&repo, "repo-url", "r", "", "URL of the git repository to pull the config from. Config file must live in '.coder/coder.yaml'.")
485+
}
486+
417487
cmd.Flags().StringVarP(&filepath, "filepath", "f", "", "path to local template file.")
418-
cmd.Flags().StringVarP(&ref, "ref", "", "master", "git reference to pull template from. May be a branch, tag, or commit hash.")
419-
cmd.Flags().StringVarP(&repo, "repo-url", "r", "", "URL of the git repository to pull the config from. Config file must live in '.coder/coder.yaml'.")
420488
cmd.Flags().BoolVar(&follow, "follow", false, "follow buildlog after initiating rebuild")
421-
cmd.Flags().StringVar(&providerName, "provider", "", "name of Workspace Provider with which to create the workspace")
422-
cmd.Flags().StringVar(&workspaceName, "name", "", "name of the workspace to be created")
423489
return cmd
424490
}
425491

0 commit comments

Comments
 (0)