mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
fix: make cli respect deployment --docs-url (#14568)
This commit is contained in:
+33
-32
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Generated
+3
@@ -8890,6 +8890,9 @@ const docTemplate = `{
|
||||
"application_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"docs_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"logo_url": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
Generated
+3
@@ -7880,6 +7880,9 @@
|
||||
"application_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"docs_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"logo_url": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -2,10 +2,7 @@ package appearance
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/coder/coder/v2/buildinfo"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
)
|
||||
|
||||
@@ -13,37 +10,6 @@ type Fetcher interface {
|
||||
Fetch(ctx context.Context) (codersdk.AppearanceConfig, error)
|
||||
}
|
||||
|
||||
func DefaultSupportLinks(docsURL string) []codersdk.LinkConfig {
|
||||
version := buildinfo.Version()
|
||||
if docsURL == "" {
|
||||
docsURL = "https://coder.com/docs/@" + strings.Split(version, "-")[0]
|
||||
}
|
||||
buildInfo := fmt.Sprintf("Version: [`%s`](%s)", version, buildinfo.ExternalURL())
|
||||
|
||||
return []codersdk.LinkConfig{
|
||||
{
|
||||
Name: "Documentation",
|
||||
Target: docsURL,
|
||||
Icon: "docs",
|
||||
},
|
||||
{
|
||||
Name: "Report a bug",
|
||||
Target: "https://github.com/coder/coder/issues/new?labels=needs+grooming&body=" + buildInfo,
|
||||
Icon: "bug",
|
||||
},
|
||||
{
|
||||
Name: "Join the Coder Discord",
|
||||
Target: "https://coder.com/chat?utm_source=coder&utm_medium=coder&utm_campaign=server-footer",
|
||||
Icon: "chat",
|
||||
},
|
||||
{
|
||||
Name: "Star the Repo",
|
||||
Target: "https://github.com/coder/coder",
|
||||
Icon: "star",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type AGPLFetcher struct {
|
||||
docsURL string
|
||||
}
|
||||
@@ -51,11 +17,15 @@ type AGPLFetcher struct {
|
||||
func (f AGPLFetcher) Fetch(context.Context) (codersdk.AppearanceConfig, error) {
|
||||
return codersdk.AppearanceConfig{
|
||||
AnnouncementBanners: []codersdk.BannerConfig{},
|
||||
SupportLinks: DefaultSupportLinks(f.docsURL),
|
||||
SupportLinks: codersdk.DefaultSupportLinks(f.docsURL),
|
||||
DocsURL: f.docsURL,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewDefaultFetcher(docsURL string) Fetcher {
|
||||
if docsURL == "" {
|
||||
docsURL = codersdk.DefaultDocsURL()
|
||||
}
|
||||
return &AGPLFetcher{
|
||||
docsURL: docsURL,
|
||||
}
|
||||
|
||||
@@ -776,6 +776,42 @@ func DefaultCacheDir() string {
|
||||
return filepath.Join(defaultCacheDir, "coder")
|
||||
}
|
||||
|
||||
func DefaultSupportLinks(docsURL string) []LinkConfig {
|
||||
version := buildinfo.Version()
|
||||
buildInfo := fmt.Sprintf("Version: [`%s`](%s)", version, buildinfo.ExternalURL())
|
||||
|
||||
return []LinkConfig{
|
||||
{
|
||||
Name: "Documentation",
|
||||
Target: docsURL,
|
||||
Icon: "docs",
|
||||
},
|
||||
{
|
||||
Name: "Report a bug",
|
||||
Target: "https://github.com/coder/coder/issues/new?labels=needs+grooming&body=" + buildInfo,
|
||||
Icon: "bug",
|
||||
},
|
||||
{
|
||||
Name: "Join the Coder Discord",
|
||||
Target: "https://coder.com/chat?utm_source=coder&utm_medium=coder&utm_campaign=server-footer",
|
||||
Icon: "chat",
|
||||
},
|
||||
{
|
||||
Name: "Star the Repo",
|
||||
Target: "https://github.com/coder/coder",
|
||||
Icon: "star",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func DefaultDocsURL() string {
|
||||
version := strings.Split(buildinfo.Version(), "-")[0]
|
||||
if version == "v0.0.0" {
|
||||
return "https://coder.com/docs"
|
||||
}
|
||||
return "https://coder.com/docs/@" + version
|
||||
}
|
||||
|
||||
// DeploymentConfig contains both the deployment values and how they're set.
|
||||
type DeploymentConfig struct {
|
||||
Values *DeploymentValues `json:"config,omitempty"`
|
||||
@@ -994,6 +1030,7 @@ when required by your organization's security policy.`,
|
||||
Name: "Docs URL",
|
||||
Description: "Specifies the custom docs URL.",
|
||||
Value: &c.DocsURL,
|
||||
Default: DefaultDocsURL(),
|
||||
Flag: "docs-url",
|
||||
Env: "CODER_DOCS_URL",
|
||||
Group: &deploymentGroupNetworking,
|
||||
@@ -2737,6 +2774,7 @@ func (c *Client) DeploymentStats(ctx context.Context) (DeploymentStats, error) {
|
||||
type AppearanceConfig struct {
|
||||
ApplicationName string `json:"application_name"`
|
||||
LogoURL string `json:"logo_url"`
|
||||
DocsURL string `json:"docs_url"`
|
||||
// Deprecated: ServiceBanner has been replaced by AnnouncementBanners.
|
||||
ServiceBanner BannerConfig `json:"service_banner"`
|
||||
AnnouncementBanners []BannerConfig `json:"announcement_banners"`
|
||||
|
||||
Generated
+1
@@ -27,6 +27,7 @@ curl -X GET http://coder-server:8080/api/v2/appearance \
|
||||
}
|
||||
],
|
||||
"application_name": "string",
|
||||
"docs_url": "string",
|
||||
"logo_url": "string",
|
||||
"service_banner": {
|
||||
"background_color": "string",
|
||||
|
||||
Generated
+2
@@ -391,6 +391,7 @@
|
||||
}
|
||||
],
|
||||
"application_name": "string",
|
||||
"docs_url": "string",
|
||||
"logo_url": "string",
|
||||
"service_banner": {
|
||||
"background_color": "string",
|
||||
@@ -413,6 +414,7 @@
|
||||
| ---------------------- | ------------------------------------------------------- | -------- | ------------ | ------------------------------------------------------------------- |
|
||||
| `announcement_banners` | array of [codersdk.BannerConfig](#codersdkbannerconfig) | false | | |
|
||||
| `application_name` | string | false | | |
|
||||
| `docs_url` | string | false | | |
|
||||
| `logo_url` | string | false | | |
|
||||
| `service_banner` | [codersdk.BannerConfig](#codersdkbannerconfig) | false | | Deprecated: ServiceBanner has been replaced by AnnouncementBanners. |
|
||||
| `support_links` | array of [codersdk.LinkConfig](#codersdklinkconfig) | false | | |
|
||||
|
||||
Generated
+6
-5
@@ -43,11 +43,12 @@ Specifies the wildcard hostname to use for workspace applications in the form "\
|
||||
|
||||
### --docs-url
|
||||
|
||||
| | |
|
||||
| ----------- | ------------------------------- |
|
||||
| Type | <code>url</code> |
|
||||
| Environment | <code>$CODER_DOCS_URL</code> |
|
||||
| YAML | <code>networking.docsURL</code> |
|
||||
| | |
|
||||
| ----------- | ----------------------------------- |
|
||||
| Type | <code>url</code> |
|
||||
| Environment | <code>$CODER_DOCS_URL</code> |
|
||||
| YAML | <code>networking.docsURL</code> |
|
||||
| Default | <code>https://coder.com/docs</code> |
|
||||
|
||||
Specifies the custom docs URL.
|
||||
|
||||
|
||||
+1
-1
@@ -176,7 +176,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
|
||||
|
||||
@@ -49,6 +49,9 @@ type appearanceFetcher struct {
|
||||
}
|
||||
|
||||
func newAppearanceFetcher(store database.Store, links []codersdk.LinkConfig, docsURL, coderVersion string) agpl.Fetcher {
|
||||
if docsURL == "" {
|
||||
docsURL = codersdk.DefaultDocsURL()
|
||||
}
|
||||
return &appearanceFetcher{
|
||||
database: store,
|
||||
supportLinks: links,
|
||||
@@ -94,7 +97,8 @@ func (f *appearanceFetcher) Fetch(ctx context.Context) (codersdk.AppearanceConfi
|
||||
ApplicationName: applicationName,
|
||||
LogoURL: logoURL,
|
||||
AnnouncementBanners: []codersdk.BannerConfig{},
|
||||
SupportLinks: agpl.DefaultSupportLinks(f.docsURL),
|
||||
SupportLinks: codersdk.DefaultSupportLinks(f.docsURL),
|
||||
DocsURL: f.docsURL,
|
||||
}
|
||||
|
||||
if announcementBannersJSON != "" {
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/v2/agent/proto"
|
||||
"github.com/coder/coder/v2/coderd/appearance"
|
||||
"github.com/coder/coder/v2/coderd/coderdtest"
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/database/dbfake"
|
||||
@@ -230,6 +229,25 @@ func TestCustomSupportLinks(t *testing.T) {
|
||||
require.Equal(t, supportLinks, appr.SupportLinks)
|
||||
}
|
||||
|
||||
func TestCustomDocsURL(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testURLRawString := "http://google.com"
|
||||
testURL, err := url.Parse(testURLRawString)
|
||||
require.NoError(t, err)
|
||||
cfg := coderdtest.DeploymentValues(t)
|
||||
cfg.DocsURL = *serpent.URLOf(testURL)
|
||||
adminClient, adminUser := coderdenttest.New(t, &coderdenttest.Options{DontAddLicense: true, Options: &coderdtest.Options{DeploymentValues: cfg}})
|
||||
anotherClient, _ := coderdtest.CreateAnotherUser(t, adminClient, adminUser.OrganizationID)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitMedium)
|
||||
defer cancel()
|
||||
|
||||
appr, err := anotherClient.Appearance(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, testURLRawString, appr.DocsURL)
|
||||
}
|
||||
|
||||
func TestDefaultSupportLinksWithCustomDocsUrl(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -247,7 +265,7 @@ func TestDefaultSupportLinksWithCustomDocsUrl(t *testing.T) {
|
||||
|
||||
appr, err := anotherClient.Appearance(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, appearance.DefaultSupportLinks(testURLRawString), appr.SupportLinks)
|
||||
require.Equal(t, codersdk.DefaultSupportLinks(testURLRawString), appr.SupportLinks)
|
||||
}
|
||||
|
||||
func TestDefaultSupportLinks(t *testing.T) {
|
||||
@@ -262,5 +280,5 @@ func TestDefaultSupportLinks(t *testing.T) {
|
||||
|
||||
appr, err := anotherClient.Appearance(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, appearance.DefaultSupportLinks(""), appr.SupportLinks)
|
||||
require.Equal(t, codersdk.DefaultSupportLinks(codersdk.DefaultDocsURL()), appr.SupportLinks)
|
||||
}
|
||||
|
||||
@@ -1801,6 +1801,7 @@ class ApiMethods {
|
||||
if (isAxiosError(ex) && ex.response?.status === 404) {
|
||||
return {
|
||||
application_name: "",
|
||||
docs_url: "",
|
||||
logo_url: "",
|
||||
announcement_banners: [],
|
||||
service_banner: {
|
||||
|
||||
Generated
+1
@@ -48,6 +48,7 @@ export interface AppHostResponse {
|
||||
export interface AppearanceConfig {
|
||||
readonly application_name: string;
|
||||
readonly logo_url: string;
|
||||
readonly docs_url: string;
|
||||
readonly service_banner: BannerConfig;
|
||||
readonly announcement_banners: Readonly<Array<BannerConfig>>;
|
||||
readonly support_links?: Readonly<Array<LinkConfig>>;
|
||||
|
||||
@@ -2713,6 +2713,7 @@ export const MockAppearanceConfig: TypesGen.AppearanceConfig = {
|
||||
enabled: false,
|
||||
},
|
||||
announcement_banners: [],
|
||||
docs_url: "https://coder.com/docs/@main/",
|
||||
};
|
||||
|
||||
export const MockWorkspaceBuildParameter1: TypesGen.WorkspaceBuildParameter = {
|
||||
|
||||
Reference in New Issue
Block a user