Skip to content
This repository was archived by the owner on Aug 30, 2024. It is now read-only.
Merged
3 changes: 3 additions & 0 deletions coder-sdk/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -238,4 +238,7 @@ type Client interface {

// RenameWorkspaceProvider changes an existing providers name field.
RenameWorkspaceProvider(ctx context.Context, id string, name string) error

// SetPolicyTemplate sets the workspace policy template
SetPolicyTemplate(ctx context.Context, templateID string, templateScope TemplateScope, dryRun bool) (*SetPolicyTemplateResponse, error)
}
145 changes: 145 additions & 0 deletions coder-sdk/workspace.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package coder

import (
"context"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"

"cdr.dev/wsep"
Expand Down Expand Up @@ -364,3 +366,146 @@ func (c *DefaultClient) WorkspacesByWorkspaceProvider(ctx context.Context, wpID
}
return workspaces, nil
}

const (
// SkipTemplateOrg allows skipping checks on organizations.
SkipTemplateOrg = "SKIP_ORG"
)

type TemplateScope string

const (
// TemplateScopeSite is the scope for a site wide policy template.
TemplateScopeSite = "site"
)

type SetPolicyTemplateRequest struct {
TemplateID string `json:"template_id"`
Type string `json:"type"` // site, org
}

type SetPolicyTemplateResponse struct {
MergeConflicts []*WorkspaceTemplateMergeConflict `json:"merge_conflicts"`
}

type WorkspaceTemplateMergeConflict struct {
WorkspaceID string `json:"workspace_id"`
CurrentTemplateWarnings []string `json:"current_template_warnings"`
CurrentTemplateError *TplError `json:"current_template_errors"`
LatestTemplateWarnings []string `json:"latest_template_warnings"`
LatestTemplateError *TplError `json:"latest_template_errors"`
CurrentTemplateIsLatest bool `json:"current_template_is_latest"`
Message string `json:"message"`
}

func (mc WorkspaceTemplateMergeConflict) String() string {
var sb strings.Builder

if mc.Message != "" {
sb.WriteString(mc.Message)
}

currentConflicts := len(mc.CurrentTemplateWarnings) != 0 || mc.CurrentTemplateError != nil
updateConflicts := len(mc.LatestTemplateWarnings) != 0 || mc.LatestTemplateError != nil

if !currentConflicts && !updateConflicts {
sb.WriteString("No workspace conflicts\n")
return sb.String()
}

if currentConflicts {
if len(mc.CurrentTemplateWarnings) != 0 {
fmt.Fprintf(&sb, "Warnings: \n%s\n", strings.Join(mc.CurrentTemplateWarnings, "\n"))
}
if mc.CurrentTemplateError != nil {
fmt.Fprintf(&sb, "Errors: \n%s\n", strings.Join(mc.CurrentTemplateError.Msgs, "\n"))
}
}

if !mc.CurrentTemplateIsLatest && updateConflicts {
sb.WriteString("If workspace is updated to the latest template:\n")
if len(mc.LatestTemplateWarnings) != 0 {
fmt.Fprintf(&sb, "Warnings: \n%s\n", strings.Join(mc.LatestTemplateWarnings, "\n"))
}
if mc.LatestTemplateError != nil {
fmt.Fprintf(&sb, "Errors: \n%s\n", strings.Join(mc.LatestTemplateError.Msgs, "\n"))
}
}

return sb.String()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something that might be helpful is some sort of summary at the end. I have 200 workspaces in my dogfood, and it would be nice to see something like:

102 Workspaces will not be able to be rebuilt.
152 Workspaces have warnings but are still functional.

In the future, it would be great to merge-common errors and report which users are affected maybe? But for now I think a simple summary like that would be great.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a summary at the end.

Mine isn't too exciting bc I regularly delete my workspaces in my dev deployment, so I only have like 2.
I'm curious to see what you think of the summary when there are 200 🤔

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm very curious about why @Emyrk 200 workspaces 😂

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ha, my deployment has 200 workspaces because I was testing the response times of our apis at those numbers. I made a little deployment branch to make them fast, but I never made anything to delete them in bulk haha.

I figure I'll just keep em as a bit of a "stress test" for the UI

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The summary at 200 is not the most helpful thing, but I'm not worried about optimizing for 200 workspaces right now. Just thought a summary would be nice even for like ~30 workspaces which is reasonable at a company

}

type WorkspaceTemplateMergeConflicts []*WorkspaceTemplateMergeConflict

func (mcs WorkspaceTemplateMergeConflicts) Summary() string {
var (
sb strings.Builder
currentWarnings int
updateWarnings int
currentErrors int
updateErrors int
)

for _, mc := range mcs {
if len(mc.CurrentTemplateWarnings) != 0 {
currentWarnings++
}
if len(mc.LatestTemplateWarnings) != 0 {
updateWarnings++
}
if mc.CurrentTemplateError != nil {
currentErrors++
}
if mc.LatestTemplateError != nil {
updateErrors++
}
}

if currentErrors == 0 && updateErrors == 0 && currentWarnings == 0 && updateWarnings == 0 {
sb.WriteString("No workspace conflicts\n")
return sb.String()
}

if currentErrors != 0 {
fmt.Fprintf(&sb, "%d workspaces will not be able to be rebuilt\n", currentErrors)
}
if updateErrors != 0 {
fmt.Fprintf(&sb, "%d workspaces will not be able to be rebuilt if updated to the latest version\n", updateErrors)
}
if currentWarnings != 0 {
fmt.Fprintf(&sb, "%d workspaces will be impacted\n", currentWarnings)
}
if updateWarnings != 0 {
fmt.Fprintf(&sb, "%d workspaces will be impacted if updated to the latest version\n", updateWarnings)
}

return sb.String()
}

type TplError struct {
// Msgs are the human facing strings to present to the user. Since there can be multiple
// problems with a template, there might be multiple strings
Msgs []string `json:"messages"`
}

func (c *DefaultClient) SetPolicyTemplate(ctx context.Context, templateID string, templateScope TemplateScope, dryRun bool) (*SetPolicyTemplateResponse, error) {
var (
resp SetPolicyTemplateResponse
query = url.Values{}
)

req := SetPolicyTemplateRequest{
TemplateID: templateID,
Type: string(templateScope),
}

if dryRun {
query.Set("dry-run", "true")
}

if err := c.requestBody(ctx, http.MethodPost, "/api/private/workspaces/template/policy", req, &resp, withQueryParams(query)); err != nil {
return nil, err
}

return &resp, nil
}
1 change: 1 addition & 0 deletions docs/coder_workspaces.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Perform operations on the Coder workspaces owned by the active user.
* [coder workspaces edit](coder_workspaces_edit.md) - edit an existing workspace and initiate a rebuild.
* [coder workspaces edit-from-config](coder_workspaces_edit-from-config.md) - change the template a workspace is tracking
* [coder workspaces ls](coder_workspaces_ls.md) - list all workspaces owned by the active user
* [coder workspaces policy-template](coder_workspaces_policy-template.md) - Set workspace policy template
* [coder workspaces rebuild](coder_workspaces_rebuild.md) - rebuild a Coder workspace
* [coder workspaces rm](coder_workspaces_rm.md) - remove Coder workspaces by name
* [coder workspaces stop](coder_workspaces_stop.md) - stop Coder workspaces by name
Expand Down
32 changes: 32 additions & 0 deletions docs/coder_workspaces_policy-template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
## coder workspaces policy-template

Set workspace policy template

### Synopsis

Set workspace policy template or restore to default configuration. This feature is for site admins only.

```
coder workspaces policy-template [flags]
```

### Options

```
--default Restore policy template to default configuration
--dry-run skip setting policy template, but view errors/warnings about how this policy template would impact existing workspaces
-f, --filepath string full path to local policy template file.
-h, --help help for policy-template
--scope string scope of impact for the policy template. Supported values: site (default "site")
```

### Options inherited from parent commands

```
-v, --verbose show verbose output
```

### SEE ALSO

* [coder workspaces](coder_workspaces.md) - Interact with Coder workspaces

83 changes: 83 additions & 0 deletions internal/cmd/workspaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ func workspacesCmd() *cobra.Command {
workspaceFromConfigCmd(true),
workspaceFromConfigCmd(false),
editWorkspaceCmd(),
setPolicyTemplate(),
)
return cmd
}
Expand Down Expand Up @@ -752,3 +753,85 @@ func buildUpdateReq(ctx context.Context, client coder.Client, conf updateConf) (
}
return &updateReq, nil
}

func setPolicyTemplate() *cobra.Command {
var (
ref string
repo string
filepath string
dryRun bool
defaultTemplate bool
scope string
)

cmd := &cobra.Command{
Use: "policy-template",
Short: "Set workspace policy template",
Long: "Set workspace policy template or restore to default configuration. This feature is for site admins only.",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
client, err := newClient(ctx, true)
if err != nil {
return err
}

if scope != coder.TemplateScopeSite {
return clog.Error("Invalid 'scope' value", "Valid scope values: site")
}

if filepath == "" && !defaultTemplate {
return clog.Error("Missing required parameter --filepath or --default", "Must specify a template to set")
}

templateID := ""
if filepath != "" {
var rd io.Reader
b, err := ioutil.ReadFile(filepath)
if err != nil {
return xerrors.Errorf("read local file: %w", err)
}
rd = bytes.NewReader(b)

req := coder.ParseTemplateRequest{
RepoURL: repo,
Ref: ref,
Local: rd,
OrgID: coder.SkipTemplateOrg,
Filepath: ".coder/coder.yaml",
}

version, err := client.ParseTemplate(ctx, req)
if err != nil {
return handleAPIError(err)
}
templateID = version.TemplateID
}

resp, err := client.SetPolicyTemplate(ctx, templateID, coder.TemplateScope(scope), dryRun)
if err != nil {
return handleAPIError(err)
}

for _, mc := range resp.MergeConflicts {
workspace, err := client.WorkspaceByID(ctx, mc.WorkspaceID)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the extra details. In my dogfood of 200 workspaces this was still pretty quick 👍

if err != nil {
fmt.Printf("Workspace %q:\n", mc.WorkspaceID)
} else {
fmt.Printf("Workspace %q in organization %q:\n", workspace.Name, workspace.OrganizationID)
}

fmt.Println(mc.String())
}

fmt.Println("Summary:")
fmt.Println(coder.WorkspaceTemplateMergeConflicts(resp.MergeConflicts).Summary())

return nil
},
}
cmd.Flags().BoolVarP(&dryRun, "dry-run", "", false, "skip setting policy template, but view errors/warnings about how this policy template would impact existing workspaces")
cmd.Flags().StringVarP(&filepath, "filepath", "f", "", "full path to local policy template file.")
cmd.Flags().StringVar(&scope, "scope", "site", "scope of impact for the policy template. Supported values: site")
cmd.Flags().BoolVar(&defaultTemplate, "default", false, "Restore policy template to default configuration")
return cmd
}