mirror of
https://github.com/coder/coder.git
synced 2026-06-03 04:58:23 +00:00
bddb808b25
Fixes all our Go file imports to match the preferred spec that we've _mostly_ been using. For example: ``` import ( "context" "time" "github.com/prometheus/client_golang/prometheus" "golang.org/x/xerrors" "gopkg.in/natefinch/lumberjack.v2" "cdr.dev/slog/v3" "github.com/coder/coder/v2/codersdk/agentsdk" "github.com/coder/serpent" ) ``` 3 groups: standard library, 3rd partly libs, Coder libs. This PR makes the change across the codebase. The PR in the stack above modifies our formatting to maintain this state of affairs, and is a separate PR so it's possible to review that one in detail.
470 lines
13 KiB
Go
470 lines
13 KiB
Go
package cli
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/fatih/color"
|
|
"golang.org/x/xerrors"
|
|
|
|
"github.com/coder/coder/v2/cli/cliui"
|
|
"github.com/coder/coder/v2/codersdk"
|
|
"github.com/coder/pretty"
|
|
"github.com/coder/serpent"
|
|
)
|
|
|
|
func (r *RootCmd) workspaceProxy() *serpent.Command {
|
|
cmd := &serpent.Command{
|
|
Use: "workspace-proxy",
|
|
Short: "Workspace proxies provide low-latency experiences for geo-distributed teams.",
|
|
Long: "Workspace proxies provide low-latency experiences for geo-distributed teams. " +
|
|
"It will act as a connection gateway to your workspace. " +
|
|
"Best used if Coder and your workspace are deployed in different regions.",
|
|
Aliases: []string{"wsproxy"},
|
|
Hidden: true,
|
|
Handler: func(inv *serpent.Invocation) error {
|
|
return inv.Command.HelpHandler(inv)
|
|
},
|
|
Children: []*serpent.Command{
|
|
r.proxyServer(),
|
|
r.createProxy(),
|
|
r.deleteProxy(),
|
|
r.listProxies(),
|
|
r.patchProxy(),
|
|
r.regenerateProxyToken(),
|
|
},
|
|
}
|
|
|
|
return cmd
|
|
}
|
|
|
|
func (r *RootCmd) regenerateProxyToken() *serpent.Command {
|
|
formatter := newUpdateProxyResponseFormatter()
|
|
cmd := &serpent.Command{
|
|
Use: "regenerate-token <name|id>",
|
|
Short: "Regenerate a workspace proxy authentication token. " +
|
|
"This will invalidate the existing authentication token.",
|
|
Middleware: serpent.Chain(
|
|
serpent.RequireNArgs(1),
|
|
),
|
|
Handler: func(inv *serpent.Invocation) error {
|
|
ctx := inv.Context()
|
|
client, err := r.InitClient(inv)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
formatter.primaryAccessURL = client.URL.String()
|
|
// This is cheeky, but you can also use a uuid string in
|
|
// 'DeleteWorkspaceProxyByName' and it will work.
|
|
proxy, err := client.WorkspaceProxyByName(ctx, inv.Args[0])
|
|
if err != nil {
|
|
return xerrors.Errorf("fetch workspace proxy %q: %w", inv.Args[0], err)
|
|
}
|
|
|
|
// Only regenerate the token
|
|
updated, err := client.PatchWorkspaceProxy(ctx, codersdk.PatchWorkspaceProxy{
|
|
ID: proxy.ID,
|
|
Name: proxy.Name,
|
|
DisplayName: proxy.DisplayName,
|
|
Icon: proxy.IconURL,
|
|
RegenerateToken: true,
|
|
})
|
|
if err != nil {
|
|
return xerrors.Errorf("update workspace proxy %q: %w", inv.Args[0], err)
|
|
}
|
|
|
|
output, err := formatter.Format(ctx, updated)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = fmt.Fprintln(inv.Stdout, output)
|
|
return err
|
|
},
|
|
}
|
|
formatter.AttachOptions(&cmd.Options)
|
|
|
|
return cmd
|
|
}
|
|
|
|
func (r *RootCmd) patchProxy() *serpent.Command {
|
|
var (
|
|
proxyName string
|
|
displayName string
|
|
proxyIcon string
|
|
formatter = cliui.NewOutputFormatter(
|
|
// Text formatter should be human readable.
|
|
cliui.ChangeFormatterData(cliui.TextFormat(), func(data any) (any, error) {
|
|
response, ok := data.(codersdk.WorkspaceProxy)
|
|
if !ok {
|
|
return nil, xerrors.Errorf("unexpected type %T", data)
|
|
}
|
|
return fmt.Sprintf("Workspace Proxy %q updated successfully.", response.Name), nil
|
|
}),
|
|
cliui.JSONFormat(),
|
|
// Table formatter expects a slice, make a slice of one.
|
|
cliui.ChangeFormatterData(cliui.TableFormat([]codersdk.WorkspaceProxy{}, []string{"name", "url"}),
|
|
func(data any) (any, error) {
|
|
response, ok := data.(codersdk.WorkspaceProxy)
|
|
if !ok {
|
|
return nil, xerrors.Errorf("unexpected type %T", data)
|
|
}
|
|
return []codersdk.WorkspaceProxy{response}, nil
|
|
}),
|
|
)
|
|
)
|
|
cmd := &serpent.Command{
|
|
Use: "edit <name|id>",
|
|
Short: "Edit a workspace proxy",
|
|
Middleware: serpent.Chain(
|
|
serpent.RequireNArgs(1),
|
|
),
|
|
Handler: func(inv *serpent.Invocation) error {
|
|
ctx := inv.Context()
|
|
client, err := r.InitClient(inv)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if proxyIcon == "" && displayName == "" && proxyName == "" {
|
|
_ = inv.Command.HelpHandler(inv)
|
|
return xerrors.Errorf("specify at least one field to update")
|
|
}
|
|
|
|
// This is cheeky, but you can also use a uuid string in
|
|
// 'DeleteWorkspaceProxyByName' and it will work.
|
|
proxy, err := client.WorkspaceProxyByName(ctx, inv.Args[0])
|
|
if err != nil {
|
|
return xerrors.Errorf("fetch workspace proxy %q: %w", inv.Args[0], err)
|
|
}
|
|
|
|
// Use the existing values if the user didn't specify them.
|
|
if proxyName == "" {
|
|
proxyName = proxy.Name
|
|
}
|
|
if displayName == "" {
|
|
displayName = proxy.DisplayName
|
|
}
|
|
if proxyIcon == "" {
|
|
proxyIcon = proxy.IconURL
|
|
}
|
|
|
|
updated, err := client.PatchWorkspaceProxy(ctx, codersdk.PatchWorkspaceProxy{
|
|
ID: proxy.ID,
|
|
Name: proxyName,
|
|
DisplayName: displayName,
|
|
Icon: proxyIcon,
|
|
})
|
|
if err != nil {
|
|
return xerrors.Errorf("update workspace proxy %q: %w", inv.Args[0], err)
|
|
}
|
|
|
|
output, err := formatter.Format(ctx, updated.Proxy)
|
|
if err != nil {
|
|
return xerrors.Errorf("format response: %w", err)
|
|
}
|
|
_, err = fmt.Fprintln(inv.Stdout, output)
|
|
return err
|
|
},
|
|
}
|
|
|
|
formatter.AttachOptions(&cmd.Options)
|
|
cmd.Options.Add(
|
|
serpent.Option{
|
|
Flag: "name",
|
|
Description: "(Optional) Name of the proxy. This is used to identify the proxy.",
|
|
Value: serpent.StringOf(&proxyName),
|
|
},
|
|
serpent.Option{
|
|
Flag: "display-name",
|
|
Description: "(Optional) Display of the proxy. A more human friendly name to be displayed.",
|
|
Value: serpent.StringOf(&displayName),
|
|
},
|
|
serpent.Option{
|
|
Flag: "icon",
|
|
Description: "(Optional) Display icon of the proxy.",
|
|
Value: serpent.StringOf(&proxyIcon),
|
|
},
|
|
)
|
|
|
|
return cmd
|
|
}
|
|
|
|
func (r *RootCmd) deleteProxy() *serpent.Command {
|
|
cmd := &serpent.Command{
|
|
Use: "delete <name|id>",
|
|
Short: "Delete a workspace proxy",
|
|
Options: serpent.OptionSet{
|
|
cliui.SkipPromptOption(),
|
|
},
|
|
Middleware: serpent.Chain(
|
|
serpent.RequireNArgs(1),
|
|
),
|
|
Handler: func(inv *serpent.Invocation) error {
|
|
ctx := inv.Context()
|
|
client, err := r.InitClient(inv)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
wsproxy, err := client.WorkspaceProxyByName(ctx, inv.Args[0])
|
|
if err != nil {
|
|
return xerrors.Errorf("fetch workspace proxy %q: %w", inv.Args[0], err)
|
|
}
|
|
|
|
// Confirm deletion of the template.
|
|
_, err = cliui.Prompt(inv, cliui.PromptOptions{
|
|
Text: fmt.Sprintf("Delete this workspace proxy: %s?", pretty.Sprint(cliui.DefaultStyles.Code, wsproxy.DisplayName)),
|
|
IsConfirm: true,
|
|
Default: cliui.ConfirmNo,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = client.DeleteWorkspaceProxyByName(ctx, inv.Args[0])
|
|
if err != nil {
|
|
return xerrors.Errorf("delete workspace proxy %q: %w", inv.Args[0], err)
|
|
}
|
|
|
|
_, _ = fmt.Fprintf(inv.Stdout, "Workspace proxy %q deleted successfully\n", inv.Args[0])
|
|
return nil
|
|
},
|
|
}
|
|
|
|
return cmd
|
|
}
|
|
|
|
func (r *RootCmd) createProxy() *serpent.Command {
|
|
var (
|
|
proxyName string
|
|
displayName string
|
|
proxyIcon string
|
|
noPrompts bool
|
|
formatter = newUpdateProxyResponseFormatter()
|
|
)
|
|
validateIcon := func(s *serpent.String) error {
|
|
if !(strings.HasPrefix(s.Value(), "/emojis/") || strings.HasPrefix(s.Value(), "http")) {
|
|
return xerrors.New("icon must be a relative path to an emoji or a publicly hosted image URL")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
cmd := &serpent.Command{
|
|
Use: "create",
|
|
Short: "Create a workspace proxy",
|
|
Middleware: serpent.Chain(
|
|
serpent.RequireNArgs(0),
|
|
),
|
|
Handler: func(inv *serpent.Invocation) error {
|
|
ctx := inv.Context()
|
|
client, err := r.InitClient(inv)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
formatter.primaryAccessURL = client.URL.String()
|
|
if proxyName == "" && !noPrompts {
|
|
proxyName, err = cliui.Prompt(inv, cliui.PromptOptions{
|
|
Text: "Proxy Name:",
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if displayName == "" && !noPrompts {
|
|
displayName, err = cliui.Prompt(inv, cliui.PromptOptions{
|
|
Text: "Display Name:",
|
|
Default: proxyName,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if proxyIcon == "" && !noPrompts {
|
|
proxyIcon, err = cliui.Prompt(inv, cliui.PromptOptions{
|
|
Text: "Icon URL:",
|
|
Default: "/emojis/1f5fa.png",
|
|
Validate: func(s string) error {
|
|
return validateIcon(serpent.StringOf(&s))
|
|
},
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if proxyName == "" {
|
|
return xerrors.New("proxy name is required")
|
|
}
|
|
|
|
resp, err := client.CreateWorkspaceProxy(ctx, codersdk.CreateWorkspaceProxyRequest{
|
|
Name: proxyName,
|
|
DisplayName: displayName,
|
|
Icon: proxyIcon,
|
|
})
|
|
if err != nil {
|
|
return xerrors.Errorf("create workspace proxy: %w", err)
|
|
}
|
|
|
|
output, err := formatter.Format(ctx, resp)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = fmt.Fprintln(inv.Stdout, output)
|
|
return err
|
|
},
|
|
}
|
|
|
|
formatter.AttachOptions(&cmd.Options)
|
|
cmd.Options.Add(
|
|
serpent.Option{
|
|
Flag: "name",
|
|
Description: "Name of the proxy. This is used to identify the proxy.",
|
|
Value: serpent.StringOf(&proxyName),
|
|
},
|
|
serpent.Option{
|
|
Flag: "display-name",
|
|
Description: "Display of the proxy. If omitted, the name is reused as the display name.",
|
|
Value: serpent.StringOf(&displayName),
|
|
},
|
|
serpent.Option{
|
|
Flag: "icon",
|
|
Description: "Display icon of the proxy.",
|
|
Value: serpent.Validate(serpent.StringOf(&proxyIcon), validateIcon),
|
|
},
|
|
serpent.Option{
|
|
Flag: "no-prompt",
|
|
Description: "Disable all input prompting, and fail if any required flags are missing.",
|
|
Value: serpent.BoolOf(&noPrompts),
|
|
},
|
|
)
|
|
return cmd
|
|
}
|
|
|
|
func (r *RootCmd) listProxies() *serpent.Command {
|
|
formatter := cliui.NewOutputFormatter(
|
|
cliui.TableFormat([]codersdk.WorkspaceProxy{}, []string{"name", "url", "proxy status"}),
|
|
cliui.JSONFormat(),
|
|
cliui.ChangeFormatterData(cliui.TextFormat(), func(data any) (any, error) {
|
|
resp, ok := data.([]codersdk.WorkspaceProxy)
|
|
if !ok {
|
|
return nil, xerrors.Errorf("unexpected type %T", data)
|
|
}
|
|
var str strings.Builder
|
|
_, _ = str.WriteString("Workspace Proxies:\n")
|
|
sep := ""
|
|
for i, proxy := range resp {
|
|
_, _ = str.WriteString(sep)
|
|
_, _ = str.WriteString(fmt.Sprintf("%d: %s %s %s", i, proxy.Name, proxy.PathAppURL, proxy.Status.Status))
|
|
for _, errMsg := range proxy.Status.Report.Errors {
|
|
_, _ = str.WriteString(color.RedString("\n\tErr: %s", errMsg))
|
|
}
|
|
for _, warnMsg := range proxy.Status.Report.Errors {
|
|
_, _ = str.WriteString(color.YellowString("\n\tWarn: %s", warnMsg))
|
|
}
|
|
sep = "\n"
|
|
}
|
|
return str.String(), nil
|
|
}),
|
|
)
|
|
|
|
cmd := &serpent.Command{
|
|
Use: "ls",
|
|
Aliases: []string{"list"},
|
|
Short: "List all workspace proxies",
|
|
Middleware: serpent.Chain(
|
|
serpent.RequireNArgs(0),
|
|
),
|
|
Handler: func(inv *serpent.Invocation) error {
|
|
ctx := inv.Context()
|
|
client, err := r.InitClient(inv)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
proxies, err := client.WorkspaceProxies(ctx)
|
|
if err != nil {
|
|
return xerrors.Errorf("list workspace proxies: %w", err)
|
|
}
|
|
|
|
output, err := formatter.Format(ctx, proxies.Regions)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if output == "" {
|
|
cliui.Infof(inv.Stderr, "No workspace proxies found.")
|
|
return nil
|
|
}
|
|
|
|
_, err = fmt.Fprintln(inv.Stdout, output)
|
|
return err
|
|
},
|
|
}
|
|
|
|
formatter.AttachOptions(&cmd.Options)
|
|
return cmd
|
|
}
|
|
|
|
// updateProxyResponseFormatter is used for both create and regenerate proxy commands.
|
|
type updateProxyResponseFormatter struct {
|
|
onlyToken bool
|
|
formatter *cliui.OutputFormatter
|
|
primaryAccessURL string
|
|
}
|
|
|
|
func (f *updateProxyResponseFormatter) Format(ctx context.Context, data codersdk.UpdateWorkspaceProxyResponse) (string, error) {
|
|
if f.onlyToken {
|
|
return data.ProxyToken, nil
|
|
}
|
|
return f.formatter.Format(ctx, data)
|
|
}
|
|
|
|
func (f *updateProxyResponseFormatter) AttachOptions(opts *serpent.OptionSet) {
|
|
opts.Add(
|
|
serpent.Option{
|
|
Flag: "only-token",
|
|
Description: "Only print the token. This is useful for scripting.",
|
|
Value: serpent.BoolOf(&f.onlyToken),
|
|
},
|
|
)
|
|
f.formatter.AttachOptions(opts)
|
|
}
|
|
|
|
func newUpdateProxyResponseFormatter() *updateProxyResponseFormatter {
|
|
up := &updateProxyResponseFormatter{
|
|
onlyToken: false,
|
|
}
|
|
up.formatter = cliui.NewOutputFormatter(
|
|
// Text formatter should be human readable.
|
|
cliui.ChangeFormatterData(cliui.TextFormat(), func(data any) (any, error) {
|
|
response, ok := data.(codersdk.UpdateWorkspaceProxyResponse)
|
|
if !ok {
|
|
return nil, xerrors.Errorf("unexpected type %T", data)
|
|
}
|
|
|
|
return fmt.Sprintf("Workspace Proxy %[1]q updated successfully.\n"+
|
|
pretty.Sprint(cliui.DefaultStyles.Placeholder, "—————————————————————————————————————————————————")+"\n"+
|
|
"Save this authentication token, it will not be shown again.\n"+
|
|
"Token: %[2]s\n"+
|
|
"\n"+
|
|
"Start the proxy by running:\n"+
|
|
cliui.Code("CODER_PROXY_SESSION_TOKEN=%[2]s coder wsproxy server --primary-access-url %[3]s --http-address=0.0.0.0:3001")+
|
|
// This is required to turn off the code style. Otherwise it appears in the code block until the end of the line.
|
|
pretty.Sprint(cliui.DefaultStyles.Placeholder, ""),
|
|
response.Proxy.Name, response.ProxyToken, up.primaryAccessURL), nil
|
|
}),
|
|
cliui.JSONFormat(),
|
|
// Table formatter expects a slice, make a slice of one.
|
|
cliui.ChangeFormatterData(cliui.TableFormat([]codersdk.UpdateWorkspaceProxyResponse{}, []string{"name", "url", "proxy token"}),
|
|
func(data any) (any, error) {
|
|
response, ok := data.(codersdk.UpdateWorkspaceProxyResponse)
|
|
if !ok {
|
|
return nil, xerrors.Errorf("unexpected type %T", data)
|
|
}
|
|
return []codersdk.UpdateWorkspaceProxyResponse{response}, nil
|
|
}),
|
|
)
|
|
|
|
return up
|
|
}
|