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

This commit is contained in:
Ethan
2024-09-18 21:47:53 +10:00
committed by GitHub
parent 20a3801600
commit 37885e2e82
26 changed files with 201 additions and 108 deletions
+33 -32
View File
@@ -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.
@@ -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)
@@ -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.
@@ -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 {
@@ -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) {
@@ -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)
}
}
@@ -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)
}
}
@@ -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
}
+1 -1
View File
@@ -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
+5 -2
View File
@@ -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)
@@ -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())
@@ -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) {
+13 -10
View File
@@ -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)
@@ -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())
@@ -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
}
@@ -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)
@@ -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)
}
+5 -2
View File
@@ -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{
@@ -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())
@@ -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)
+3 -1
View File
@@ -13,6 +13,7 @@ import (
)
func (r *RootCmd) rename() *serpent.Command {
var appearanceConfig codersdk.AppearanceConfig
client := new(codersdk.Client)
cmd := &serpent.Command{
Annotations: workspaceCommand,
@@ -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])
@@ -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 {
+20
View File
@@ -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)
+2 -2
View File
@@ -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())),
)
}
},
@@ -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.
+10 -7
View File
@@ -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 {
@@ -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())
@@ -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)
+7 -3
View File
@@ -67,6 +67,7 @@ func (r *RootCmd) ssh() *serpent.Command {
env []string
usageApp string
disableAutostart bool
appearanceConfig codersdk.AppearanceConfig
)
client := new(codersdk.Client)
cmd := &serpent.Command{
@@ -76,6 +77,7 @@ func (r *RootCmd) ssh() *serpent.Command {
Middleware: serpent.Chain(
serpent.RequireNArgs(1),
r.InitClient(client),
initAppearance(client, &appearanceConfig),
),
Handler: func(inv *serpent.Invocation) (retErr error) {
// Before dialing the SSH server over TCP, capture Interrupt signals
@@ -230,9 +232,11 @@ func (r *RootCmd) ssh() *serpent.Command {
// OpenSSH passes stderr directly to the calling TTY.
// This is required in "stdio" mode so a connecting indicator can be displayed.
err = cliui.Agent(ctx, inv.Stderr, workspaceAgent.ID, cliui.AgentOptions{
Fetch: client.WorkspaceAgent,
FetchLogs: client.WorkspaceAgentLogsAfter,
Wait: wait,
FetchInterval: 0,
Fetch: client.WorkspaceAgent,
FetchLogs: client.WorkspaceAgentLogsAfter,
Wait: wait,
DocsURL: appearanceConfig.DocsURL,
})
if err != nil {
if xerrors.Is(err, context.Canceled) {
+1 -1
View File
@@ -175,7 +175,7 @@ NETWORKING OPTIONS:
--access-url url, $CODER_ACCESS_URL
The URL that users will use to access the Coder deployment.
--docs-url url, $CODER_DOCS_URL
--docs-url url, $CODER_DOCS_URL (default: https://coder.com/docs)
Specifies the custom docs URL.
--proxy-trusted-headers string-array, $CODER_PROXY_TRUSTED_HEADERS
+2 -2
View File
@@ -7,8 +7,8 @@ networking:
# (default: <unset>, type: string)
wildcardAccessURL: ""
# Specifies the custom docs URL.
# (default: <unset>, type: url)
docsURL:
# (default: https://coder.com/docs, type: url)
docsURL: https://coder.com/docs
# Specifies whether to redirect requests that do not match the access URL host.
# (default: <unset>, type: bool)
redirectToAccessURL: false
+11
View File
@@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
@@ -133,10 +134,20 @@ func (r *RootCmd) vscodeSSH() *serpent.Command {
return xerrors.Errorf("unknown wait value %q", waitEnum)
}
appearanceCfg, err := client.Appearance(ctx)
if err != nil {
var sdkErr *codersdk.Error
if !(xerrors.As(err, &sdkErr) && sdkErr.StatusCode() == http.StatusNotFound) {
return xerrors.Errorf("get appearance config: %w", err)
}
appearanceCfg.DocsURL = codersdk.DefaultDocsURL()
}
err = cliui.Agent(ctx, inv.Stderr, workspaceAgent.ID, cliui.AgentOptions{
Fetch: client.WorkspaceAgent,
FetchLogs: client.WorkspaceAgentLogsAfter,
Wait: wait,
DocsURL: appearanceCfg.DocsURL,
})
if err != nil {
if xerrors.Is(err, context.Canceled) {