Skip to content

fix: make cli respect deployment --docs-url #14568

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 33 additions & 32 deletions cli/cliui/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type AgentOptions struct {
Fetch func(ctx context.Context, agentID uuid.UUID) (codersdk.WorkspaceAgent, error)
FetchLogs func(ctx context.Context, agentID uuid.UUID, after int64, follow bool) (<-chan []codersdk.WorkspaceAgentLog, io.Closer, error)
Wait bool // If true, wait for the agent to be ready (startup script).
DocsURL string
}

// Agent displays a spinning indicator that waits for a workspace agent to connect.
Expand Down Expand Up @@ -119,7 +120,7 @@ func Agent(ctx context.Context, writer io.Writer, agentID uuid.UUID, opts AgentO
if agent.Status == codersdk.WorkspaceAgentTimeout {
now := time.Now()
sw.Log(now, codersdk.LogLevelInfo, "The workspace agent is having trouble connecting, wait for it to connect or restart your workspace.")
sw.Log(now, codersdk.LogLevelInfo, troubleshootingMessage(agent, "https://coder.com/docs/templates#agent-connection-issues"))
sw.Log(now, codersdk.LogLevelInfo, troubleshootingMessage(agent, fmt.Sprintf("%s/templates#agent-connection-issues", opts.DocsURL)))
for agent.Status == codersdk.WorkspaceAgentTimeout {
if agent, err = fetch(); err != nil {
return xerrors.Errorf("fetch: %w", err)
Expand Down Expand Up @@ -224,13 +225,13 @@ func Agent(ctx context.Context, writer io.Writer, agentID uuid.UUID, opts AgentO
sw.Fail(stage, safeDuration(sw, agent.ReadyAt, agent.StartedAt))
// Use zero time (omitted) to separate these from the startup logs.
sw.Log(time.Time{}, codersdk.LogLevelWarn, "Warning: A startup script exited with an error and your workspace may be incomplete.")
sw.Log(time.Time{}, codersdk.LogLevelWarn, troubleshootingMessage(agent, "https://coder.com/docs/templates/troubleshooting#startup-script-exited-with-an-error"))
sw.Log(time.Time{}, codersdk.LogLevelWarn, troubleshootingMessage(agent, fmt.Sprintf("%s/templates#startup-script-exited-with-an-error", opts.DocsURL)))
default:
switch {
case agent.LifecycleState.Starting():
// Use zero time (omitted) to separate these from the startup logs.
sw.Log(time.Time{}, codersdk.LogLevelWarn, "Notice: The startup scripts are still running and your workspace may be incomplete.")
sw.Log(time.Time{}, codersdk.LogLevelWarn, troubleshootingMessage(agent, "https://coder.com/docs/templates/troubleshooting#your-workspace-may-be-incomplete"))
sw.Log(time.Time{}, codersdk.LogLevelWarn, troubleshootingMessage(agent, fmt.Sprintf("%s/templates#your-workspace-may-be-incomplete", opts.DocsURL)))
// Note: We don't complete or fail the stage here, it's
// intentionally left open to indicate this stage didn't
// complete.
Expand All @@ -252,7 +253,7 @@ func Agent(ctx context.Context, writer io.Writer, agentID uuid.UUID, opts AgentO
stage := "The workspace agent lost connection"
sw.Start(stage)
sw.Log(time.Now(), codersdk.LogLevelWarn, "Wait for it to reconnect or restart your workspace.")
sw.Log(time.Now(), codersdk.LogLevelWarn, troubleshootingMessage(agent, "https://coder.com/docs/templates/troubleshooting#agent-connection-issues"))
sw.Log(time.Now(), codersdk.LogLevelWarn, troubleshootingMessage(agent, fmt.Sprintf("%s/templates#agent-connection-issues", opts.DocsURL)))

disconnectedAt := agent.DisconnectedAt
for agent.Status == codersdk.WorkspaceAgentDisconnected {
Expand Down Expand Up @@ -351,16 +352,16 @@ func PeerDiagnostics(w io.Writer, d tailnet.PeerDiagnostics) {
}

type ConnDiags struct {
ConnInfo workspacesdk.AgentConnectionInfo
PingP2P bool
DisableDirect bool
LocalNetInfo *tailcfg.NetInfo
LocalInterfaces *healthsdk.InterfacesReport
AgentNetcheck *healthsdk.AgentNetcheckReport
ClientIPIsAWS bool
AgentIPIsAWS bool
Verbose bool
// TODO: More diagnostics
ConnInfo workspacesdk.AgentConnectionInfo
PingP2P bool
DisableDirect bool
LocalNetInfo *tailcfg.NetInfo
LocalInterfaces *healthsdk.InterfacesReport
AgentNetcheck *healthsdk.AgentNetcheckReport
ClientIPIsAWS bool
AgentIPIsAWS bool
Verbose bool
TroubleshootingURL string
}

func (d ConnDiags) Write(w io.Writer) {
Expand Down Expand Up @@ -395,7 +396,7 @@ func (d ConnDiags) splitDiagnostics() (general, client, agent []string) {
agent = append(agent, msg.Message)
}
if len(d.AgentNetcheck.Interfaces.Warnings) > 0 {
agent[len(agent)-1] += "\nhttps://coder.com/docs/networking/troubleshooting#low-mtu"
agent[len(agent)-1] += fmt.Sprintf("\n%s#low-mtu", d.TroubleshootingURL)
}
}

Expand All @@ -404,7 +405,7 @@ func (d ConnDiags) splitDiagnostics() (general, client, agent []string) {
client = append(client, msg.Message)
}
if len(d.LocalInterfaces.Warnings) > 0 {
client[len(client)-1] += "\nhttps://coder.com/docs/networking/troubleshooting#low-mtu"
client[len(client)-1] += fmt.Sprintf("\n%s#low-mtu", d.TroubleshootingURL)
}
}

Expand All @@ -420,45 +421,45 @@ func (d ConnDiags) splitDiagnostics() (general, client, agent []string) {
}

if d.ConnInfo.DisableDirectConnections {
general = append(general, "❗ Your Coder administrator has blocked direct connections\n"+
" https://coder.com/docs/networking/troubleshooting#disabled-deployment-wide")
general = append(general,
fmt.Sprintf("❗ Your Coder administrator has blocked direct connections\n %s#disabled-deployment-wide", d.TroubleshootingURL))
if !d.Verbose {
return general, client, agent
}
}

if !d.ConnInfo.DERPMap.HasSTUN() {
general = append(general, "❗ The DERP map is not configured to use STUN\n"+
" https://coder.com/docs/networking/troubleshooting#no-stun-servers")
general = append(general,
fmt.Sprintf("❗ The DERP map is not configured to use STUN\n %s#no-stun-servers", d.TroubleshootingURL))
} else if d.LocalNetInfo != nil && !d.LocalNetInfo.UDP {
client = append(client, "Client could not connect to STUN over UDP\n"+
" https://coder.com/docs/networking/troubleshooting#udp-blocked")
client = append(client,
fmt.Sprintf("Client could not connect to STUN over UDP\n %s#udp-blocked", d.TroubleshootingURL))
}

if d.LocalNetInfo != nil && d.LocalNetInfo.MappingVariesByDestIP.EqualBool(true) {
client = append(client, "Client is potentially behind a hard NAT, as multiple endpoints were retrieved from different STUN servers\n"+
" https://coder.com/docs/networking/troubleshooting#Endpoint-Dependent-Nat-Hard-NAT")
client = append(client,
fmt.Sprintf("Client is potentially behind a hard NAT, as multiple endpoints were retrieved from different STUN servers\n %s#endpoint-dependent-nat-hard-nat", d.TroubleshootingURL))
}

if d.AgentNetcheck != nil && d.AgentNetcheck.NetInfo != nil {
if d.AgentNetcheck.NetInfo.MappingVariesByDestIP.EqualBool(true) {
agent = append(agent, "Agent is potentially behind a hard NAT, as multiple endpoints were retrieved from different STUN servers\n"+
" https://coder.com/docs/networking/troubleshooting#Endpoint-Dependent-Nat-Hard-NAT")
agent = append(agent,
fmt.Sprintf("Agent is potentially behind a hard NAT, as multiple endpoints were retrieved from different STUN servers\n %s#endpoint-dependent-nat-hard-nat", d.TroubleshootingURL))
}
if !d.AgentNetcheck.NetInfo.UDP {
agent = append(agent, "Agent could not connect to STUN over UDP\n"+
" https://coder.com/docs/networking/troubleshooting#udp-blocked")
agent = append(agent,
fmt.Sprintf("Agent could not connect to STUN over UDP\n %s#udp-blocked", d.TroubleshootingURL))
}
}

if d.ClientIPIsAWS {
client = append(client, "Client IP address is within an AWS range (AWS uses hard NAT)\n"+
" https://coder.com/docs/networking/troubleshooting#Endpoint-Dependent-Nat-Hard-NAT")
client = append(client,
fmt.Sprintf("Client IP address is within an AWS range (AWS uses hard NAT)\n %s#endpoint-dependent-nat-hard-nat", d.TroubleshootingURL))
}

if d.AgentIPIsAWS {
agent = append(agent, "Agent IP address is within an AWS range (AWS uses hard NAT)\n"+
" https://coder.com/docs/networking/troubleshooting#Endpoint-Dependent-Nat-Hard-NAT")
agent = append(agent,
fmt.Sprintf("Agent IP address is within an AWS range (AWS uses hard NAT)\n %s#endpoint-dependent-nat-hard-nat", d.TroubleshootingURL))
}
return general, client, agent
}
2 changes: 1 addition & 1 deletion cli/dotfiles.go
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ func (r *RootCmd) dotfiles() *serpent.Command {
}

if fi.Mode()&0o111 == 0 {
return xerrors.Errorf("script %q is not executable. See https://coder.com/docs/dotfiles for information on how to resolve the issue.", script)
return xerrors.Errorf("script %q does not have execute permissions", script)
}

// it is safe to use a variable command here because it's from
Expand Down
7 changes: 5 additions & 2 deletions cli/open.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,9 @@ const vscodeDesktopName = "VS Code Desktop"

func (r *RootCmd) openVSCode() *serpent.Command {
var (
generateToken bool
testOpenError bool
generateToken bool
testOpenError bool
appearanceConfig codersdk.AppearanceConfig
)

client := new(codersdk.Client)
Expand All @@ -47,6 +48,7 @@ func (r *RootCmd) openVSCode() *serpent.Command {
Middleware: serpent.Chain(
serpent.RequireRangeArgs(1, 2),
r.InitClient(client),
initAppearance(client, &appearanceConfig),
),
Handler: func(inv *serpent.Invocation) error {
ctx, cancel := context.WithCancel(inv.Context())
Expand Down Expand Up @@ -79,6 +81,7 @@ func (r *RootCmd) openVSCode() *serpent.Command {
Fetch: client.WorkspaceAgent,
FetchLogs: nil,
Wait: false,
DocsURL: appearanceConfig.DocsURL,
})
if err != nil {
if xerrors.Is(err, context.Canceled) {
Expand Down
23 changes: 13 additions & 10 deletions cli/ping.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,10 @@ import (

func (r *RootCmd) ping() *serpent.Command {
var (
pingNum int64
pingTimeout time.Duration
pingWait time.Duration
pingNum int64
pingTimeout time.Duration
pingWait time.Duration
appearanceConfig codersdk.AppearanceConfig
)

client := new(codersdk.Client)
Expand All @@ -39,6 +40,7 @@ func (r *RootCmd) ping() *serpent.Command {
Middleware: serpent.Chain(
serpent.RequireNArgs(1),
r.InitClient(client),
initAppearance(client, &appearanceConfig),
),
Handler: func(inv *serpent.Invocation) error {
ctx, cancel := context.WithCancel(inv.Context())
Expand Down Expand Up @@ -67,8 +69,8 @@ func (r *RootCmd) ping() *serpent.Command {
if !r.disableNetworkTelemetry {
opts.EnableTelemetry = true
}
client := workspacesdk.New(client)
conn, err := client.DialAgent(ctx, workspaceAgent.ID, opts)
wsClient := workspacesdk.New(client)
conn, err := wsClient.DialAgent(ctx, workspaceAgent.ID, opts)
if err != nil {
return err
}
Expand Down Expand Up @@ -155,10 +157,11 @@ func (r *RootCmd) ping() *serpent.Command {

ni := conn.GetNetInfo()
connDiags := cliui.ConnDiags{
PingP2P: didP2p,
DisableDirect: r.disableDirect,
LocalNetInfo: ni,
Verbose: r.verbose,
PingP2P: didP2p,
DisableDirect: r.disableDirect,
LocalNetInfo: ni,
Verbose: r.verbose,
TroubleshootingURL: appearanceConfig.DocsURL + "/networking/troubleshooting",
}

awsRanges, err := cliutil.FetchAWSIPRanges(diagCtx, cliutil.AWSIPRangesURL)
Expand All @@ -168,7 +171,7 @@ func (r *RootCmd) ping() *serpent.Command {

connDiags.ClientIPIsAWS = isAWSIP(awsRanges, ni)

connInfo, err := client.AgentConnectionInfoGeneric(diagCtx)
connInfo, err := wsClient.AgentConnectionInfoGeneric(diagCtx)
if err != nil || connInfo.DERPMap == nil {
return xerrors.Errorf("Failed to retrieve connection info from server: %w\n", err)
}
Expand Down
7 changes: 5 additions & 2 deletions cli/portforward.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ func (r *RootCmd) portForward() *serpent.Command {
tcpForwards []string // <port>:<port>
udpForwards []string // <port>:<port>
disableAutostart bool
appearanceConfig codersdk.AppearanceConfig
)
client := new(codersdk.Client)
cmd := &serpent.Command{
Expand Down Expand Up @@ -60,6 +61,7 @@ func (r *RootCmd) portForward() *serpent.Command {
Middleware: serpent.Chain(
serpent.RequireNArgs(1),
r.InitClient(client),
initAppearance(client, &appearanceConfig),
),
Handler: func(inv *serpent.Invocation) error {
ctx, cancel := context.WithCancel(inv.Context())
Expand Down Expand Up @@ -88,8 +90,9 @@ func (r *RootCmd) portForward() *serpent.Command {
}

err = cliui.Agent(ctx, inv.Stderr, workspaceAgent.ID, cliui.AgentOptions{
Fetch: client.WorkspaceAgent,
Wait: false,
Fetch: client.WorkspaceAgent,
Wait: false,
DocsURL: appearanceConfig.DocsURL,
})
if err != nil {
return xerrors.Errorf("await agent: %w", err)
Expand Down
4 changes: 3 additions & 1 deletion cli/rename.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
)

func (r *RootCmd) rename() *serpent.Command {
var appearanceConfig codersdk.AppearanceConfig
client := new(codersdk.Client)
cmd := &serpent.Command{
Annotations: workspaceCommand,
Expand All @@ -21,6 +22,7 @@ func (r *RootCmd) rename() *serpent.Command {
Middleware: serpent.Chain(
serpent.RequireNArgs(2),
r.InitClient(client),
initAppearance(client, &appearanceConfig),
),
Handler: func(inv *serpent.Invocation) error {
workspace, err := namedWorkspace(inv.Context(), client, inv.Args[0])
Expand All @@ -31,7 +33,7 @@ func (r *RootCmd) rename() *serpent.Command {
_, _ = fmt.Fprintf(inv.Stdout, "%s\n\n",
pretty.Sprint(cliui.DefaultStyles.Wrap, "WARNING: A rename can result in data loss if a resource references the workspace name in the template (e.g volumes). Please backup any data before proceeding."),
)
_, _ = fmt.Fprintf(inv.Stdout, "See: %s\n\n", "https://coder.com/docs/templates/resource-persistence#%EF%B8%8F-persistence-pitfalls")
_, _ = fmt.Fprintf(inv.Stdout, "See: %s%s\n\n", appearanceConfig.DocsURL, "/templates/resource-persistence#%EF%B8%8F-persistence-pitfalls")
_, err = cliui.Prompt(inv, cliui.PromptOptions{
Text: fmt.Sprintf("Type %q to confirm rename:", workspace.Name),
Validate: func(s string) error {
Expand Down
20 changes: 20 additions & 0 deletions cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -692,6 +692,26 @@ func namedWorkspace(ctx context.Context, client *codersdk.Client, identifier str
return client.WorkspaceByOwnerAndName(ctx, owner, name, codersdk.WorkspaceOptions{})
}

func initAppearance(client *codersdk.Client, outConfig *codersdk.AppearanceConfig) serpent.MiddlewareFunc {
return func(next serpent.HandlerFunc) serpent.HandlerFunc {
return func(inv *serpent.Invocation) error {
var err error
cfg, err := client.Appearance(inv.Context())
if err != nil {
var sdkErr *codersdk.Error
if !(xerrors.As(err, &sdkErr) && sdkErr.StatusCode() == http.StatusNotFound) {
return err
}
}
if cfg.DocsURL == "" {
cfg.DocsURL = codersdk.DefaultDocsURL()
}
*outConfig = cfg
return next(inv)
}
}
}

// createConfig consumes the global configuration flag to produce a config root.
func (r *RootCmd) createConfig() config.Root {
return config.Root(r.globalConfig)
Expand Down
4 changes: 2 additions & 2 deletions cli/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -628,7 +628,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
"new version of coder available",
slog.F("new_version", r.Version),
slog.F("url", r.URL),
slog.F("upgrade_instructions", "https://coder.com/docs/admin/upgrade"),
slog.F("upgrade_instructions", fmt.Sprintf("%s/admin/upgrade", vals.DocsURL.String())),
)
}
},
Expand Down Expand Up @@ -854,7 +854,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
}
defer options.Telemetry.Close()
} else {
logger.Warn(ctx, `telemetry disabled, unable to notify of security issues. Read more: https://coder.com/docs/admin/telemetry`)
logger.Warn(ctx, fmt.Sprintf(`telemetry disabled, unable to notify of security issues. Read more: %s/admin/telemetry`, vals.DocsURL.String()))
}

// This prevents the pprof import from being accidentally deleted.
Expand Down
17 changes: 10 additions & 7 deletions cli/speedtest.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,12 @@ type speedtestTableItem struct {

func (r *RootCmd) speedtest() *serpent.Command {
var (
direct bool
duration time.Duration
direction string
pcapFile string
formatter = cliui.NewOutputFormatter(
direct bool
duration time.Duration
direction string
pcapFile string
appearanceConfig codersdk.AppearanceConfig
formatter = cliui.NewOutputFormatter(
cliui.ChangeFormatterData(cliui.TableFormat([]speedtestTableItem{}, []string{"Interval", "Throughput"}), func(data any) (any, error) {
res, ok := data.(SpeedtestResult)
if !ok {
Expand Down Expand Up @@ -72,6 +73,7 @@ func (r *RootCmd) speedtest() *serpent.Command {
Middleware: serpent.Chain(
serpent.RequireNArgs(1),
r.InitClient(client),
initAppearance(client, &appearanceConfig),
),
Handler: func(inv *serpent.Invocation) error {
ctx, cancel := context.WithCancel(inv.Context())
Expand All @@ -87,8 +89,9 @@ func (r *RootCmd) speedtest() *serpent.Command {
}

err = cliui.Agent(ctx, inv.Stderr, workspaceAgent.ID, cliui.AgentOptions{
Fetch: client.WorkspaceAgent,
Wait: false,
Fetch: client.WorkspaceAgent,
Wait: false,
DocsURL: appearanceConfig.DocsURL,
})
if err != nil {
return xerrors.Errorf("await agent: %w", err)
Expand Down
Loading
Loading