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

Commit cdbfc48

Browse files
authored
feat: support setting policy template via local file or restoring to default (#366)
1 parent dd614ec commit cdbfc48

File tree

5 files changed

+264
-0
lines changed

5 files changed

+264
-0
lines changed

coder-sdk/interface.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,4 +238,7 @@ type Client interface {
238238

239239
// RenameWorkspaceProvider changes an existing providers name field.
240240
RenameWorkspaceProvider(ctx context.Context, id string, name string) error
241+
242+
// SetPolicyTemplate sets the workspace policy template
243+
SetPolicyTemplate(ctx context.Context, templateID string, templateScope TemplateScope, dryRun bool) (*SetPolicyTemplateResponse, error)
241244
}

coder-sdk/workspace.go

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ package coder
22

33
import (
44
"context"
5+
"fmt"
56
"io"
67
"net/http"
78
"net/url"
9+
"strings"
810
"time"
911

1012
"cdr.dev/wsep"
@@ -364,3 +366,146 @@ func (c *DefaultClient) WorkspacesByWorkspaceProvider(ctx context.Context, wpID
364366
}
365367
return workspaces, nil
366368
}
369+
370+
const (
371+
// SkipTemplateOrg allows skipping checks on organizations.
372+
SkipTemplateOrg = "SKIP_ORG"
373+
)
374+
375+
type TemplateScope string
376+
377+
const (
378+
// TemplateScopeSite is the scope for a site wide policy template.
379+
TemplateScopeSite = "site"
380+
)
381+
382+
type SetPolicyTemplateRequest struct {
383+
TemplateID string `json:"template_id"`
384+
Type string `json:"type"` // site, org
385+
}
386+
387+
type SetPolicyTemplateResponse struct {
388+
MergeConflicts []*WorkspaceTemplateMergeConflict `json:"merge_conflicts"`
389+
}
390+
391+
type WorkspaceTemplateMergeConflict struct {
392+
WorkspaceID string `json:"workspace_id"`
393+
CurrentTemplateWarnings []string `json:"current_template_warnings"`
394+
CurrentTemplateError *TplError `json:"current_template_errors"`
395+
LatestTemplateWarnings []string `json:"latest_template_warnings"`
396+
LatestTemplateError *TplError `json:"latest_template_errors"`
397+
CurrentTemplateIsLatest bool `json:"current_template_is_latest"`
398+
Message string `json:"message"`
399+
}
400+
401+
func (mc WorkspaceTemplateMergeConflict) String() string {
402+
var sb strings.Builder
403+
404+
if mc.Message != "" {
405+
sb.WriteString(mc.Message)
406+
}
407+
408+
currentConflicts := len(mc.CurrentTemplateWarnings) != 0 || mc.CurrentTemplateError != nil
409+
updateConflicts := len(mc.LatestTemplateWarnings) != 0 || mc.LatestTemplateError != nil
410+
411+
if !currentConflicts && !updateConflicts {
412+
sb.WriteString("No workspace conflicts\n")
413+
return sb.String()
414+
}
415+
416+
if currentConflicts {
417+
if len(mc.CurrentTemplateWarnings) != 0 {
418+
fmt.Fprintf(&sb, "Warnings: \n%s\n", strings.Join(mc.CurrentTemplateWarnings, "\n"))
419+
}
420+
if mc.CurrentTemplateError != nil {
421+
fmt.Fprintf(&sb, "Errors: \n%s\n", strings.Join(mc.CurrentTemplateError.Msgs, "\n"))
422+
}
423+
}
424+
425+
if !mc.CurrentTemplateIsLatest && updateConflicts {
426+
sb.WriteString("If workspace is updated to the latest template:\n")
427+
if len(mc.LatestTemplateWarnings) != 0 {
428+
fmt.Fprintf(&sb, "Warnings: \n%s\n", strings.Join(mc.LatestTemplateWarnings, "\n"))
429+
}
430+
if mc.LatestTemplateError != nil {
431+
fmt.Fprintf(&sb, "Errors: \n%s\n", strings.Join(mc.LatestTemplateError.Msgs, "\n"))
432+
}
433+
}
434+
435+
return sb.String()
436+
}
437+
438+
type WorkspaceTemplateMergeConflicts []*WorkspaceTemplateMergeConflict
439+
440+
func (mcs WorkspaceTemplateMergeConflicts) Summary() string {
441+
var (
442+
sb strings.Builder
443+
currentWarnings int
444+
updateWarnings int
445+
currentErrors int
446+
updateErrors int
447+
)
448+
449+
for _, mc := range mcs {
450+
if len(mc.CurrentTemplateWarnings) != 0 {
451+
currentWarnings++
452+
}
453+
if len(mc.LatestTemplateWarnings) != 0 {
454+
updateWarnings++
455+
}
456+
if mc.CurrentTemplateError != nil {
457+
currentErrors++
458+
}
459+
if mc.LatestTemplateError != nil {
460+
updateErrors++
461+
}
462+
}
463+
464+
if currentErrors == 0 && updateErrors == 0 && currentWarnings == 0 && updateWarnings == 0 {
465+
sb.WriteString("No workspace conflicts\n")
466+
return sb.String()
467+
}
468+
469+
if currentErrors != 0 {
470+
fmt.Fprintf(&sb, "%d workspaces will not be able to be rebuilt\n", currentErrors)
471+
}
472+
if updateErrors != 0 {
473+
fmt.Fprintf(&sb, "%d workspaces will not be able to be rebuilt if updated to the latest version\n", updateErrors)
474+
}
475+
if currentWarnings != 0 {
476+
fmt.Fprintf(&sb, "%d workspaces will be impacted\n", currentWarnings)
477+
}
478+
if updateWarnings != 0 {
479+
fmt.Fprintf(&sb, "%d workspaces will be impacted if updated to the latest version\n", updateWarnings)
480+
}
481+
482+
return sb.String()
483+
}
484+
485+
type TplError struct {
486+
// Msgs are the human facing strings to present to the user. Since there can be multiple
487+
// problems with a template, there might be multiple strings
488+
Msgs []string `json:"messages"`
489+
}
490+
491+
func (c *DefaultClient) SetPolicyTemplate(ctx context.Context, templateID string, templateScope TemplateScope, dryRun bool) (*SetPolicyTemplateResponse, error) {
492+
var (
493+
resp SetPolicyTemplateResponse
494+
query = url.Values{}
495+
)
496+
497+
req := SetPolicyTemplateRequest{
498+
TemplateID: templateID,
499+
Type: string(templateScope),
500+
}
501+
502+
if dryRun {
503+
query.Set("dry-run", "true")
504+
}
505+
506+
if err := c.requestBody(ctx, http.MethodPost, "/api/private/workspaces/template/policy", req, &resp, withQueryParams(query)); err != nil {
507+
return nil, err
508+
}
509+
510+
return &resp, nil
511+
}

docs/coder_workspaces.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ Perform operations on the Coder workspaces owned by the active user.
2626
* [coder workspaces edit](coder_workspaces_edit.md) - edit an existing workspace and initiate a rebuild.
2727
* [coder workspaces edit-from-config](coder_workspaces_edit-from-config.md) - change the template a workspace is tracking
2828
* [coder workspaces ls](coder_workspaces_ls.md) - list all workspaces owned by the active user
29+
* [coder workspaces policy-template](coder_workspaces_policy-template.md) - Set workspace policy template
2930
* [coder workspaces rebuild](coder_workspaces_rebuild.md) - rebuild a Coder workspace
3031
* [coder workspaces rm](coder_workspaces_rm.md) - remove Coder workspaces by name
3132
* [coder workspaces stop](coder_workspaces_stop.md) - stop Coder workspaces by name
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
## coder workspaces policy-template
2+
3+
Set workspace policy template
4+
5+
### Synopsis
6+
7+
Set workspace policy template or restore to default configuration. This feature is for site admins only.
8+
9+
```
10+
coder workspaces policy-template [flags]
11+
```
12+
13+
### Options
14+
15+
```
16+
--default Restore policy template to default configuration
17+
--dry-run skip setting policy template, but view errors/warnings about how this policy template would impact existing workspaces
18+
-f, --filepath string full path to local policy template file.
19+
-h, --help help for policy-template
20+
--scope string scope of impact for the policy template. Supported values: site (default "site")
21+
```
22+
23+
### Options inherited from parent commands
24+
25+
```
26+
-v, --verbose show verbose output
27+
```
28+
29+
### SEE ALSO
30+
31+
* [coder workspaces](coder_workspaces.md) - Interact with Coder workspaces
32+

internal/cmd/workspaces.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ func workspacesCmd() *cobra.Command {
4747
workspaceFromConfigCmd(true),
4848
workspaceFromConfigCmd(false),
4949
editWorkspaceCmd(),
50+
setPolicyTemplate(),
5051
)
5152
return cmd
5253
}
@@ -752,3 +753,85 @@ func buildUpdateReq(ctx context.Context, client coder.Client, conf updateConf) (
752753
}
753754
return &updateReq, nil
754755
}
756+
757+
func setPolicyTemplate() *cobra.Command {
758+
var (
759+
ref string
760+
repo string
761+
filepath string
762+
dryRun bool
763+
defaultTemplate bool
764+
scope string
765+
)
766+
767+
cmd := &cobra.Command{
768+
Use: "policy-template",
769+
Short: "Set workspace policy template",
770+
Long: "Set workspace policy template or restore to default configuration. This feature is for site admins only.",
771+
RunE: func(cmd *cobra.Command, args []string) error {
772+
ctx := cmd.Context()
773+
client, err := newClient(ctx, true)
774+
if err != nil {
775+
return err
776+
}
777+
778+
if scope != coder.TemplateScopeSite {
779+
return clog.Error("Invalid 'scope' value", "Valid scope values: site")
780+
}
781+
782+
if filepath == "" && !defaultTemplate {
783+
return clog.Error("Missing required parameter --filepath or --default", "Must specify a template to set")
784+
}
785+
786+
templateID := ""
787+
if filepath != "" {
788+
var rd io.Reader
789+
b, err := ioutil.ReadFile(filepath)
790+
if err != nil {
791+
return xerrors.Errorf("read local file: %w", err)
792+
}
793+
rd = bytes.NewReader(b)
794+
795+
req := coder.ParseTemplateRequest{
796+
RepoURL: repo,
797+
Ref: ref,
798+
Local: rd,
799+
OrgID: coder.SkipTemplateOrg,
800+
Filepath: ".coder/coder.yaml",
801+
}
802+
803+
version, err := client.ParseTemplate(ctx, req)
804+
if err != nil {
805+
return handleAPIError(err)
806+
}
807+
templateID = version.TemplateID
808+
}
809+
810+
resp, err := client.SetPolicyTemplate(ctx, templateID, coder.TemplateScope(scope), dryRun)
811+
if err != nil {
812+
return handleAPIError(err)
813+
}
814+
815+
for _, mc := range resp.MergeConflicts {
816+
workspace, err := client.WorkspaceByID(ctx, mc.WorkspaceID)
817+
if err != nil {
818+
fmt.Printf("Workspace %q:\n", mc.WorkspaceID)
819+
} else {
820+
fmt.Printf("Workspace %q in organization %q:\n", workspace.Name, workspace.OrganizationID)
821+
}
822+
823+
fmt.Println(mc.String())
824+
}
825+
826+
fmt.Println("Summary:")
827+
fmt.Println(coder.WorkspaceTemplateMergeConflicts(resp.MergeConflicts).Summary())
828+
829+
return nil
830+
},
831+
}
832+
cmd.Flags().BoolVarP(&dryRun, "dry-run", "", false, "skip setting policy template, but view errors/warnings about how this policy template would impact existing workspaces")
833+
cmd.Flags().StringVarP(&filepath, "filepath", "f", "", "full path to local policy template file.")
834+
cmd.Flags().StringVar(&scope, "scope", "site", "scope of impact for the policy template. Supported values: site")
835+
cmd.Flags().BoolVar(&defaultTemplate, "default", false, "Restore policy template to default configuration")
836+
return cmd
837+
}

0 commit comments

Comments
 (0)