From 5e8927e73e4d2492b9cbc65f1f1b04134d37ae58 Mon Sep 17 00:00:00 2001 From: Garrett Date: Fri, 5 Feb 2021 21:50:20 +0000 Subject: [PATCH] Add commands for workspace providers --- .golangci.yml | 1 - coder-sdk/resourcepools.go | 65 ------------- coder-sdk/workspace_providers.go | 80 ++++++++++++++++ internal/cmd/cmd.go | 1 + internal/cmd/configssh.go | 24 ++--- internal/cmd/providers.go | 154 +++++++++++++++++++++++++++++++ internal/coderutil/env.go | 42 ++++----- 7 files changed, 268 insertions(+), 99 deletions(-) delete mode 100644 coder-sdk/resourcepools.go create mode 100644 coder-sdk/workspace_providers.go create mode 100644 internal/cmd/providers.go diff --git a/.golangci.yml b/.golangci.yml index bfe48ebf..423540d5 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -39,7 +39,6 @@ linters: - structcheck - stylecheck - typecheck - - noctx - nolintlint - rowserrcheck - scopelint diff --git a/coder-sdk/resourcepools.go b/coder-sdk/resourcepools.go deleted file mode 100644 index 2556aa37..00000000 --- a/coder-sdk/resourcepools.go +++ /dev/null @@ -1,65 +0,0 @@ -package coder - -import ( - "context" - "net/http" -) - -// ResourcePool defines an entity capable of deploying and acting as an ingress for Coder environments. -type ResourcePool struct { - ID string `json:"id"` - Name string `json:"name"` - Local bool `json:"local"` - ClusterAddress string `json:"cluster_address"` - DefaultNamespace string `json:"default_namespace"` - StorageClass string `json:"storage_class"` - ClusterDomainSuffix string `json:"cluster_domain_suffix"` - DevurlHost string `json:"devurl_host"` - NamespaceWhitelist []string `json:"namespace_whitelist"` - OrgWhitelist []string `json:"org_whitelist"` - SSHEnabled bool `json:"ssh_enabled"` - AccessURL string `json:"envproxy_access_url"` -} - -// ResourcePoolByID fetches a resource pool entity by its unique ID. -func (c *Client) ResourcePoolByID(ctx context.Context, id string) (*ResourcePool, error) { - var rp ResourcePool - if err := c.requestBody(ctx, http.MethodGet, "/api/private/resource-pools/"+id, nil, &rp); err != nil { - return nil, err - } - return &rp, nil -} - -// DeleteResourcePoolByID deletes a resource pool entity from the Coder control plane. -func (c *Client) DeleteResourcePoolByID(ctx context.Context, id string) error { - return c.requestBody(ctx, http.MethodDelete, "/api/private/resource-pools/"+id, nil, nil) -} - -// ResourcePools fetches all resource pools known to the Coder control plane. -func (c *Client) ResourcePools(ctx context.Context) ([]ResourcePool, error) { - var pools []ResourcePool - if err := c.requestBody(ctx, http.MethodGet, "/api/private/resource-pools", nil, &pools); err != nil { - return nil, err - } - return pools, nil -} - -// CreateResourcePoolReq defines the request parameters for creating a new resource pool entity. -type CreateResourcePoolReq struct { - Name string `json:"name"` - Local bool `json:"local"` - ClusterCA string `json:"cluster_ca"` - ClusterAddress string `json:"cluster_address"` - SAToken string `json:"sa_token"` - DefaultNamespace string `json:"default_namespace"` - StorageClass string `json:"storage_class"` - ClusterDomainSuffix string `json:"cluster_domain_suffix"` - DevurlHost string `json:"devurl_host"` - NamespaceWhitelist []string `json:"namespace_whitelist"` - OrgWhitelist []string `json:"org_whitelist"` -} - -// CreateResourcePool creates a new ResourcePool entity. -func (c *Client) CreateResourcePool(ctx context.Context, req CreateResourcePoolReq) error { - return c.requestBody(ctx, http.MethodPost, "/api/private/resource-pools", req, nil) -} diff --git a/coder-sdk/workspace_providers.go b/coder-sdk/workspace_providers.go new file mode 100644 index 00000000..d397c9f5 --- /dev/null +++ b/coder-sdk/workspace_providers.go @@ -0,0 +1,80 @@ +package coder + +import ( + "context" + "net/http" +) + +// WorkspaceProvider defines an entity capable of deploying and acting as an ingress for Coder environments. +type WorkspaceProvider struct { + ID string `json:"id" table:"-"` + Name string `json:"name" table:"Name"` + Status WorkspaceProviderStatus `json:"status" table:"Status"` + Local bool `json:"local" table:"-"` + ClusterAddress string `json:"cluster_address" table:"Cluster Address"` + DefaultNamespace string `json:"default_namespace" table:"Namespace"` + StorageClass string `json:"storage_class" table:"Storage Class"` + ClusterDomainSuffix string `json:"cluster_domain_suffix" table:"Cluster Domain Suffix"` + EnvproxyAccessURL string `json:"envproxy_access_url" validate:"required" table:"Access URL"` + DevurlHost string `json:"devurl_host" table:"Devurl Host"` + SSHEnabled bool `json:"ssh_enabled" table:"SSH Enabled"` + NamespaceWhitelist []string `json:"namespace_whitelist" table:"Namespace Allowlist"` + OrgWhitelist []string `json:"org_whitelist" table:"-"` +} + +// WorkspaceProviderStatus represents the configuration state of a workspace provider. +type WorkspaceProviderStatus string + +// Workspace Provider statuses. +const ( + WorkspaceProviderPending WorkspaceProviderStatus = "pending" + WorkspaceProviderReady WorkspaceProviderStatus = "ready" +) + +// WorkspaceProviderByID fetches a workspace provider entity by its unique ID. +func (c *Client) WorkspaceProviderByID(ctx context.Context, id string) (*WorkspaceProvider, error) { + var wp WorkspaceProvider + err := c.requestBody(ctx, http.MethodGet, "/api/private/resource-pools/"+id, nil, &wp) + if err != nil { + return nil, err + } + return &wp, nil +} + +// WorkspaceProviders fetches all workspace providers known to the Coder control plane. +func (c *Client) WorkspaceProviders(ctx context.Context) ([]WorkspaceProvider, error) { + var providers []WorkspaceProvider + err := c.requestBody(ctx, http.MethodGet, "/api/private/resource-pools", nil, &providers) + if err != nil { + return nil, err + } + return providers, nil +} + +// CreateWorkspaceProviderReq defines the request parameters for creating a new workspace provider entity. +type CreateWorkspaceProviderReq struct { + Name string `json:"name"` +} + +// CreateWorkspaceProviderRes defines the response from creating a new workspace provider entity. +type CreateWorkspaceProviderRes struct { + ID string `json:"id" table:"ID"` + Name string `json:"name" table:"Name"` + Status WorkspaceProviderStatus `json:"status" table:"Status"` + EnvproxyToken string `json:"envproxy_token" table:"Envproxy Token"` +} + +// CreateWorkspaceProvider creates a new WorkspaceProvider entity. +func (c *Client) CreateWorkspaceProvider(ctx context.Context, req CreateWorkspaceProviderReq) (*CreateWorkspaceProviderRes, error) { + var res CreateWorkspaceProviderRes + err := c.requestBody(ctx, http.MethodPost, "/api/private/resource-pools", req, &res) + if err != nil { + return nil, err + } + return &res, nil +} + +// DeleteWorkspaceProviderByID deletes a workspace provider entity from the Coder control plane. +func (c *Client) DeleteWorkspaceProviderByID(ctx context.Context, id string) error { + return c.requestBody(ctx, http.MethodDelete, "/api/private/resource-pools/"+id, nil, nil) +} diff --git a/internal/cmd/cmd.go b/internal/cmd/cmd.go index c268ff49..ddc39c9c 100644 --- a/internal/cmd/cmd.go +++ b/internal/cmd/cmd.go @@ -37,6 +37,7 @@ func Make() *cobra.Command { resourceCmd(), completionCmd(), imgsCmd(), + providersCmd(), genDocsCmd(app), ) app.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "show verbose output") diff --git a/internal/cmd/configssh.go b/internal/cmd/configssh.go index 26cb0afe..a63d2b3a 100644 --- a/internal/cmd/configssh.go +++ b/internal/cmd/configssh.go @@ -104,16 +104,16 @@ func configSSH(configpath *string, remove *bool) func(cmd *cobra.Command, _ []st return xerrors.New("no environments found") } - envsWithPools, err := coderutil.EnvsWithPool(ctx, client, envs) + envsWithProviders, err := coderutil.EnvsWithProvider(ctx, client, envs) if err != nil { - return xerrors.Errorf("resolve env pools: %w", err) + return xerrors.Errorf("resolve env workspace providers: %w", err) } - if !sshAvailable(envsWithPools) { + if !sshAvailable(envsWithProviders) { return xerrors.New("SSH is disabled or not available for any environments in your Coder Enterprise deployment.") } - newConfig := makeNewConfigs(user.Username, envsWithPools, privateKeyFilepath) + newConfig := makeNewConfigs(user.Username, envsWithProviders, privateKeyFilepath) err = os.MkdirAll(filepath.Dir(*configpath), os.ModePerm) if err != nil { @@ -157,9 +157,9 @@ func removeOldConfig(config string) (string, bool) { } // sshAvailable returns true if SSH is available for at least one environment. -func sshAvailable(envs []coderutil.EnvWithPool) bool { +func sshAvailable(envs []coderutil.EnvWithWorkspaceProvider) bool { for _, env := range envs { - if env.Pool.SSHEnabled { + if env.WorkspaceProvider.SSHEnabled { return true } } @@ -174,19 +174,19 @@ func writeSSHKey(ctx context.Context, client *coder.Client, privateKeyPath strin return ioutil.WriteFile(privateKeyPath, []byte(key.PrivateKey), 0600) } -func makeNewConfigs(userName string, envs []coderutil.EnvWithPool, privateKeyFilepath string) string { +func makeNewConfigs(userName string, envs []coderutil.EnvWithWorkspaceProvider, privateKeyFilepath string) string { newConfig := fmt.Sprintf("\n%s\n%s\n\n", sshStartToken, sshStartMessage) for _, env := range envs { - if !env.Pool.SSHEnabled { - clog.LogWarn(fmt.Sprintf("SSH is not enabled for pool %q", env.Pool.Name), + if !env.WorkspaceProvider.SSHEnabled { + clog.LogWarn(fmt.Sprintf("SSH is not enabled for workspace provider %q", env.WorkspaceProvider.Name), clog.BlankLine, - clog.Tipf("ask an infrastructure administrator to enable SSH for this resource pool"), + clog.Tipf("ask an infrastructure administrator to enable SSH for this workspace provider"), ) continue } - u, err := url.Parse(env.Pool.AccessURL) + u, err := url.Parse(env.WorkspaceProvider.EnvproxyAccessURL) if err != nil { - clog.LogWarn("invalid access url", clog.Causef("malformed url: %q", env.Pool.AccessURL)) + clog.LogWarn("invalid access url", clog.Causef("malformed url: %q", env.WorkspaceProvider.EnvproxyAccessURL)) continue } newConfig += makeSSHConfig(u.Host, userName, env.Env.Name, privateKeyFilepath) diff --git a/internal/cmd/providers.go b/internal/cmd/providers.go new file mode 100644 index 00000000..8d5ff12b --- /dev/null +++ b/internal/cmd/providers.go @@ -0,0 +1,154 @@ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" + "golang.org/x/xerrors" + + "cdr.dev/coder-cli/coder-sdk" + "cdr.dev/coder-cli/internal/x/xcobra" + "cdr.dev/coder-cli/pkg/clog" + "cdr.dev/coder-cli/pkg/tablewriter" +) + +func providersCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "providers", + Short: "Interact with Coder workspace providers", + Long: "Perform operations on the Coder Workspace Providers for the platform.", + Hidden: true, + } + + cmd.AddCommand( + createProviderCmd(), + listProviderCmd(), + deleteProviderCmd(), + ) + return cmd +} + +func createProviderCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "create [workspace_provider_name]", + Short: "create a new workspace provider.", + Args: xcobra.ExactArgs(1), + Long: "Create a new Coder workspace provider.", + Example: `# create a new workspace provider in a pending state +coder providers create my-new-workspace-provider`, + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + + client, err := newClient(ctx) + if err != nil { + return err + } + + // ExactArgs(1) ensures our name value can't panic on an out of bounds. + createReq := &coder.CreateWorkspaceProviderReq{ + Name: args[0], + } + + wp, err := client.CreateWorkspaceProvider(ctx, *createReq) + if err != nil { + return xerrors.Errorf("create workspace provider: %w", err) + } + + err = tablewriter.WriteTable(1, func(i int) interface{} { + return *wp + }) + if err != nil { + return xerrors.Errorf("write table: %w", err) + } + return nil + }, + } + return cmd +} + +func listProviderCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "ls", + Short: "list workspace providers.", + Long: "List all Coder workspace providers.", + Example: `# list workspace providers +coder providers ls`, + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + + client, err := newClient(ctx) + if err != nil { + return err + } + + wps, err := client.WorkspaceProviders(ctx) + if err != nil { + return xerrors.Errorf("list workspace providers: %w", err) + } + + err = tablewriter.WriteTable(len(wps), func(i int) interface{} { + return wps[i] + }) + if err != nil { + return xerrors.Errorf("write table: %w", err) + } + return nil + }, + } + return cmd +} + +func deleteProviderCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "rm [workspace_provider_name]", + Short: "remove a workspace provider.", + Long: "Remove an existing Coder workspace provider by name.", + Example: `# remove an existing workspace provider by name +coder providers rm my-workspace-provider`, + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + client, err := newClient(ctx) + if err != nil { + return err + } + + wps, err := client.WorkspaceProviders(ctx) + if err != nil { + return xerrors.Errorf("listing workspace providers: %w", err) + } + + egroup := clog.LoggedErrGroup() + for _, wpName := range args { + name := wpName + egroup.Go(func() error { + var id string + for _, wp := range wps { + if wp.Name == name { + id = wp.ID + } + } + if id == "" { + return clog.Error( + fmt.Sprintf(`failed to remove workspace provider "%s"`, name), + clog.Causef(`no workspace provider found by name "%s"`, name), + ) + } + + err = client.DeleteWorkspaceProviderByID(ctx, id) + if err != nil { + return clog.Error( + fmt.Sprintf(`failed to remove workspace provider "%s"`, name), + clog.Causef(err.Error()), + ) + } + + clog.LogSuccess(fmt.Sprintf(`removed workspace provider with name "%s"`, name)) + + return nil + }) + } + return egroup.Wait() + }, + } + return cmd +} diff --git a/internal/coderutil/env.go b/internal/coderutil/env.go index c961fa56..784b606d 100644 --- a/internal/coderutil/env.go +++ b/internal/coderutil/env.go @@ -11,15 +11,15 @@ import ( ) // DialEnvWsep dials the executor endpoint using the https://github.com/cdr/wsep message protocol. -// The proper resource pool access URL is used. +// The proper workspace provider envproxy access URL is used. func DialEnvWsep(ctx context.Context, client *coder.Client, env *coder.Environment) (*websocket.Conn, error) { - resourcePool, err := client.ResourcePoolByID(ctx, env.ResourcePoolID) + workspaceProvider, err := client.WorkspaceProviderByID(ctx, env.ResourcePoolID) if err != nil { - return nil, xerrors.Errorf("get env resource pool: %w", err) + return nil, xerrors.Errorf("get env workspace provider: %w", err) } - accessURL, err := url.Parse(resourcePool.AccessURL) + accessURL, err := url.Parse(workspaceProvider.EnvproxyAccessURL) if err != nil { - return nil, xerrors.Errorf("invalid resource pool access url: %w", err) + return nil, xerrors.Errorf("invalid workspace provider envproxy access url: %w", err) } conn, err := client.DialWsep(ctx, accessURL, env.ID) @@ -29,31 +29,31 @@ func DialEnvWsep(ctx context.Context, client *coder.Client, env *coder.Environme return conn, nil } -// EnvWithPool composes an Environment entity with its associated ResourcePool. -type EnvWithPool struct { - Env coder.Environment - Pool coder.ResourcePool +// EnvWithWorkspaceProvider composes an Environment entity with its associated WorkspaceProvider. +type EnvWithWorkspaceProvider struct { + Env coder.Environment + WorkspaceProvider coder.WorkspaceProvider } -// EnvsWithPool performs the composition of each Environment with its associated ResourcePool. -func EnvsWithPool(ctx context.Context, client *coder.Client, envs []coder.Environment) ([]EnvWithPool, error) { - pooledEnvs := make([]EnvWithPool, 0, len(envs)) - pools, err := client.ResourcePools(ctx) +// EnvsWithProvider performs the composition of each Environment with its associated WorkspaceProvider. +func EnvsWithProvider(ctx context.Context, client *coder.Client, envs []coder.Environment) ([]EnvWithWorkspaceProvider, error) { + pooledEnvs := make([]EnvWithWorkspaceProvider, 0, len(envs)) + providers, err := client.WorkspaceProviders(ctx) if err != nil { return nil, err } - poolMap := make(map[string]coder.ResourcePool, len(pools)) - for _, p := range pools { - poolMap[p.ID] = p + providerMap := make(map[string]coder.WorkspaceProvider, len(providers)) + for _, p := range providers { + providerMap[p.ID] = p } for _, e := range envs { - envPool, ok := poolMap[e.ResourcePoolID] + envProvider, ok := providerMap[e.ResourcePoolID] if !ok { - return nil, xerrors.Errorf("fetch env resource pool: %w", coder.ErrNotFound) + return nil, xerrors.Errorf("fetch env workspace provider: %w", coder.ErrNotFound) } - pooledEnvs = append(pooledEnvs, EnvWithPool{ - Env: e, - Pool: envPool, + pooledEnvs = append(pooledEnvs, EnvWithWorkspaceProvider{ + Env: e, + WorkspaceProvider: envProvider, }) } return pooledEnvs, nil