mirror of https://github.com/coder/coder.git
Merge branch 'main' into node-20
This commit is contained in:
commit
41e640fab3
|
@ -64,7 +64,7 @@ func (r *RootCmd) openVSCode() *serpent.Command {
|
|||
// need to wait for the agent to start.
|
||||
workspaceQuery := inv.Args[0]
|
||||
autostart := true
|
||||
workspace, workspaceAgent, err := getWorkspaceAndAgent(ctx, inv, client, autostart, codersdk.Me, workspaceQuery)
|
||||
workspace, workspaceAgent, err := getWorkspaceAndAgent(ctx, inv, client, autostart, workspaceQuery)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get workspace and agent: %w", err)
|
||||
}
|
||||
|
|
|
@ -42,7 +42,7 @@ func (r *RootCmd) ping() *serpent.Command {
|
|||
_, workspaceAgent, err := getWorkspaceAndAgent(
|
||||
ctx, inv, client,
|
||||
false, // Do not autostart for a ping.
|
||||
codersdk.Me, workspaceName,
|
||||
workspaceName,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
@ -73,7 +73,7 @@ func (r *RootCmd) portForward() *serpent.Command {
|
|||
return xerrors.New("no port-forwards requested")
|
||||
}
|
||||
|
||||
workspace, workspaceAgent, err := getWorkspaceAndAgent(ctx, inv, client, !disableAutostart, codersdk.Me, inv.Args[0])
|
||||
workspace, workspaceAgent, err := getWorkspaceAndAgent(ctx, inv, client, !disableAutostart, inv.Args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -39,7 +39,7 @@ func (r *RootCmd) speedtest() *serpent.Command {
|
|||
ctx, cancel := context.WithCancel(inv.Context())
|
||||
defer cancel()
|
||||
|
||||
_, workspaceAgent, err := getWorkspaceAndAgent(ctx, inv, client, false, codersdk.Me, inv.Args[0])
|
||||
_, workspaceAgent, err := getWorkspaceAndAgent(ctx, inv, client, false, inv.Args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
10
cli/ssh.go
10
cli/ssh.go
|
@ -157,7 +157,7 @@ func (r *RootCmd) ssh() *serpent.Command {
|
|||
}
|
||||
}
|
||||
|
||||
workspace, workspaceAgent, err := getWorkspaceAndAgent(ctx, inv, client, !disableAutostart, codersdk.Me, inv.Args[0])
|
||||
workspace, workspaceAgent, err := getWorkspaceAndAgent(ctx, inv, client, !disableAutostart, inv.Args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -551,10 +551,12 @@ startWatchLoop:
|
|||
// getWorkspaceAgent returns the workspace and agent selected using either the
|
||||
// `<workspace>[.<agent>]` syntax via `in`.
|
||||
// If autoStart is true, the workspace will be started if it is not already running.
|
||||
func getWorkspaceAndAgent(ctx context.Context, inv *serpent.Invocation, client *codersdk.Client, autostart bool, userID string, in string) (codersdk.Workspace, codersdk.WorkspaceAgent, error) { //nolint:revive
|
||||
func getWorkspaceAndAgent(ctx context.Context, inv *serpent.Invocation, client *codersdk.Client, autostart bool, input string) (codersdk.Workspace, codersdk.WorkspaceAgent, error) { //nolint:revive
|
||||
var (
|
||||
workspace codersdk.Workspace
|
||||
workspaceParts = strings.Split(in, ".")
|
||||
workspace codersdk.Workspace
|
||||
// The input will be `owner/name.agent`
|
||||
// The agent is optional.
|
||||
workspaceParts = strings.Split(input, ".")
|
||||
err error
|
||||
)
|
||||
|
||||
|
|
|
@ -101,7 +101,7 @@ func (r *RootCmd) supportBundle() *serpent.Command {
|
|||
|
||||
// Check if we're running inside a workspace
|
||||
if val, found := os.LookupEnv("CODER"); found && val == "true" {
|
||||
_, _ = fmt.Fprintln(inv.Stderr, "Running inside Coder workspace; this can affect results!")
|
||||
cliui.Warn(inv.Stderr, "Running inside Coder workspace; this can affect results!")
|
||||
cliLog.Debug(inv.Context(), "running inside coder workspace")
|
||||
}
|
||||
|
||||
|
@ -122,7 +122,7 @@ func (r *RootCmd) supportBundle() *serpent.Command {
|
|||
|
||||
if len(inv.Args) == 0 {
|
||||
cliLog.Warn(inv.Context(), "no workspace specified")
|
||||
_, _ = fmt.Fprintln(inv.Stderr, "Warning: no workspace specified. This will result in incomplete information.")
|
||||
cliui.Warn(inv.Stderr, "No workspace specified. This will result in incomplete information.")
|
||||
} else {
|
||||
ws, err := namedWorkspace(inv.Context(), client, inv.Args[0])
|
||||
if err != nil {
|
||||
|
@ -184,6 +184,16 @@ func (r *RootCmd) supportBundle() *serpent.Command {
|
|||
_ = os.Remove(outputPath) // best effort
|
||||
return xerrors.Errorf("create support bundle: %w", err)
|
||||
}
|
||||
docsURL := bun.Deployment.Config.Values.DocsURL.String()
|
||||
deployHealthSummary := bun.Deployment.HealthReport.Summarize(docsURL)
|
||||
if len(deployHealthSummary) > 0 {
|
||||
cliui.Warn(inv.Stdout, "Deployment health issues detected:", deployHealthSummary...)
|
||||
}
|
||||
clientNetcheckSummary := bun.Network.Netcheck.Summarize("Client netcheck:", docsURL)
|
||||
if len(clientNetcheckSummary) > 0 {
|
||||
cliui.Warn(inv.Stdout, "Networking issues detected:", deployHealthSummary...)
|
||||
}
|
||||
|
||||
bun.CLILogs = cliLogBuf.Bytes()
|
||||
|
||||
if err := writeBundle(bun, zwr); err != nil {
|
||||
|
@ -191,6 +201,7 @@ func (r *RootCmd) supportBundle() *serpent.Command {
|
|||
return xerrors.Errorf("write support bundle to %s: %w", outputPath, err)
|
||||
}
|
||||
_, _ = fmt.Fprintln(inv.Stderr, "Wrote support bundle to "+outputPath)
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
|
|
@ -110,7 +110,7 @@ func (r *RootCmd) vscodeSSH() *serpent.Command {
|
|||
// will call this command after the workspace is started.
|
||||
autostart := false
|
||||
|
||||
_, workspaceAgent, err := getWorkspaceAndAgent(ctx, inv, client, autostart, owner, name)
|
||||
_, workspaceAgent, err := getWorkspaceAndAgent(ctx, inv, client, autostart, fmt.Sprintf("%s/%s", owner, name))
|
||||
if err != nil {
|
||||
return xerrors.Errorf("find workspace and agent: %w", err)
|
||||
}
|
||||
|
|
|
@ -13834,7 +13834,16 @@ const docTemplate = `{
|
|||
}
|
||||
},
|
||||
"severity": {
|
||||
"$ref": "#/definitions/health.Severity"
|
||||
"enum": [
|
||||
"ok",
|
||||
"warning",
|
||||
"error"
|
||||
],
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/health.Severity"
|
||||
}
|
||||
]
|
||||
},
|
||||
"warnings": {
|
||||
"type": "array",
|
||||
|
@ -13917,7 +13926,7 @@ const docTemplate = `{
|
|||
"warnings": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
"$ref": "#/definitions/health.Message"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13932,10 +13941,20 @@ const docTemplate = `{
|
|||
"type": "string"
|
||||
},
|
||||
"healthy": {
|
||||
"description": "Healthy is deprecated and left for backward compatibility purposes, use ` + "`" + `Severity` + "`" + ` instead.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"severity": {
|
||||
"$ref": "#/definitions/health.Severity"
|
||||
"enum": [
|
||||
"ok",
|
||||
"warning",
|
||||
"error"
|
||||
],
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/health.Severity"
|
||||
}
|
||||
]
|
||||
},
|
||||
"warnings": {
|
||||
"type": "array",
|
||||
|
|
|
@ -12574,7 +12574,12 @@
|
|||
}
|
||||
},
|
||||
"severity": {
|
||||
"$ref": "#/definitions/health.Severity"
|
||||
"enum": ["ok", "warning", "error"],
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/health.Severity"
|
||||
}
|
||||
]
|
||||
},
|
||||
"warnings": {
|
||||
"type": "array",
|
||||
|
@ -12653,7 +12658,7 @@
|
|||
"warnings": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
"$ref": "#/definitions/health.Message"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12668,10 +12673,16 @@
|
|||
"type": "string"
|
||||
},
|
||||
"healthy": {
|
||||
"description": "Healthy is deprecated and left for backward compatibility purposes, use `Severity` instead.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"severity": {
|
||||
"$ref": "#/definitions/health.Severity"
|
||||
"enum": ["ok", "warning", "error"],
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/health.Severity"
|
||||
}
|
||||
]
|
||||
},
|
||||
"warnings": {
|
||||
"type": "array",
|
||||
|
|
|
@ -9089,7 +9089,6 @@ func (q *FakeQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg database.
|
|||
params = append(params, param)
|
||||
}
|
||||
|
||||
var innerErr error
|
||||
index := slices.IndexFunc(params, func(buildParam database.WorkspaceBuildParameter) bool {
|
||||
// If hasParam matches, then we are done. This is a good match.
|
||||
if slices.ContainsFunc(arg.HasParam, func(name string) bool {
|
||||
|
@ -9116,9 +9115,6 @@ func (q *FakeQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg database.
|
|||
|
||||
return match
|
||||
})
|
||||
if innerErr != nil {
|
||||
return nil, xerrors.Errorf("error searching workspace build params: %w", innerErr)
|
||||
}
|
||||
if index < 0 {
|
||||
continue
|
||||
}
|
||||
|
|
|
@ -179,8 +179,9 @@ func (m metricsStore) DeleteApplicationConnectAPIKeysByUserID(ctx context.Contex
|
|||
|
||||
func (m metricsStore) DeleteCoordinator(ctx context.Context, id uuid.UUID) error {
|
||||
start := time.Now()
|
||||
defer m.queryLatencies.WithLabelValues("DeleteCoordinator").Observe(time.Since(start).Seconds())
|
||||
return m.s.DeleteCoordinator(ctx, id)
|
||||
r0 := m.s.DeleteCoordinator(ctx, id)
|
||||
m.queryLatencies.WithLabelValues("DeleteCoordinator").Observe(time.Since(start).Seconds())
|
||||
return r0
|
||||
}
|
||||
|
||||
func (m metricsStore) DeleteExternalAuthLink(ctx context.Context, arg database.DeleteExternalAuthLinkParams) error {
|
||||
|
@ -283,14 +284,16 @@ func (m metricsStore) DeleteReplicasUpdatedBefore(ctx context.Context, updatedAt
|
|||
|
||||
func (m metricsStore) DeleteTailnetAgent(ctx context.Context, arg database.DeleteTailnetAgentParams) (database.DeleteTailnetAgentRow, error) {
|
||||
start := time.Now()
|
||||
defer m.queryLatencies.WithLabelValues("DeleteTailnetAgent").Observe(time.Since(start).Seconds())
|
||||
return m.s.DeleteTailnetAgent(ctx, arg)
|
||||
r0, r1 := m.s.DeleteTailnetAgent(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("DeleteTailnetAgent").Observe(time.Since(start).Seconds())
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m metricsStore) DeleteTailnetClient(ctx context.Context, arg database.DeleteTailnetClientParams) (database.DeleteTailnetClientRow, error) {
|
||||
start := time.Now()
|
||||
defer m.queryLatencies.WithLabelValues("DeleteTailnetClient").Observe(time.Since(start).Seconds())
|
||||
return m.s.DeleteTailnetClient(ctx, arg)
|
||||
r0, r1 := m.s.DeleteTailnetClient(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("DeleteTailnetClient").Observe(time.Since(start).Seconds())
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m metricsStore) DeleteTailnetClientSubscription(ctx context.Context, arg database.DeleteTailnetClientSubscriptionParams) error {
|
||||
|
@ -855,14 +858,16 @@ func (m metricsStore) GetServiceBanner(ctx context.Context) (string, error) {
|
|||
|
||||
func (m metricsStore) GetTailnetAgents(ctx context.Context, id uuid.UUID) ([]database.TailnetAgent, error) {
|
||||
start := time.Now()
|
||||
defer m.queryLatencies.WithLabelValues("GetTailnetAgents").Observe(time.Since(start).Seconds())
|
||||
return m.s.GetTailnetAgents(ctx, id)
|
||||
r0, r1 := m.s.GetTailnetAgents(ctx, id)
|
||||
m.queryLatencies.WithLabelValues("GetTailnetAgents").Observe(time.Since(start).Seconds())
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m metricsStore) GetTailnetClientsForAgent(ctx context.Context, agentID uuid.UUID) ([]database.TailnetClient, error) {
|
||||
start := time.Now()
|
||||
defer m.queryLatencies.WithLabelValues("GetTailnetClientsForAgent").Observe(time.Since(start).Seconds())
|
||||
return m.s.GetTailnetClientsForAgent(ctx, agentID)
|
||||
r0, r1 := m.s.GetTailnetClientsForAgent(ctx, agentID)
|
||||
m.queryLatencies.WithLabelValues("GetTailnetClientsForAgent").Observe(time.Since(start).Seconds())
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m metricsStore) GetTailnetPeers(ctx context.Context, id uuid.UUID) ([]database.TailnetPeer, error) {
|
||||
|
@ -2204,14 +2209,16 @@ func (m metricsStore) UpsertServiceBanner(ctx context.Context, value string) err
|
|||
|
||||
func (m metricsStore) UpsertTailnetAgent(ctx context.Context, arg database.UpsertTailnetAgentParams) (database.TailnetAgent, error) {
|
||||
start := time.Now()
|
||||
defer m.queryLatencies.WithLabelValues("UpsertTailnetAgent").Observe(time.Since(start).Seconds())
|
||||
return m.s.UpsertTailnetAgent(ctx, arg)
|
||||
r0, r1 := m.s.UpsertTailnetAgent(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("UpsertTailnetAgent").Observe(time.Since(start).Seconds())
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m metricsStore) UpsertTailnetClient(ctx context.Context, arg database.UpsertTailnetClientParams) (database.TailnetClient, error) {
|
||||
start := time.Now()
|
||||
defer m.queryLatencies.WithLabelValues("UpsertTailnetClient").Observe(time.Since(start).Seconds())
|
||||
return m.s.UpsertTailnetClient(ctx, arg)
|
||||
r0, r1 := m.s.UpsertTailnetClient(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("UpsertTailnetClient").Observe(time.Since(start).Seconds())
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m metricsStore) UpsertTailnetClientSubscription(ctx context.Context, arg database.UpsertTailnetClientSubscriptionParams) error {
|
||||
|
@ -2223,8 +2230,9 @@ func (m metricsStore) UpsertTailnetClientSubscription(ctx context.Context, arg d
|
|||
|
||||
func (m metricsStore) UpsertTailnetCoordinator(ctx context.Context, id uuid.UUID) (database.TailnetCoordinator, error) {
|
||||
start := time.Now()
|
||||
defer m.queryLatencies.WithLabelValues("UpsertTailnetCoordinator").Observe(time.Since(start).Seconds())
|
||||
return m.s.UpsertTailnetCoordinator(ctx, id)
|
||||
r0, r1 := m.s.UpsertTailnetCoordinator(ctx, id)
|
||||
m.queryLatencies.WithLabelValues("UpsertTailnetCoordinator").Observe(time.Since(start).Seconds())
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m metricsStore) UpsertTailnetPeer(ctx context.Context, arg database.UpsertTailnetPeerParams) (database.TailnetPeer, error) {
|
||||
|
|
|
@ -32,6 +32,8 @@ const (
|
|||
warningNodeUsesWebsocket = `Node uses WebSockets because the "Upgrade: DERP" header may be blocked on the load balancer.`
|
||||
oneNodeUnhealthy = "Region is operational, but performance might be degraded as one node is unhealthy."
|
||||
missingNodeReport = "Missing node health report, probably a developer error."
|
||||
noSTUN = "No STUN servers are available."
|
||||
stunMapVaryDest = "STUN returned different addresses; you may be behind a hard NAT."
|
||||
)
|
||||
|
||||
type ReportOptions struct {
|
||||
|
@ -107,9 +109,30 @@ func (r *Report) Run(ctx context.Context, opts *ReportOptions) {
|
|||
ncReport, netcheckErr := nc.GetReport(ctx, opts.DERPMap)
|
||||
r.Netcheck = ncReport
|
||||
r.NetcheckErr = convertError(netcheckErr)
|
||||
if mapVaryDest, _ := r.Netcheck.MappingVariesByDestIP.Get(); mapVaryDest {
|
||||
r.Warnings = append(r.Warnings, health.Messagef(health.CodeSTUNMapVaryDest, stunMapVaryDest))
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Count the number of STUN-capable nodes.
|
||||
var stunCapableNodes int
|
||||
var stunTotalNodes int
|
||||
for _, region := range r.Regions {
|
||||
for _, node := range region.NodeReports {
|
||||
if node.STUN.Enabled {
|
||||
stunTotalNodes++
|
||||
}
|
||||
if node.STUN.CanSTUN {
|
||||
stunCapableNodes++
|
||||
}
|
||||
}
|
||||
}
|
||||
if stunCapableNodes == 0 && stunTotalNodes > 0 {
|
||||
r.Severity = health.SeverityWarning
|
||||
r.Warnings = append(r.Warnings, health.Messagef(health.CodeSTUNNoNodes, noSTUN))
|
||||
}
|
||||
|
||||
// Review region reports and select the highest severity.
|
||||
for _, regionReport := range r.Regions {
|
||||
if regionReport.Severity.Value() > r.Severity.Value() {
|
||||
|
@ -133,8 +156,8 @@ func (r *RegionReport) Run(ctx context.Context) {
|
|||
node = node
|
||||
nodeReport = NodeReport{
|
||||
DERPNodeReport: healthsdk.DERPNodeReport{
|
||||
Node: node,
|
||||
Healthy: true,
|
||||
Node: node,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
|
|
@ -129,9 +129,67 @@ func TestDERP(t *testing.T) {
|
|||
assert.True(t, report.Healthy)
|
||||
assert.Equal(t, health.SeverityWarning, report.Severity)
|
||||
assert.True(t, report.Dismissed)
|
||||
if assert.NotEmpty(t, report.Warnings) {
|
||||
if assert.Len(t, report.Warnings, 1) {
|
||||
assert.Contains(t, report.Warnings[0].Code, health.CodeDERPOneNodeUnhealthy)
|
||||
}
|
||||
for _, region := range report.Regions {
|
||||
assert.True(t, region.Healthy)
|
||||
assert.True(t, region.NodeReports[0].Healthy)
|
||||
assert.Empty(t, region.NodeReports[0].Warnings)
|
||||
assert.Equal(t, health.SeverityOK, region.NodeReports[0].Severity)
|
||||
assert.False(t, region.NodeReports[1].Healthy)
|
||||
assert.Equal(t, health.SeverityError, region.NodeReports[1].Severity)
|
||||
assert.Len(t, region.Warnings, 1)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("HealthyWithNoSTUN", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
healthyDerpSrv := derp.NewServer(key.NewNode(), func(format string, args ...any) { t.Logf(format, args...) })
|
||||
defer healthyDerpSrv.Close()
|
||||
healthySrv := httptest.NewServer(derphttp.Handler(healthyDerpSrv))
|
||||
defer healthySrv.Close()
|
||||
|
||||
var (
|
||||
ctx = context.Background()
|
||||
report = derphealth.Report{}
|
||||
derpURL, _ = url.Parse(healthySrv.URL)
|
||||
opts = &derphealth.ReportOptions{
|
||||
DERPMap: &tailcfg.DERPMap{Regions: map[int]*tailcfg.DERPRegion{
|
||||
1: {
|
||||
EmbeddedRelay: true,
|
||||
RegionID: 999,
|
||||
Nodes: []*tailcfg.DERPNode{{
|
||||
Name: "1a",
|
||||
RegionID: 999,
|
||||
HostName: derpURL.Host,
|
||||
IPv4: derpURL.Host,
|
||||
STUNPort: -1,
|
||||
InsecureForTests: true,
|
||||
ForceHTTP: true,
|
||||
}, {
|
||||
Name: "badstun",
|
||||
RegionID: 999,
|
||||
HostName: derpURL.Host,
|
||||
STUNPort: 19302,
|
||||
STUNOnly: true,
|
||||
InsecureForTests: true,
|
||||
ForceHTTP: true,
|
||||
}},
|
||||
},
|
||||
}},
|
||||
}
|
||||
)
|
||||
|
||||
report.Run(ctx, opts)
|
||||
|
||||
assert.True(t, report.Healthy)
|
||||
assert.Equal(t, health.SeverityWarning, report.Severity)
|
||||
if assert.Len(t, report.Warnings, 2) {
|
||||
assert.EqualValues(t, report.Warnings[1].Code, health.CodeSTUNNoNodes)
|
||||
assert.EqualValues(t, report.Warnings[0].Code, health.CodeDERPOneNodeUnhealthy)
|
||||
}
|
||||
for _, region := range report.Regions {
|
||||
assert.True(t, region.Healthy)
|
||||
assert.True(t, region.NodeReports[0].Healthy)
|
||||
|
@ -291,8 +349,10 @@ func TestDERP(t *testing.T) {
|
|||
report.Run(ctx, opts)
|
||||
|
||||
assert.True(t, report.Healthy)
|
||||
assert.Equal(t, health.SeverityOK, report.Severity)
|
||||
for _, region := range report.Regions {
|
||||
assert.True(t, region.Healthy)
|
||||
assert.Equal(t, health.SeverityOK, region.Severity)
|
||||
for _, node := range region.NodeReports {
|
||||
assert.True(t, node.Healthy)
|
||||
assert.False(t, node.CanExchangeMessages)
|
||||
|
@ -304,6 +364,107 @@ func TestDERP(t *testing.T) {
|
|||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("STUNOnly/OneBadOneGood", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
ctx = context.Background()
|
||||
report = derphealth.Report{}
|
||||
opts = &derphealth.ReportOptions{
|
||||
DERPMap: &tailcfg.DERPMap{
|
||||
Regions: map[int]*tailcfg.DERPRegion{
|
||||
1: {
|
||||
EmbeddedRelay: true,
|
||||
RegionID: 999,
|
||||
Nodes: []*tailcfg.DERPNode{{
|
||||
Name: "badstun",
|
||||
RegionID: 999,
|
||||
HostName: "badstun.example.com",
|
||||
STUNPort: 19302,
|
||||
STUNOnly: true,
|
||||
InsecureForTests: true,
|
||||
ForceHTTP: true,
|
||||
}, {
|
||||
Name: "goodstun",
|
||||
RegionID: 999,
|
||||
HostName: "stun.l.google.com",
|
||||
STUNPort: 19302,
|
||||
STUNOnly: true,
|
||||
InsecureForTests: true,
|
||||
ForceHTTP: true,
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
report.Run(ctx, opts)
|
||||
assert.True(t, report.Healthy)
|
||||
assert.Equal(t, health.SeverityWarning, report.Severity)
|
||||
if assert.Len(t, report.Warnings, 1) {
|
||||
assert.Equal(t, health.CodeDERPOneNodeUnhealthy, report.Warnings[0].Code)
|
||||
}
|
||||
for _, region := range report.Regions {
|
||||
assert.True(t, region.Healthy)
|
||||
assert.Equal(t, health.SeverityWarning, region.Severity)
|
||||
// badstun
|
||||
assert.False(t, region.NodeReports[0].Healthy)
|
||||
assert.True(t, region.NodeReports[0].STUN.Enabled)
|
||||
assert.False(t, region.NodeReports[0].STUN.CanSTUN)
|
||||
assert.NotNil(t, region.NodeReports[0].STUN.Error)
|
||||
// goodstun
|
||||
assert.True(t, region.NodeReports[1].Healthy)
|
||||
assert.True(t, region.NodeReports[1].STUN.Enabled)
|
||||
assert.True(t, region.NodeReports[1].STUN.CanSTUN)
|
||||
assert.Nil(t, region.NodeReports[1].STUN.Error)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("STUNOnly/NoStun", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
ctx = context.Background()
|
||||
report = derphealth.Report{}
|
||||
opts = &derphealth.ReportOptions{
|
||||
DERPMap: &tailcfg.DERPMap{
|
||||
Regions: map[int]*tailcfg.DERPRegion{
|
||||
1: {
|
||||
EmbeddedRelay: true,
|
||||
RegionID: 999,
|
||||
Nodes: []*tailcfg.DERPNode{{
|
||||
Name: "badstun",
|
||||
RegionID: 999,
|
||||
HostName: "badstun.example.com",
|
||||
STUNPort: 19302,
|
||||
STUNOnly: true,
|
||||
InsecureForTests: true,
|
||||
ForceHTTP: true,
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
report.Run(ctx, opts)
|
||||
assert.False(t, report.Healthy)
|
||||
assert.Equal(t, health.SeverityError, report.Severity)
|
||||
for _, region := range report.Regions {
|
||||
assert.False(t, region.Healthy)
|
||||
assert.Equal(t, health.SeverityError, region.Severity)
|
||||
for _, node := range region.NodeReports {
|
||||
assert.False(t, node.Healthy)
|
||||
assert.False(t, node.CanExchangeMessages)
|
||||
assert.Empty(t, node.ClientLogs)
|
||||
assert.True(t, node.STUN.Enabled)
|
||||
assert.False(t, node.STUN.CanSTUN)
|
||||
assert.NotNil(t, node.STUN.Error)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func tsDERPMap(ctx context.Context, t testing.TB) *tailcfg.DERPMap {
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/coder/coder/v2/buildinfo"
|
||||
"github.com/coder/coder/v2/coderd/util/ptr"
|
||||
)
|
||||
|
||||
|
@ -36,12 +37,19 @@ const (
|
|||
|
||||
CodeDERPNodeUsesWebsocket Code = `EDERP01`
|
||||
CodeDERPOneNodeUnhealthy Code = `EDERP02`
|
||||
CodeSTUNNoNodes = `ESTUN01`
|
||||
CodeSTUNMapVaryDest = `ESTUN02`
|
||||
|
||||
CodeProvisionerDaemonsNoProvisionerDaemons Code = `EPD01`
|
||||
CodeProvisionerDaemonVersionMismatch Code = `EPD02`
|
||||
CodeProvisionerDaemonAPIMajorVersionDeprecated Code = `EPD03`
|
||||
)
|
||||
|
||||
// Default docs URL
|
||||
var (
|
||||
docsURLDefault = "https://coder.com/docs/v2"
|
||||
)
|
||||
|
||||
// @typescript-generate Severity
|
||||
type Severity string
|
||||
|
||||
|
@ -70,6 +78,30 @@ func (m Message) String() string {
|
|||
return sb.String()
|
||||
}
|
||||
|
||||
// URL returns a link to the admin/healthcheck docs page for the given Message.
|
||||
// NOTE: if using a custom docs URL, specify base.
|
||||
func (m Message) URL(base string) string {
|
||||
var codeAnchor string
|
||||
if m.Code == "" {
|
||||
codeAnchor = strings.ToLower(string(CodeUnknown))
|
||||
} else {
|
||||
codeAnchor = strings.ToLower(string(m.Code))
|
||||
}
|
||||
|
||||
if base == "" {
|
||||
base = docsURLDefault
|
||||
versionPath := buildinfo.Version()
|
||||
if buildinfo.IsDev() {
|
||||
// for development versions, just use latest
|
||||
versionPath = "latest"
|
||||
}
|
||||
return fmt.Sprintf("%s/%s/admin/healthcheck#%s", base, versionPath, codeAnchor)
|
||||
}
|
||||
|
||||
// We don't assume that custom docs URLs are versioned.
|
||||
return fmt.Sprintf("%s/admin/healthcheck#%s", base, codeAnchor)
|
||||
}
|
||||
|
||||
// Code is a stable identifier used to link to documentation.
|
||||
// @typescript-generate Code
|
||||
type Code string
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
package health_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/healthcheck/health"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_MessageURL(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
code health.Code
|
||||
base string
|
||||
expected string
|
||||
}{
|
||||
{"empty", "", "", "https://coder.com/docs/v2/latest/admin/healthcheck#eunknown"},
|
||||
{"default", health.CodeAccessURLFetch, "", "https://coder.com/docs/v2/latest/admin/healthcheck#eacs03"},
|
||||
{"custom docs base", health.CodeAccessURLFetch, "https://example.com/docs", "https://example.com/docs/admin/healthcheck#eacs03"},
|
||||
} {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
uut := health.Message{Code: tt.code}
|
||||
actual := uut.URL(tt.base)
|
||||
assert.Equal(t, tt.expected, actual)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -58,27 +58,39 @@ func TestHealthcheck(t *testing.T) {
|
|||
name: "OK",
|
||||
checker: &testChecker{
|
||||
DERPReport: healthsdk.DERPHealthReport{
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
},
|
||||
AccessURLReport: healthsdk.AccessURLReport{
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
},
|
||||
WebsocketReport: healthsdk.WebsocketReport{
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
},
|
||||
DatabaseReport: healthsdk.DatabaseReport{
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
},
|
||||
WorkspaceProxyReport: healthsdk.WorkspaceProxyReport{
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
},
|
||||
ProvisionerDaemonsReport: healthsdk.ProvisionerDaemonsReport{
|
||||
Severity: health.SeverityOK,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
},
|
||||
},
|
||||
healthy: true,
|
||||
|
@ -88,27 +100,39 @@ func TestHealthcheck(t *testing.T) {
|
|||
name: "DERPFail",
|
||||
checker: &testChecker{
|
||||
DERPReport: healthsdk.DERPHealthReport{
|
||||
Healthy: false,
|
||||
Severity: health.SeverityError,
|
||||
Healthy: false,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityError,
|
||||
},
|
||||
},
|
||||
AccessURLReport: healthsdk.AccessURLReport{
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
},
|
||||
WebsocketReport: healthsdk.WebsocketReport{
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
},
|
||||
DatabaseReport: healthsdk.DatabaseReport{
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
},
|
||||
WorkspaceProxyReport: healthsdk.WorkspaceProxyReport{
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
},
|
||||
ProvisionerDaemonsReport: healthsdk.ProvisionerDaemonsReport{
|
||||
Severity: health.SeverityOK,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
},
|
||||
},
|
||||
healthy: false,
|
||||
|
@ -118,28 +142,40 @@ func TestHealthcheck(t *testing.T) {
|
|||
name: "DERPWarning",
|
||||
checker: &testChecker{
|
||||
DERPReport: healthsdk.DERPHealthReport{
|
||||
Healthy: true,
|
||||
Warnings: []health.Message{{Message: "foobar", Code: "EFOOBAR"}},
|
||||
Severity: health.SeverityWarning,
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Warnings: []health.Message{{Message: "foobar", Code: "EFOOBAR"}},
|
||||
Severity: health.SeverityWarning,
|
||||
},
|
||||
},
|
||||
AccessURLReport: healthsdk.AccessURLReport{
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
},
|
||||
WebsocketReport: healthsdk.WebsocketReport{
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
},
|
||||
DatabaseReport: healthsdk.DatabaseReport{
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
},
|
||||
WorkspaceProxyReport: healthsdk.WorkspaceProxyReport{
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
},
|
||||
ProvisionerDaemonsReport: healthsdk.ProvisionerDaemonsReport{
|
||||
Severity: health.SeverityOK,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
},
|
||||
},
|
||||
healthy: true,
|
||||
|
@ -149,27 +185,39 @@ func TestHealthcheck(t *testing.T) {
|
|||
name: "AccessURLFail",
|
||||
checker: &testChecker{
|
||||
DERPReport: healthsdk.DERPHealthReport{
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
},
|
||||
AccessURLReport: healthsdk.AccessURLReport{
|
||||
Healthy: false,
|
||||
Severity: health.SeverityWarning,
|
||||
Healthy: false,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityWarning,
|
||||
},
|
||||
},
|
||||
WebsocketReport: healthsdk.WebsocketReport{
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
},
|
||||
DatabaseReport: healthsdk.DatabaseReport{
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
},
|
||||
WorkspaceProxyReport: healthsdk.WorkspaceProxyReport{
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
},
|
||||
ProvisionerDaemonsReport: healthsdk.ProvisionerDaemonsReport{
|
||||
Severity: health.SeverityOK,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
},
|
||||
},
|
||||
healthy: false,
|
||||
|
@ -179,27 +227,39 @@ func TestHealthcheck(t *testing.T) {
|
|||
name: "WebsocketFail",
|
||||
checker: &testChecker{
|
||||
DERPReport: healthsdk.DERPHealthReport{
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
},
|
||||
AccessURLReport: healthsdk.AccessURLReport{
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
},
|
||||
WebsocketReport: healthsdk.WebsocketReport{
|
||||
Healthy: false,
|
||||
Severity: health.SeverityError,
|
||||
Healthy: false,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityError,
|
||||
},
|
||||
},
|
||||
DatabaseReport: healthsdk.DatabaseReport{
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
},
|
||||
WorkspaceProxyReport: healthsdk.WorkspaceProxyReport{
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
},
|
||||
ProvisionerDaemonsReport: healthsdk.ProvisionerDaemonsReport{
|
||||
Severity: health.SeverityOK,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
},
|
||||
},
|
||||
healthy: false,
|
||||
|
@ -209,27 +269,39 @@ func TestHealthcheck(t *testing.T) {
|
|||
name: "DatabaseFail",
|
||||
checker: &testChecker{
|
||||
DERPReport: healthsdk.DERPHealthReport{
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
},
|
||||
AccessURLReport: healthsdk.AccessURLReport{
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
},
|
||||
WebsocketReport: healthsdk.WebsocketReport{
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
},
|
||||
DatabaseReport: healthsdk.DatabaseReport{
|
||||
Healthy: false,
|
||||
Severity: health.SeverityError,
|
||||
Healthy: false,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityError,
|
||||
},
|
||||
},
|
||||
WorkspaceProxyReport: healthsdk.WorkspaceProxyReport{
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
},
|
||||
ProvisionerDaemonsReport: healthsdk.ProvisionerDaemonsReport{
|
||||
Severity: health.SeverityOK,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
},
|
||||
},
|
||||
healthy: false,
|
||||
|
@ -239,27 +311,39 @@ func TestHealthcheck(t *testing.T) {
|
|||
name: "ProxyFail",
|
||||
checker: &testChecker{
|
||||
DERPReport: healthsdk.DERPHealthReport{
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
},
|
||||
AccessURLReport: healthsdk.AccessURLReport{
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
},
|
||||
WebsocketReport: healthsdk.WebsocketReport{
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
},
|
||||
DatabaseReport: healthsdk.DatabaseReport{
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
},
|
||||
WorkspaceProxyReport: healthsdk.WorkspaceProxyReport{
|
||||
Healthy: false,
|
||||
Severity: health.SeverityError,
|
||||
Healthy: false,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityError,
|
||||
},
|
||||
},
|
||||
ProvisionerDaemonsReport: healthsdk.ProvisionerDaemonsReport{
|
||||
Severity: health.SeverityOK,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
},
|
||||
},
|
||||
severity: health.SeverityError,
|
||||
|
@ -269,28 +353,40 @@ func TestHealthcheck(t *testing.T) {
|
|||
name: "ProxyWarn",
|
||||
checker: &testChecker{
|
||||
DERPReport: healthsdk.DERPHealthReport{
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
},
|
||||
AccessURLReport: healthsdk.AccessURLReport{
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
},
|
||||
WebsocketReport: healthsdk.WebsocketReport{
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
},
|
||||
DatabaseReport: healthsdk.DatabaseReport{
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
},
|
||||
WorkspaceProxyReport: healthsdk.WorkspaceProxyReport{
|
||||
Healthy: true,
|
||||
Warnings: []health.Message{{Message: "foobar", Code: "EFOOBAR"}},
|
||||
Severity: health.SeverityWarning,
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Warnings: []health.Message{{Message: "foobar", Code: "EFOOBAR"}},
|
||||
Severity: health.SeverityWarning,
|
||||
},
|
||||
},
|
||||
ProvisionerDaemonsReport: healthsdk.ProvisionerDaemonsReport{
|
||||
Severity: health.SeverityOK,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
},
|
||||
},
|
||||
severity: health.SeverityWarning,
|
||||
|
@ -300,27 +396,39 @@ func TestHealthcheck(t *testing.T) {
|
|||
name: "ProvisionerDaemonsFail",
|
||||
checker: &testChecker{
|
||||
DERPReport: healthsdk.DERPHealthReport{
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
},
|
||||
AccessURLReport: healthsdk.AccessURLReport{
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
},
|
||||
WebsocketReport: healthsdk.WebsocketReport{
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
},
|
||||
DatabaseReport: healthsdk.DatabaseReport{
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
},
|
||||
WorkspaceProxyReport: healthsdk.WorkspaceProxyReport{
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
},
|
||||
ProvisionerDaemonsReport: healthsdk.ProvisionerDaemonsReport{
|
||||
Severity: health.SeverityError,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityError,
|
||||
},
|
||||
},
|
||||
},
|
||||
severity: health.SeverityError,
|
||||
|
@ -330,28 +438,40 @@ func TestHealthcheck(t *testing.T) {
|
|||
name: "ProvisionerDaemonsWarn",
|
||||
checker: &testChecker{
|
||||
DERPReport: healthsdk.DERPHealthReport{
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
},
|
||||
AccessURLReport: healthsdk.AccessURLReport{
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
},
|
||||
WebsocketReport: healthsdk.WebsocketReport{
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
},
|
||||
DatabaseReport: healthsdk.DatabaseReport{
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
},
|
||||
WorkspaceProxyReport: healthsdk.WorkspaceProxyReport{
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
},
|
||||
ProvisionerDaemonsReport: healthsdk.ProvisionerDaemonsReport{
|
||||
Severity: health.SeverityWarning,
|
||||
Warnings: []health.Message{{Message: "foobar", Code: "EFOOBAR"}},
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityWarning,
|
||||
Warnings: []health.Message{{Message: "foobar", Code: "EFOOBAR"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
severity: health.SeverityWarning,
|
||||
|
@ -362,27 +482,39 @@ func TestHealthcheck(t *testing.T) {
|
|||
healthy: false,
|
||||
checker: &testChecker{
|
||||
DERPReport: healthsdk.DERPHealthReport{
|
||||
Healthy: false,
|
||||
Severity: health.SeverityError,
|
||||
Healthy: false,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityError,
|
||||
},
|
||||
},
|
||||
AccessURLReport: healthsdk.AccessURLReport{
|
||||
Healthy: false,
|
||||
Severity: health.SeverityError,
|
||||
Healthy: false,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityError,
|
||||
},
|
||||
},
|
||||
WebsocketReport: healthsdk.WebsocketReport{
|
||||
Healthy: false,
|
||||
Severity: health.SeverityError,
|
||||
Healthy: false,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityError,
|
||||
},
|
||||
},
|
||||
DatabaseReport: healthsdk.DatabaseReport{
|
||||
Healthy: false,
|
||||
Severity: health.SeverityError,
|
||||
Healthy: false,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityError,
|
||||
},
|
||||
},
|
||||
WorkspaceProxyReport: healthsdk.WorkspaceProxyReport{
|
||||
Healthy: false,
|
||||
Severity: health.SeverityError,
|
||||
Healthy: false,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityError,
|
||||
},
|
||||
},
|
||||
ProvisionerDaemonsReport: healthsdk.ProvisionerDaemonsReport{
|
||||
Severity: health.SeverityError,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityError,
|
||||
},
|
||||
},
|
||||
},
|
||||
severity: health.SeverityError,
|
||||
|
|
|
@ -31,7 +31,7 @@ func (r *WebsocketReport) Run(ctx context.Context, opts *WebsocketReportOptions)
|
|||
defer cancel()
|
||||
|
||||
r.Severity = health.SeverityOK
|
||||
r.Warnings = []string{}
|
||||
r.Warnings = []health.Message{}
|
||||
r.Dismissed = opts.Dismissed
|
||||
|
||||
u, err := opts.AccessURL.Parse("/api/v2/debug/ws")
|
||||
|
|
|
@ -624,6 +624,7 @@ func ConvertWorkspaceResource(resource database.WorkspaceResource) WorkspaceReso
|
|||
return WorkspaceResource{
|
||||
ID: resource.ID,
|
||||
JobID: resource.JobID,
|
||||
CreatedAt: resource.CreatedAt,
|
||||
Transition: resource.Transition,
|
||||
Type: resource.Type,
|
||||
InstanceType: resource.InstanceType.String,
|
||||
|
@ -833,6 +834,7 @@ type User struct {
|
|||
|
||||
type WorkspaceResource struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
JobID uuid.UUID `json:"job_id"`
|
||||
Transition database.WorkspaceTransition `json:"transition"`
|
||||
Type string `json:"type"`
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
|
@ -95,6 +96,7 @@ func (c *HealthClient) PutHealthSettings(ctx context.Context, settings HealthSet
|
|||
return nil
|
||||
}
|
||||
|
||||
// HealthcheckReport contains information about the health status of a Coder deployment.
|
||||
type HealthcheckReport struct {
|
||||
// Time is the time the report was generated at.
|
||||
Time time.Time `json:"time" format:"date-time"`
|
||||
|
@ -117,52 +119,97 @@ type HealthcheckReport struct {
|
|||
CoderVersion string `json:"coder_version"`
|
||||
}
|
||||
|
||||
// Summarize returns a summary of all errors and warnings of components of HealthcheckReport.
|
||||
func (r *HealthcheckReport) Summarize(docsURL string) []string {
|
||||
var msgs []string
|
||||
msgs = append(msgs, r.AccessURL.Summarize("Access URL:", docsURL)...)
|
||||
msgs = append(msgs, r.Database.Summarize("Database:", docsURL)...)
|
||||
msgs = append(msgs, r.DERP.Summarize("DERP:", docsURL)...)
|
||||
msgs = append(msgs, r.ProvisionerDaemons.Summarize("Provisioner Daemons:", docsURL)...)
|
||||
msgs = append(msgs, r.Websocket.Summarize("Websocket:", docsURL)...)
|
||||
msgs = append(msgs, r.WorkspaceProxy.Summarize("Workspace Proxies:", docsURL)...)
|
||||
return msgs
|
||||
}
|
||||
|
||||
// BaseReport holds fields common to various health reports.
|
||||
type BaseReport struct {
|
||||
Error *string `json:"error"`
|
||||
Severity health.Severity `json:"severity" enums:"ok,warning,error"`
|
||||
Warnings []health.Message `json:"warnings"`
|
||||
Dismissed bool `json:"dismissed"`
|
||||
}
|
||||
|
||||
// Summarize returns a list of strings containing the errors and warnings of BaseReport, if present.
|
||||
// All strings are prefixed with prefix.
|
||||
func (b *BaseReport) Summarize(prefix, docsURL string) []string {
|
||||
if b == nil {
|
||||
return []string{}
|
||||
}
|
||||
var msgs []string
|
||||
if b.Error != nil {
|
||||
var sb strings.Builder
|
||||
if prefix != "" {
|
||||
_, _ = sb.WriteString(prefix)
|
||||
_, _ = sb.WriteString(" ")
|
||||
}
|
||||
_, _ = sb.WriteString("Error: ")
|
||||
_, _ = sb.WriteString(*b.Error)
|
||||
msgs = append(msgs, sb.String())
|
||||
}
|
||||
for _, warn := range b.Warnings {
|
||||
var sb strings.Builder
|
||||
if prefix != "" {
|
||||
_, _ = sb.WriteString(prefix)
|
||||
_, _ = sb.WriteString(" ")
|
||||
}
|
||||
_, _ = sb.WriteString("Warn: ")
|
||||
_, _ = sb.WriteString(warn.String())
|
||||
msgs = append(msgs, sb.String())
|
||||
msgs = append(msgs, "See: "+warn.URL(docsURL))
|
||||
}
|
||||
return msgs
|
||||
}
|
||||
|
||||
// AccessURLReport shows the results of performing a HTTP_GET to the /healthz endpoint through the configured access URL.
|
||||
type AccessURLReport struct {
|
||||
BaseReport
|
||||
// Healthy is deprecated and left for backward compatibility purposes, use `Severity` instead.
|
||||
Healthy bool `json:"healthy"`
|
||||
Severity health.Severity `json:"severity" enums:"ok,warning,error"`
|
||||
Warnings []health.Message `json:"warnings"`
|
||||
Dismissed bool `json:"dismissed"`
|
||||
|
||||
AccessURL string `json:"access_url"`
|
||||
Reachable bool `json:"reachable"`
|
||||
StatusCode int `json:"status_code"`
|
||||
HealthzResponse string `json:"healthz_response"`
|
||||
Error *string `json:"error"`
|
||||
Healthy bool `json:"healthy"`
|
||||
AccessURL string `json:"access_url"`
|
||||
Reachable bool `json:"reachable"`
|
||||
StatusCode int `json:"status_code"`
|
||||
HealthzResponse string `json:"healthz_response"`
|
||||
}
|
||||
|
||||
// DERPHealthReport includes health details of each configured DERP/STUN region.
|
||||
type DERPHealthReport struct {
|
||||
BaseReport
|
||||
// Healthy is deprecated and left for backward compatibility purposes, use `Severity` instead.
|
||||
Healthy bool `json:"healthy"`
|
||||
Severity health.Severity `json:"severity" enums:"ok,warning,error"`
|
||||
Warnings []health.Message `json:"warnings"`
|
||||
Dismissed bool `json:"dismissed"`
|
||||
|
||||
Regions map[int]*DERPRegionReport `json:"regions"`
|
||||
|
||||
Netcheck *netcheck.Report `json:"netcheck"`
|
||||
NetcheckErr *string `json:"netcheck_err"`
|
||||
NetcheckLogs []string `json:"netcheck_logs"`
|
||||
|
||||
Error *string `json:"error"`
|
||||
Healthy bool `json:"healthy"`
|
||||
Regions map[int]*DERPRegionReport `json:"regions"`
|
||||
Netcheck *netcheck.Report `json:"netcheck"`
|
||||
NetcheckErr *string `json:"netcheck_err"`
|
||||
NetcheckLogs []string `json:"netcheck_logs"`
|
||||
}
|
||||
|
||||
// DERPHealthReport includes health details of each node in a single region.
|
||||
type DERPRegionReport struct {
|
||||
// Healthy is deprecated and left for backward compatibility purposes, use `Severity` instead.
|
||||
Healthy bool `json:"healthy"`
|
||||
Severity health.Severity `json:"severity" enums:"ok,warning,error"`
|
||||
Warnings []health.Message `json:"warnings"`
|
||||
|
||||
Healthy bool `json:"healthy"`
|
||||
Severity health.Severity `json:"severity" enums:"ok,warning,error"`
|
||||
Warnings []health.Message `json:"warnings"`
|
||||
Error *string `json:"error"`
|
||||
Region *tailcfg.DERPRegion `json:"region"`
|
||||
NodeReports []*DERPNodeReport `json:"node_reports"`
|
||||
Error *string `json:"error"`
|
||||
}
|
||||
|
||||
// DERPHealthReport includes health details of a single node in a single region.
|
||||
type DERPNodeReport struct {
|
||||
// Healthy is deprecated and left for backward compatibility purposes, use `Severity` instead.
|
||||
Healthy bool `json:"healthy"`
|
||||
Severity health.Severity `json:"severity" enums:"ok,warning,error"`
|
||||
Warnings []health.Message `json:"warnings"`
|
||||
Error *string `json:"error"`
|
||||
|
||||
Node *tailcfg.DERPNode `json:"node"`
|
||||
|
||||
|
@ -173,37 +220,31 @@ type DERPNodeReport struct {
|
|||
UsesWebsocket bool `json:"uses_websocket"`
|
||||
ClientLogs [][]string `json:"client_logs"`
|
||||
ClientErrs [][]string `json:"client_errs"`
|
||||
Error *string `json:"error"`
|
||||
|
||||
STUN STUNReport `json:"stun"`
|
||||
}
|
||||
|
||||
// STUNReport contains information about a given node's STUN capabilities.
|
||||
type STUNReport struct {
|
||||
Enabled bool
|
||||
CanSTUN bool
|
||||
Error *string
|
||||
}
|
||||
|
||||
// DatabaseReport shows the results of pinging the configured database.Conn.
|
||||
type DatabaseReport struct {
|
||||
BaseReport
|
||||
// Healthy is deprecated and left for backward compatibility purposes, use `Severity` instead.
|
||||
Healthy bool `json:"healthy"`
|
||||
Severity health.Severity `json:"severity" enums:"ok,warning,error"`
|
||||
Warnings []health.Message `json:"warnings"`
|
||||
Dismissed bool `json:"dismissed"`
|
||||
|
||||
Reachable bool `json:"reachable"`
|
||||
Latency string `json:"latency"`
|
||||
LatencyMS int64 `json:"latency_ms"`
|
||||
ThresholdMS int64 `json:"threshold_ms"`
|
||||
Error *string `json:"error"`
|
||||
Healthy bool `json:"healthy"`
|
||||
Reachable bool `json:"reachable"`
|
||||
Latency string `json:"latency"`
|
||||
LatencyMS int64 `json:"latency_ms"`
|
||||
ThresholdMS int64 `json:"threshold_ms"`
|
||||
}
|
||||
|
||||
// ProvisionerDaemonsReport includes health details of each connected provisioner daemon.
|
||||
type ProvisionerDaemonsReport struct {
|
||||
Severity health.Severity `json:"severity"`
|
||||
Warnings []health.Message `json:"warnings"`
|
||||
Dismissed bool `json:"dismissed"`
|
||||
Error *string `json:"error"`
|
||||
|
||||
BaseReport
|
||||
Items []ProvisionerDaemonsReportItem `json:"items"`
|
||||
}
|
||||
|
||||
|
@ -212,24 +253,19 @@ type ProvisionerDaemonsReportItem struct {
|
|||
Warnings []health.Message `json:"warnings"`
|
||||
}
|
||||
|
||||
// WebsocketReport shows if the configured access URL allows establishing WebSocket connections.
|
||||
type WebsocketReport struct {
|
||||
// Healthy is deprecated and left for backward compatibility purposes, use `Severity` instead.
|
||||
Healthy bool `json:"healthy"`
|
||||
Severity health.Severity `json:"severity" enums:"ok,warning,error"`
|
||||
Warnings []string `json:"warnings"`
|
||||
Dismissed bool `json:"dismissed"`
|
||||
|
||||
Body string `json:"body"`
|
||||
Code int `json:"code"`
|
||||
Error *string `json:"error"`
|
||||
Healthy bool `json:"healthy"`
|
||||
BaseReport
|
||||
Body string `json:"body"`
|
||||
Code int `json:"code"`
|
||||
}
|
||||
|
||||
// WorkspaceProxyReport includes health details of each connected workspace proxy.
|
||||
type WorkspaceProxyReport struct {
|
||||
Healthy bool `json:"healthy"`
|
||||
Severity health.Severity `json:"severity"`
|
||||
Warnings []health.Message `json:"warnings"`
|
||||
Dismissed bool `json:"dismissed"`
|
||||
Error *string `json:"error"`
|
||||
|
||||
// Healthy is deprecated and left for backward compatibility purposes, use `Severity` instead.
|
||||
Healthy bool `json:"healthy"`
|
||||
BaseReport
|
||||
WorkspaceProxies codersdk.RegionsResponse[codersdk.WorkspaceProxy] `json:"workspace_proxies"`
|
||||
}
|
||||
|
|
|
@ -0,0 +1,137 @@
|
|||
package healthsdk_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/healthcheck/health"
|
||||
"github.com/coder/coder/v2/coderd/util/ptr"
|
||||
"github.com/coder/coder/v2/codersdk/healthsdk"
|
||||
)
|
||||
|
||||
func TestSummarize(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("HealthcheckReport", func(t *testing.T) {
|
||||
unhealthy := healthsdk.BaseReport{
|
||||
Error: ptr.Ref("test error"),
|
||||
Warnings: []health.Message{{Code: "TEST", Message: "testing"}},
|
||||
}
|
||||
hr := healthsdk.HealthcheckReport{
|
||||
AccessURL: healthsdk.AccessURLReport{
|
||||
BaseReport: unhealthy,
|
||||
},
|
||||
Database: healthsdk.DatabaseReport{
|
||||
BaseReport: unhealthy,
|
||||
},
|
||||
DERP: healthsdk.DERPHealthReport{
|
||||
BaseReport: unhealthy,
|
||||
},
|
||||
ProvisionerDaemons: healthsdk.ProvisionerDaemonsReport{
|
||||
BaseReport: unhealthy,
|
||||
},
|
||||
Websocket: healthsdk.WebsocketReport{
|
||||
BaseReport: unhealthy,
|
||||
},
|
||||
WorkspaceProxy: healthsdk.WorkspaceProxyReport{
|
||||
BaseReport: unhealthy,
|
||||
},
|
||||
}
|
||||
expected := []string{
|
||||
"Access URL: Error: test error",
|
||||
"Access URL: Warn: TEST: testing",
|
||||
"See: https://coder.com/docs/v2/latest/admin/healthcheck#test",
|
||||
"Database: Error: test error",
|
||||
"Database: Warn: TEST: testing",
|
||||
"See: https://coder.com/docs/v2/latest/admin/healthcheck#test",
|
||||
"DERP: Error: test error",
|
||||
"DERP: Warn: TEST: testing",
|
||||
"See: https://coder.com/docs/v2/latest/admin/healthcheck#test",
|
||||
"Provisioner Daemons: Error: test error",
|
||||
"Provisioner Daemons: Warn: TEST: testing",
|
||||
"See: https://coder.com/docs/v2/latest/admin/healthcheck#test",
|
||||
"Websocket: Error: test error",
|
||||
"Websocket: Warn: TEST: testing",
|
||||
"See: https://coder.com/docs/v2/latest/admin/healthcheck#test",
|
||||
"Workspace Proxies: Error: test error",
|
||||
"Workspace Proxies: Warn: TEST: testing",
|
||||
"See: https://coder.com/docs/v2/latest/admin/healthcheck#test",
|
||||
}
|
||||
actual := hr.Summarize("")
|
||||
assert.Equal(t, expected, actual)
|
||||
})
|
||||
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
br healthsdk.BaseReport
|
||||
pfx string
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
br: healthsdk.BaseReport{},
|
||||
pfx: "",
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
name: "no prefix",
|
||||
br: healthsdk.BaseReport{
|
||||
Error: ptr.Ref("testing"),
|
||||
Warnings: []health.Message{
|
||||
{
|
||||
Code: "TEST01",
|
||||
Message: "testing one",
|
||||
},
|
||||
{
|
||||
Code: "TEST02",
|
||||
Message: "testing two",
|
||||
},
|
||||
},
|
||||
},
|
||||
pfx: "",
|
||||
expected: []string{
|
||||
"Error: testing",
|
||||
"Warn: TEST01: testing one",
|
||||
"See: https://coder.com/docs/v2/latest/admin/healthcheck#test01",
|
||||
"Warn: TEST02: testing two",
|
||||
"See: https://coder.com/docs/v2/latest/admin/healthcheck#test02",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "prefix",
|
||||
br: healthsdk.BaseReport{
|
||||
Error: ptr.Ref("testing"),
|
||||
Warnings: []health.Message{
|
||||
{
|
||||
Code: "TEST01",
|
||||
Message: "testing one",
|
||||
},
|
||||
{
|
||||
Code: "TEST02",
|
||||
Message: "testing two",
|
||||
},
|
||||
},
|
||||
},
|
||||
pfx: "TEST:",
|
||||
expected: []string{
|
||||
"TEST: Error: testing",
|
||||
"TEST: Warn: TEST01: testing one",
|
||||
"See: https://coder.com/docs/v2/latest/admin/healthcheck#test01",
|
||||
"TEST: Warn: TEST02: testing two",
|
||||
"See: https://coder.com/docs/v2/latest/admin/healthcheck#test02",
|
||||
},
|
||||
},
|
||||
} {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
actual := tt.br.Summarize(tt.pfx, "")
|
||||
if len(tt.expected) == 0 {
|
||||
assert.Empty(t, actual)
|
||||
return
|
||||
}
|
||||
assert.Equal(t, tt.expected, actual)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -170,6 +170,31 @@ curl -v "https://coder.company.com/derp"
|
|||
# DERP requires connection upgrade
|
||||
```
|
||||
|
||||
### ESTUN01
|
||||
|
||||
_No STUN servers available._
|
||||
|
||||
**Problem:** This is shown if no STUN servers are available. Coder will use STUN
|
||||
to establish [direct connections](../networking/stun.md). Without at least one
|
||||
working STUN server, direct connections may not be possible.
|
||||
|
||||
**Solution:** Ensure that the
|
||||
[configured STUN severs](../cli/server.md#derp-server-stun-addresses) are
|
||||
reachable from Coder and that UDP traffic can be sent/received on the configured
|
||||
port.
|
||||
|
||||
### ESTUN02
|
||||
|
||||
_STUN returned different addresses; you may be behind a hard NAT._
|
||||
|
||||
**Problem:** This is a warning shown when multiple attempts to determine our
|
||||
public IP address/port via STUN resulted in different `ip:port` combinations.
|
||||
This is a sign that you are behind a "hard NAT", and may result in difficulty
|
||||
establishing direct connections. However, it does not mean that direct
|
||||
connections are impossible.
|
||||
|
||||
**Solution:** Engage with your network administrator.
|
||||
|
||||
## Websocket
|
||||
|
||||
Coder makes heavy use of [WebSockets](https://datatracker.ietf.org/doc/rfc6455/)
|
||||
|
|
|
@ -325,7 +325,12 @@ curl -X GET http://coder-server:8080/api/v2/debug/health \
|
|||
"error": "string",
|
||||
"healthy": true,
|
||||
"severity": "ok",
|
||||
"warnings": ["string"]
|
||||
"warnings": [
|
||||
{
|
||||
"code": "EUNKNOWN",
|
||||
"message": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"workspace_proxy": {
|
||||
"dismissed": true,
|
||||
|
|
|
@ -8134,7 +8134,12 @@ If the schedule is empty, the user will be updated to use the default schedule.|
|
|||
"error": "string",
|
||||
"healthy": true,
|
||||
"severity": "ok",
|
||||
"warnings": ["string"]
|
||||
"warnings": [
|
||||
{
|
||||
"code": "EUNKNOWN",
|
||||
"message": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"workspace_proxy": {
|
||||
"dismissed": true,
|
||||
|
@ -8251,6 +8256,14 @@ If the schedule is empty, the user will be updated to use the default schedule.|
|
|||
| `severity` | [health.Severity](#healthseverity) | false | | |
|
||||
| `warnings` | array of [health.Message](#healthmessage) | false | | |
|
||||
|
||||
#### Enumerated Values
|
||||
|
||||
| Property | Value |
|
||||
| ---------- | --------- |
|
||||
| `severity` | `ok` |
|
||||
| `severity` | `warning` |
|
||||
| `severity` | `error` |
|
||||
|
||||
## healthsdk.ProvisionerDaemonsReportItem
|
||||
|
||||
```json
|
||||
|
@ -8326,21 +8339,26 @@ If the schedule is empty, the user will be updated to use the default schedule.|
|
|||
"error": "string",
|
||||
"healthy": true,
|
||||
"severity": "ok",
|
||||
"warnings": ["string"]
|
||||
"warnings": [
|
||||
{
|
||||
"code": "EUNKNOWN",
|
||||
"message": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
| ----------- | ---------------------------------- | -------- | ------------ | ------------------------------------------------------------------------------------------- |
|
||||
| `body` | string | false | | |
|
||||
| `code` | integer | false | | |
|
||||
| `dismissed` | boolean | false | | |
|
||||
| `error` | string | false | | |
|
||||
| `healthy` | boolean | false | | Healthy is deprecated and left for backward compatibility purposes, use `Severity` instead. |
|
||||
| `severity` | [health.Severity](#healthseverity) | false | | |
|
||||
| `warnings` | array of string | false | | |
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
| ----------- | ----------------------------------------- | -------- | ------------ | ------------------------------------------------------------------------------------------- |
|
||||
| `body` | string | false | | |
|
||||
| `code` | integer | false | | |
|
||||
| `dismissed` | boolean | false | | |
|
||||
| `error` | string | false | | |
|
||||
| `healthy` | boolean | false | | Healthy is deprecated and left for backward compatibility purposes, use `Severity` instead. |
|
||||
| `severity` | [health.Severity](#healthseverity) | false | | |
|
||||
| `warnings` | array of [health.Message](#healthmessage) | false | | |
|
||||
|
||||
#### Enumerated Values
|
||||
|
||||
|
@ -8396,14 +8414,22 @@ If the schedule is empty, the user will be updated to use the default schedule.|
|
|||
|
||||
### Properties
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
| ------------------- | ---------------------------------------------------------------------------------------------------- | -------- | ------------ | ----------- |
|
||||
| `dismissed` | boolean | false | | |
|
||||
| `error` | string | false | | |
|
||||
| `healthy` | boolean | false | | |
|
||||
| `severity` | [health.Severity](#healthseverity) | false | | |
|
||||
| `warnings` | array of [health.Message](#healthmessage) | false | | |
|
||||
| `workspace_proxies` | [codersdk.RegionsResponse-codersdk_WorkspaceProxy](#codersdkregionsresponse-codersdk_workspaceproxy) | false | | |
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
| ------------------- | ---------------------------------------------------------------------------------------------------- | -------- | ------------ | ------------------------------------------------------------------------------------------- |
|
||||
| `dismissed` | boolean | false | | |
|
||||
| `error` | string | false | | |
|
||||
| `healthy` | boolean | false | | Healthy is deprecated and left for backward compatibility purposes, use `Severity` instead. |
|
||||
| `severity` | [health.Severity](#healthseverity) | false | | |
|
||||
| `warnings` | array of [health.Message](#healthmessage) | false | | |
|
||||
| `workspace_proxies` | [codersdk.RegionsResponse-codersdk_WorkspaceProxy](#codersdkregionsresponse-codersdk_workspaceproxy) | false | | |
|
||||
|
||||
#### Enumerated Values
|
||||
|
||||
| Property | Value |
|
||||
| ---------- | --------- |
|
||||
| `severity` | `ok` |
|
||||
| `severity` | `warning` |
|
||||
| `severity` | `error` |
|
||||
|
||||
## key.NodePublic
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ March 17, 2024
|
|||
|
||||
This guide will walk you through the process of adding
|
||||
[JFrog Xray](https://jfrog.com/xray/) integration to Coder Kubernetes workspaces
|
||||
using Coder's [JFrog Xray Integration](github.com/coder/coder-xray).
|
||||
using Coder's [JFrog Xray Integration](https://github.com/coder/coder-xray).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
|
@ -65,8 +65,8 @@ image = "<ARTIFACTORY_URL>/<REPO>/<IMAGE>:<TAG>"
|
|||
|
||||
> **Note**: To authenticate with the Artifactory registry, you may need to
|
||||
> create a
|
||||
> [Docker config](https://jfrog.com/artifactory/docs/docker/#docker-login) and
|
||||
> use it in the `imagePullSecrets` field of the kubernetes pod. See this
|
||||
> [Docker config](https://jfrog.com/help/r/jfrog-artifactory-documentation/docker-advanced-topics)
|
||||
> and use it in the `imagePullSecrets` field of the kubernetes pod. See this
|
||||
> [guide](./image-pull-secret.md) for more information.
|
||||
|
||||
![JFrog Xray Integration](../images/guides/xray-integration/example.png)
|
||||
|
|
|
@ -213,7 +213,7 @@ while ! maybedryrun "$DRY_RUN" timeout 1 bash -c "echo > /dev/tcp/localhost/6061
|
|||
echo "pprof failed to become ready in time!"
|
||||
exit 1
|
||||
fi
|
||||
pprof_attempt_counter+=1
|
||||
((pprof_attempt_counter += 1))
|
||||
maybedryrun "$DRY_RUN" sleep 3
|
||||
done
|
||||
|
||||
|
|
|
@ -544,7 +544,7 @@ func (g *Generator) buildStruct(obj types.Object, st *types.Struct) (string, err
|
|||
tag := reflect.StructTag(st.Tag(i))
|
||||
// Adding a json struct tag causes the json package to consider
|
||||
// the field unembedded.
|
||||
if field.Embedded() && tag.Get("json") == "" && field.Pkg().Name() == "codersdk" {
|
||||
if field.Embedded() && tag.Get("json") == "" {
|
||||
extendedFields[i] = true
|
||||
extends = append(extends, field.Name())
|
||||
}
|
||||
|
@ -814,11 +814,17 @@ func (g *Generator) typescriptType(ty types.Type) (TypescriptType, error) {
|
|||
return TypescriptType{}, xerrors.Errorf("array: %w", err)
|
||||
}
|
||||
genValue := ""
|
||||
|
||||
// Always wrap in parentheses for proper scoped types.
|
||||
// Running prettier on this output will remove redundant parenthesis,
|
||||
// so this makes our decision-making easier.
|
||||
// The example that breaks without this is:
|
||||
// readonly readonly string[][]
|
||||
if underlying.GenericValue != "" {
|
||||
genValue = underlying.GenericValue + "[]"
|
||||
genValue = "(readonly " + underlying.GenericValue + "[])"
|
||||
}
|
||||
return TypescriptType{
|
||||
ValueType: underlying.ValueType + "[]",
|
||||
ValueType: "(readonly " + underlying.ValueType + "[])",
|
||||
GenericValue: genValue,
|
||||
AboveTypeLine: underlying.AboveTypeLine,
|
||||
GenericTypes: underlying.GenericTypes,
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
@ -15,6 +16,9 @@ import (
|
|||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// updateGoldenFiles is a flag that can be set to update golden files.
|
||||
var updateGoldenFiles = flag.Bool("update", false, "Update golden files")
|
||||
|
||||
func TestGeneration(t *testing.T) {
|
||||
t.Parallel()
|
||||
files, err := os.ReadDir("testdata")
|
||||
|
@ -37,7 +41,13 @@ func TestGeneration(t *testing.T) {
|
|||
require.NoErrorf(t, err, "read file %s", golden)
|
||||
expectedString := strings.TrimSpace(string(expected))
|
||||
output = strings.TrimSpace(output)
|
||||
require.Equal(t, expectedString, output, "matched output")
|
||||
if *updateGoldenFiles {
|
||||
// nolint:gosec
|
||||
err := os.WriteFile(golden, []byte(output), 0o644)
|
||||
require.NoError(t, err, "write golden file")
|
||||
} else {
|
||||
require.Equal(t, expectedString, output, "matched output")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
package codersdk
|
||||
|
||||
type (
|
||||
Enum string
|
||||
Enums []Enum
|
||||
Enum string
|
||||
EnumSliceType []Enum
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// From codersdk/enums.go
|
||||
export type Enums = Enum[]
|
||||
export type EnumSliceType = (readonly Enum[])
|
||||
|
||||
// From codersdk/enums.go
|
||||
export type Enum = "bar" | "baz" | "foo" | "qux"
|
||||
|
|
|
@ -1,22 +1,22 @@
|
|||
package codersdk
|
||||
|
||||
type Foo struct {
|
||||
Bar string `json:"bar"`
|
||||
}
|
||||
|
||||
type Buzz struct {
|
||||
Foo `json:"foo"`
|
||||
Bazz string `json:"bazz"`
|
||||
}
|
||||
|
||||
type Custom interface {
|
||||
Foo | Buzz
|
||||
type Foo struct {
|
||||
Bar string `json:"bar"`
|
||||
}
|
||||
|
||||
type FooBuzz[R Custom] struct {
|
||||
Something []R `json:"something"`
|
||||
}
|
||||
|
||||
type Custom interface {
|
||||
Foo | Buzz
|
||||
}
|
||||
|
||||
// Not yet supported
|
||||
//type FooBuzzMap[R Custom] struct {
|
||||
// Something map[string]R `json:"something"`
|
||||
|
|
|
@ -11,8 +11,8 @@ export interface Foo {
|
|||
|
||||
// From codersdk/genericmap.go
|
||||
export interface FooBuzz<R extends Custom> {
|
||||
readonly something: R[]
|
||||
readonly something: (readonly R[])
|
||||
}
|
||||
|
||||
// From codersdk/genericmap.go
|
||||
export type Custom = Foo | Buzz
|
||||
export type Custom = Foo | Buzz
|
|
@ -33,9 +33,9 @@ export interface Static {
|
|||
}
|
||||
|
||||
// From codersdk/generics.go
|
||||
export type Custom = string | boolean | number | string[] | null
|
||||
export type Custom = string | boolean | number | (readonly string[]) | null
|
||||
|
||||
// From codersdk/generics.go
|
||||
export type Single = string
|
||||
|
||||
export type comparable = boolean | number | string | any
|
||||
export type comparable = boolean | number | string | any
|
|
@ -0,0 +1,10 @@
|
|||
package codersdk
|
||||
|
||||
type Bar struct {
|
||||
Bar string
|
||||
}
|
||||
|
||||
type Foo[R any] struct {
|
||||
Slice []R
|
||||
TwoD [][]R
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
// From codersdk/genericslice.go
|
||||
export interface Bar {
|
||||
readonly Bar: string
|
||||
}
|
||||
|
||||
// From codersdk/genericslice.go
|
||||
export interface Foo<R extends any> {
|
||||
readonly Slice: (readonly R[])
|
||||
readonly TwoD: (readonly (readonly R[])[])
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
import type { Page } from "@playwright/test";
|
||||
import { expect } from "@playwright/test";
|
||||
import * as API from "api/api";
|
||||
import { coderPort } from "./constants";
|
||||
import { findSessionToken, randomName } from "./helpers";
|
||||
|
@ -47,3 +48,68 @@ export const createGroup = async (orgId: string) => {
|
|||
});
|
||||
return group;
|
||||
};
|
||||
|
||||
export async function verifyConfigFlag(
|
||||
page: Page,
|
||||
config: API.DeploymentConfig,
|
||||
flag: string,
|
||||
) {
|
||||
const opt = config.options.find((option) => option.flag === flag);
|
||||
if (opt === undefined) {
|
||||
// must be undefined as `false` is expected
|
||||
throw new Error(`Option with env ${flag} has undefined value.`);
|
||||
}
|
||||
|
||||
// Map option type to test class name.
|
||||
let type: string;
|
||||
let value = opt.value;
|
||||
|
||||
if (typeof value === "boolean") {
|
||||
// Boolean options map to string (Enabled/Disabled).
|
||||
type = value ? "option-enabled" : "option-disabled";
|
||||
value = value ? "Enabled" : "Disabled";
|
||||
} else if (typeof value === "number") {
|
||||
type = "option-value-number";
|
||||
value = String(value);
|
||||
} else if (!value || value.length === 0) {
|
||||
type = "option-value-empty";
|
||||
} else if (typeof value === "string") {
|
||||
type = "option-value-string";
|
||||
} else if (typeof value === "object") {
|
||||
type = "option-array";
|
||||
} else {
|
||||
type = "option-value-json";
|
||||
}
|
||||
|
||||
// Special cases
|
||||
if (opt.flag === "strict-transport-security" && opt.value === 0) {
|
||||
type = "option-value-string";
|
||||
value = "Disabled"; // Display "Disabled" instead of zero seconds.
|
||||
}
|
||||
|
||||
const configOption = page.locator(
|
||||
`div.options-table .option-${flag} .${type}`,
|
||||
);
|
||||
|
||||
// Verify array of options with green marks.
|
||||
if (typeof value === "object" && !Array.isArray(value)) {
|
||||
Object.entries(value)
|
||||
.sort((a, b) => a[0].localeCompare(b[0]))
|
||||
.map(async ([item]) => {
|
||||
await expect(
|
||||
configOption.locator(`.option-array-item-${item}.option-enabled`, {
|
||||
hasText: item,
|
||||
}),
|
||||
).toBeVisible();
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Verify array of options with simmple dots
|
||||
if (Array.isArray(value)) {
|
||||
for (const item of value) {
|
||||
await expect(configOption.locator("li", { hasText: item })).toBeVisible();
|
||||
}
|
||||
return;
|
||||
}
|
||||
await expect(configOption).toHaveText(String(value));
|
||||
}
|
||||
|
|
|
@ -111,6 +111,15 @@ export default defineConfig({
|
|||
),
|
||||
CODER_PPROF_ADDRESS: "127.0.0.1:" + coderdPProfPort,
|
||||
CODER_EXPERIMENTS: e2eFakeExperiment1 + "," + e2eFakeExperiment2,
|
||||
|
||||
// Tests for Deployment / User Authentication / OIDC
|
||||
CODER_OIDC_ISSUER_URL: "https://accounts.google.com",
|
||||
CODER_OIDC_EMAIL_DOMAIN: "coder.com",
|
||||
CODER_OIDC_CLIENT_ID: "1234567890", // FIXME: https://github.com/coder/coder/issues/12585
|
||||
CODER_OIDC_CLIENT_SECRET: "1234567890Secret",
|
||||
CODER_OIDC_ALLOW_SIGNUPS: "false",
|
||||
CODER_OIDC_SIGN_IN_TEXT: "Hello",
|
||||
CODER_OIDC_ICON_URL: "/icon/google.svg",
|
||||
},
|
||||
reuseExistingServer: false,
|
||||
},
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
import { test } from "@playwright/test";
|
||||
import { getDeploymentConfig } from "api/api";
|
||||
import { setupApiCalls, verifyConfigFlag } from "../../api";
|
||||
|
||||
test("enabled security settings", async ({ page }) => {
|
||||
await setupApiCalls(page);
|
||||
const config = await getDeploymentConfig();
|
||||
|
||||
await page.goto("/deployment/security", { waitUntil: "domcontentloaded" });
|
||||
|
||||
const flags = [
|
||||
"ssh-keygen-algorithm",
|
||||
"secure-auth-cookie",
|
||||
"disable-owner-workspace-access",
|
||||
|
||||
"tls-redirect-http-to-https",
|
||||
"strict-transport-security",
|
||||
"tls-address",
|
||||
"tls-allow-insecure-ciphers",
|
||||
"tls-client-auth",
|
||||
"tls-enable",
|
||||
"tls-min-version",
|
||||
];
|
||||
|
||||
for (const flag of flags) {
|
||||
await verifyConfigFlag(page, config, flag);
|
||||
}
|
||||
});
|
|
@ -0,0 +1,33 @@
|
|||
import { test } from "@playwright/test";
|
||||
import { getDeploymentConfig } from "api/api";
|
||||
import { setupApiCalls, verifyConfigFlag } from "../../api";
|
||||
|
||||
test("login with OIDC", async ({ page }) => {
|
||||
await setupApiCalls(page);
|
||||
const config = await getDeploymentConfig();
|
||||
|
||||
await page.goto("/deployment/userauth", { waitUntil: "domcontentloaded" });
|
||||
|
||||
const flags = [
|
||||
"oidc-group-auto-create",
|
||||
"oidc-allow-signups",
|
||||
"oidc-auth-url-params",
|
||||
"oidc-client-id",
|
||||
"oidc-email-domain",
|
||||
"oidc-email-field",
|
||||
"oidc-group-mapping",
|
||||
"oidc-ignore-email-verified",
|
||||
"oidc-ignore-userinfo",
|
||||
"oidc-issuer-url",
|
||||
"oidc-group-regex-filter",
|
||||
"oidc-scopes",
|
||||
"oidc-user-role-mapping",
|
||||
"oidc-username-field",
|
||||
"oidc-sign-in-text",
|
||||
"oidc-icon-url",
|
||||
];
|
||||
|
||||
for (const flag of flags) {
|
||||
await verifyConfigFlag(page, config, flag);
|
||||
}
|
||||
});
|
|
@ -4,8 +4,8 @@
|
|||
|
||||
// From codersdk/templates.go
|
||||
export interface ACLAvailable {
|
||||
readonly users: ReducedUser[];
|
||||
readonly groups: Group[];
|
||||
readonly users: readonly ReducedUser[];
|
||||
readonly groups: readonly Group[];
|
||||
}
|
||||
|
||||
// From codersdk/apikey.go
|
||||
|
@ -49,7 +49,7 @@ export interface AppearanceConfig {
|
|||
readonly application_name: string;
|
||||
readonly logo_url: string;
|
||||
readonly service_banner: ServiceBannerConfig;
|
||||
readonly support_links?: LinkConfig[];
|
||||
readonly support_links?: readonly LinkConfig[];
|
||||
}
|
||||
|
||||
// From codersdk/templates.go
|
||||
|
@ -60,7 +60,7 @@ export interface ArchiveTemplateVersionsRequest {
|
|||
// From codersdk/templates.go
|
||||
export interface ArchiveTemplateVersionsResponse {
|
||||
readonly template_id: string;
|
||||
readonly archived_ids: string[];
|
||||
readonly archived_ids: readonly string[];
|
||||
}
|
||||
|
||||
// From codersdk/roles.go
|
||||
|
@ -108,7 +108,7 @@ export interface AuditLog {
|
|||
|
||||
// From codersdk/audit.go
|
||||
export interface AuditLogResponse {
|
||||
readonly audit_logs: AuditLog[];
|
||||
readonly audit_logs: readonly AuditLog[];
|
||||
readonly count: number;
|
||||
}
|
||||
|
||||
|
@ -153,7 +153,7 @@ export type AuthorizationResponse = Record<string, boolean>;
|
|||
|
||||
// From codersdk/deployment.go
|
||||
export interface AvailableExperiments {
|
||||
readonly safe: Experiment[];
|
||||
readonly safe: readonly Experiment[];
|
||||
}
|
||||
|
||||
// From codersdk/deployment.go
|
||||
|
@ -241,8 +241,8 @@ export interface CreateTemplateRequest {
|
|||
// From codersdk/templateversions.go
|
||||
export interface CreateTemplateVersionDryRunRequest {
|
||||
readonly workspace_name: string;
|
||||
readonly rich_parameter_values: WorkspaceBuildParameter[];
|
||||
readonly user_variable_values?: VariableValue[];
|
||||
readonly rich_parameter_values: readonly WorkspaceBuildParameter[];
|
||||
readonly user_variable_values?: readonly VariableValue[];
|
||||
}
|
||||
|
||||
// From codersdk/organizations.go
|
||||
|
@ -255,7 +255,7 @@ export interface CreateTemplateVersionRequest {
|
|||
readonly example_id?: string;
|
||||
readonly provisioner: ProvisionerType;
|
||||
readonly tags: Record<string, string>;
|
||||
readonly user_variable_values?: VariableValue[];
|
||||
readonly user_variable_values?: readonly VariableValue[];
|
||||
}
|
||||
|
||||
// From codersdk/audit.go
|
||||
|
@ -292,7 +292,7 @@ export interface CreateWorkspaceBuildRequest {
|
|||
readonly dry_run?: boolean;
|
||||
readonly state?: string;
|
||||
readonly orphan?: boolean;
|
||||
readonly rich_parameter_values?: WorkspaceBuildParameter[];
|
||||
readonly rich_parameter_values?: readonly WorkspaceBuildParameter[];
|
||||
readonly log_level?: ProvisionerLogLevel;
|
||||
}
|
||||
|
||||
|
@ -310,7 +310,7 @@ export interface CreateWorkspaceRequest {
|
|||
readonly name: string;
|
||||
readonly autostart_schedule?: string;
|
||||
readonly ttl_ms?: number;
|
||||
readonly rich_parameter_values?: WorkspaceBuildParameter[];
|
||||
readonly rich_parameter_values?: readonly WorkspaceBuildParameter[];
|
||||
readonly automatic_updates?: AutomaticUpdates;
|
||||
}
|
||||
|
||||
|
@ -327,7 +327,7 @@ export interface DAURequest {
|
|||
|
||||
// From codersdk/deployment.go
|
||||
export interface DAUsResponse {
|
||||
readonly entries: DAUEntry[];
|
||||
readonly entries: readonly DAUEntry[];
|
||||
readonly tz_hour_offset: number;
|
||||
}
|
||||
|
||||
|
@ -434,7 +434,7 @@ export interface DeploymentValues {
|
|||
readonly session_lifetime?: SessionLifetime;
|
||||
readonly disable_password_auth?: boolean;
|
||||
readonly support?: SupportConfig;
|
||||
readonly external_auth?: ExternalAuthConfig[];
|
||||
readonly external_auth?: readonly ExternalAuthConfig[];
|
||||
readonly config_ssh?: SSHConfig;
|
||||
readonly wgtunnel_host?: string;
|
||||
readonly disable_owner_workspace_exec?: boolean;
|
||||
|
@ -453,8 +453,8 @@ export interface DeploymentValues {
|
|||
// From codersdk/deployment.go
|
||||
export interface Entitlements {
|
||||
readonly features: Record<FeatureName, Feature>;
|
||||
readonly warnings: string[];
|
||||
readonly errors: string[];
|
||||
readonly warnings: readonly string[];
|
||||
readonly errors: readonly string[];
|
||||
readonly has_license: boolean;
|
||||
readonly trial: boolean;
|
||||
readonly require_telemetry: boolean;
|
||||
|
@ -462,7 +462,7 @@ export interface Entitlements {
|
|||
}
|
||||
|
||||
// From codersdk/deployment.go
|
||||
export type Experiments = Experiment[];
|
||||
export type Experiments = readonly Experiment[];
|
||||
|
||||
// From codersdk/externalauth.go
|
||||
export interface ExternalAuth {
|
||||
|
@ -471,7 +471,7 @@ export interface ExternalAuth {
|
|||
readonly display_name: string;
|
||||
readonly user?: ExternalAuthUser;
|
||||
readonly app_installable: boolean;
|
||||
readonly installations: ExternalAuthAppInstallation[];
|
||||
readonly installations: readonly ExternalAuthAppInstallation[];
|
||||
readonly app_install_url: string;
|
||||
}
|
||||
|
||||
|
@ -493,8 +493,8 @@ export interface ExternalAuthConfig {
|
|||
readonly app_install_url: string;
|
||||
readonly app_installations_url: string;
|
||||
readonly no_refresh: boolean;
|
||||
readonly scopes: string[];
|
||||
readonly extra_token_keys: string[];
|
||||
readonly scopes: readonly string[];
|
||||
readonly extra_token_keys: readonly string[];
|
||||
readonly device_flow: boolean;
|
||||
readonly device_code_url: string;
|
||||
readonly regex: string;
|
||||
|
@ -561,7 +561,7 @@ export interface GenerateAPIKeyResponse {
|
|||
|
||||
// From codersdk/users.go
|
||||
export interface GetUsersResponse {
|
||||
readonly users: User[];
|
||||
readonly users: readonly User[];
|
||||
readonly count: number;
|
||||
}
|
||||
|
||||
|
@ -579,7 +579,7 @@ export interface Group {
|
|||
readonly name: string;
|
||||
readonly display_name: string;
|
||||
readonly organization_id: string;
|
||||
readonly members: ReducedUser[];
|
||||
readonly members: readonly ReducedUser[];
|
||||
readonly avatar_url: string;
|
||||
readonly quota_allowance: number;
|
||||
readonly source: GroupSource;
|
||||
|
@ -638,8 +638,8 @@ export interface LinkConfig {
|
|||
|
||||
// From codersdk/externalauth.go
|
||||
export interface ListUserExternalAuthResponse {
|
||||
readonly providers: ExternalAuthLinkProvider[];
|
||||
readonly links: ExternalAuthLink[];
|
||||
readonly providers: readonly ExternalAuthLinkProvider[];
|
||||
readonly links: readonly ExternalAuthLink[];
|
||||
}
|
||||
|
||||
// From codersdk/deployment.go
|
||||
|
@ -753,7 +753,7 @@ export interface OIDCConfig {
|
|||
readonly groups_field: string;
|
||||
readonly group_mapping: Record<string, string>;
|
||||
readonly user_role_field: string;
|
||||
readonly user_role_mapping: Record<string, string[]>;
|
||||
readonly user_role_mapping: Record<string, readonly string[]>;
|
||||
readonly user_roles_default: string[];
|
||||
readonly sign_in_text: string;
|
||||
readonly icon_url: string;
|
||||
|
@ -775,7 +775,7 @@ export interface OrganizationMember {
|
|||
readonly organization_id: string;
|
||||
readonly created_at: string;
|
||||
readonly updated_at: string;
|
||||
readonly roles: Role[];
|
||||
readonly roles: readonly Role[];
|
||||
}
|
||||
|
||||
// From codersdk/pagination.go
|
||||
|
@ -787,8 +787,8 @@ export interface Pagination {
|
|||
|
||||
// From codersdk/groups.go
|
||||
export interface PatchGroupRequest {
|
||||
readonly add_users: string[];
|
||||
readonly remove_users: string[];
|
||||
readonly add_users: readonly string[];
|
||||
readonly remove_users: readonly string[];
|
||||
readonly name: string;
|
||||
readonly display_name?: string;
|
||||
readonly avatar_url?: string;
|
||||
|
@ -850,7 +850,7 @@ export interface ProvisionerDaemon {
|
|||
readonly name: string;
|
||||
readonly version: string;
|
||||
readonly api_version: string;
|
||||
readonly provisioners: ProvisionerType[];
|
||||
readonly provisioners: readonly ProvisionerType[];
|
||||
readonly tags: Record<string, string>;
|
||||
}
|
||||
|
||||
|
@ -883,8 +883,8 @@ export interface ProvisionerJobLog {
|
|||
|
||||
// From codersdk/workspaceproxy.go
|
||||
export interface ProxyHealthReport {
|
||||
readonly errors: string[];
|
||||
readonly warnings: string[];
|
||||
readonly errors: readonly string[];
|
||||
readonly warnings: readonly string[];
|
||||
}
|
||||
|
||||
// From codersdk/workspaces.go
|
||||
|
@ -929,7 +929,7 @@ export interface Region {
|
|||
|
||||
// From codersdk/workspaceproxy.go
|
||||
export interface RegionsResponse<R extends RegionTypes> {
|
||||
readonly regions: R[];
|
||||
readonly regions: readonly R[];
|
||||
}
|
||||
|
||||
// From codersdk/replicas.go
|
||||
|
@ -952,7 +952,7 @@ export interface ResolveAutostartResponse {
|
|||
export interface Response {
|
||||
readonly message: string;
|
||||
readonly detail?: string;
|
||||
readonly validations?: ValidationError[];
|
||||
readonly validations?: readonly ValidationError[];
|
||||
}
|
||||
|
||||
// From codersdk/roles.go
|
||||
|
@ -1005,7 +1005,7 @@ export interface SessionLifetime {
|
|||
|
||||
// From codersdk/deployment.go
|
||||
export interface SupportConfig {
|
||||
readonly links: LinkConfig[];
|
||||
readonly links: readonly LinkConfig[];
|
||||
}
|
||||
|
||||
// From codersdk/deployment.go
|
||||
|
@ -1070,13 +1070,13 @@ export interface Template {
|
|||
|
||||
// From codersdk/templates.go
|
||||
export interface TemplateACL {
|
||||
readonly users: TemplateUser[];
|
||||
readonly group: TemplateGroup[];
|
||||
readonly users: readonly TemplateUser[];
|
||||
readonly group: readonly TemplateGroup[];
|
||||
}
|
||||
|
||||
// From codersdk/insights.go
|
||||
export interface TemplateAppUsage {
|
||||
readonly template_ids: string[];
|
||||
readonly template_ids: readonly string[];
|
||||
readonly type: TemplateAppsType;
|
||||
readonly display_name: string;
|
||||
readonly slug: string;
|
||||
|
@ -1086,12 +1086,12 @@ export interface TemplateAppUsage {
|
|||
|
||||
// From codersdk/templates.go
|
||||
export interface TemplateAutostartRequirement {
|
||||
readonly days_of_week: string[];
|
||||
readonly days_of_week: readonly string[];
|
||||
}
|
||||
|
||||
// From codersdk/templates.go
|
||||
export interface TemplateAutostopRequirement {
|
||||
readonly days_of_week: string[];
|
||||
readonly days_of_week: readonly string[];
|
||||
readonly weeks: number;
|
||||
}
|
||||
|
||||
|
@ -1108,7 +1108,7 @@ export interface TemplateExample {
|
|||
readonly name: string;
|
||||
readonly description: string;
|
||||
readonly icon: string;
|
||||
readonly tags: string[];
|
||||
readonly tags: readonly string[];
|
||||
readonly markdown: string;
|
||||
}
|
||||
|
||||
|
@ -1121,7 +1121,7 @@ export interface TemplateGroup extends Group {
|
|||
export interface TemplateInsightsIntervalReport {
|
||||
readonly start_time: string;
|
||||
readonly end_time: string;
|
||||
readonly template_ids: string[];
|
||||
readonly template_ids: readonly string[];
|
||||
readonly interval: InsightsReportInterval;
|
||||
readonly active_users: number;
|
||||
}
|
||||
|
@ -1130,36 +1130,36 @@ export interface TemplateInsightsIntervalReport {
|
|||
export interface TemplateInsightsReport {
|
||||
readonly start_time: string;
|
||||
readonly end_time: string;
|
||||
readonly template_ids: string[];
|
||||
readonly template_ids: readonly string[];
|
||||
readonly active_users: number;
|
||||
readonly apps_usage: TemplateAppUsage[];
|
||||
readonly parameters_usage: TemplateParameterUsage[];
|
||||
readonly apps_usage: readonly TemplateAppUsage[];
|
||||
readonly parameters_usage: readonly TemplateParameterUsage[];
|
||||
}
|
||||
|
||||
// From codersdk/insights.go
|
||||
export interface TemplateInsightsRequest {
|
||||
readonly start_time: string;
|
||||
readonly end_time: string;
|
||||
readonly template_ids: string[];
|
||||
readonly template_ids: readonly string[];
|
||||
readonly interval: InsightsReportInterval;
|
||||
readonly sections: TemplateInsightsSection[];
|
||||
readonly sections: readonly TemplateInsightsSection[];
|
||||
}
|
||||
|
||||
// From codersdk/insights.go
|
||||
export interface TemplateInsightsResponse {
|
||||
readonly report?: TemplateInsightsReport;
|
||||
readonly interval_reports?: TemplateInsightsIntervalReport[];
|
||||
readonly interval_reports?: readonly TemplateInsightsIntervalReport[];
|
||||
}
|
||||
|
||||
// From codersdk/insights.go
|
||||
export interface TemplateParameterUsage {
|
||||
readonly template_ids: string[];
|
||||
readonly template_ids: readonly string[];
|
||||
readonly display_name: string;
|
||||
readonly name: string;
|
||||
readonly type: string;
|
||||
readonly description: string;
|
||||
readonly options?: TemplateVersionParameterOption[];
|
||||
readonly values: TemplateParameterValue[];
|
||||
readonly options?: readonly TemplateVersionParameterOption[];
|
||||
readonly values: readonly TemplateParameterValue[];
|
||||
}
|
||||
|
||||
// From codersdk/insights.go
|
||||
|
@ -1186,7 +1186,7 @@ export interface TemplateVersion {
|
|||
readonly readme: string;
|
||||
readonly created_by: MinimalUser;
|
||||
readonly archived: boolean;
|
||||
readonly warnings?: TemplateVersionWarning[];
|
||||
readonly warnings?: readonly TemplateVersionWarning[];
|
||||
}
|
||||
|
||||
// From codersdk/templateversions.go
|
||||
|
@ -1210,7 +1210,7 @@ export interface TemplateVersionParameter {
|
|||
readonly mutable: boolean;
|
||||
readonly default_value: string;
|
||||
readonly icon: string;
|
||||
readonly options: TemplateVersionParameterOption[];
|
||||
readonly options: readonly TemplateVersionParameterOption[];
|
||||
readonly validation_error?: string;
|
||||
readonly validation_regex?: string;
|
||||
readonly validation_min?: number;
|
||||
|
@ -1290,7 +1290,7 @@ export interface UpdateCheckResponse {
|
|||
|
||||
// From codersdk/users.go
|
||||
export interface UpdateRoles {
|
||||
readonly roles: string[];
|
||||
readonly roles: readonly string[];
|
||||
}
|
||||
|
||||
// From codersdk/templates.go
|
||||
|
@ -1391,13 +1391,13 @@ export interface UpsertWorkspaceAgentPortShareRequest {
|
|||
|
||||
// From codersdk/users.go
|
||||
export interface User extends ReducedUser {
|
||||
readonly organization_ids: string[];
|
||||
readonly roles: Role[];
|
||||
readonly organization_ids: readonly string[];
|
||||
readonly roles: readonly Role[];
|
||||
}
|
||||
|
||||
// From codersdk/insights.go
|
||||
export interface UserActivity {
|
||||
readonly template_ids: string[];
|
||||
readonly template_ids: readonly string[];
|
||||
readonly user_id: string;
|
||||
readonly username: string;
|
||||
readonly avatar_url: string;
|
||||
|
@ -1408,15 +1408,15 @@ export interface UserActivity {
|
|||
export interface UserActivityInsightsReport {
|
||||
readonly start_time: string;
|
||||
readonly end_time: string;
|
||||
readonly template_ids: string[];
|
||||
readonly users: UserActivity[];
|
||||
readonly template_ids: readonly string[];
|
||||
readonly users: readonly UserActivity[];
|
||||
}
|
||||
|
||||
// From codersdk/insights.go
|
||||
export interface UserActivityInsightsRequest {
|
||||
readonly start_time: string;
|
||||
readonly end_time: string;
|
||||
readonly template_ids: string[];
|
||||
readonly template_ids: readonly string[];
|
||||
}
|
||||
|
||||
// From codersdk/insights.go
|
||||
|
@ -1426,7 +1426,7 @@ export interface UserActivityInsightsResponse {
|
|||
|
||||
// From codersdk/insights.go
|
||||
export interface UserLatency {
|
||||
readonly template_ids: string[];
|
||||
readonly template_ids: readonly string[];
|
||||
readonly user_id: string;
|
||||
readonly username: string;
|
||||
readonly avatar_url: string;
|
||||
|
@ -1437,15 +1437,15 @@ export interface UserLatency {
|
|||
export interface UserLatencyInsightsReport {
|
||||
readonly start_time: string;
|
||||
readonly end_time: string;
|
||||
readonly template_ids: string[];
|
||||
readonly users: UserLatency[];
|
||||
readonly template_ids: readonly string[];
|
||||
readonly users: readonly UserLatency[];
|
||||
}
|
||||
|
||||
// From codersdk/insights.go
|
||||
export interface UserLatencyInsightsRequest {
|
||||
readonly start_time: string;
|
||||
readonly end_time: string;
|
||||
readonly template_ids: string[];
|
||||
readonly template_ids: readonly string[];
|
||||
}
|
||||
|
||||
// From codersdk/insights.go
|
||||
|
@ -1482,8 +1482,8 @@ export interface UserQuietHoursScheduleResponse {
|
|||
|
||||
// From codersdk/users.go
|
||||
export interface UserRoles {
|
||||
readonly roles: string[];
|
||||
readonly organization_roles: Record<string, string[]>;
|
||||
readonly roles: readonly string[];
|
||||
readonly organization_roles: Record<string, readonly string[]>;
|
||||
}
|
||||
|
||||
// From codersdk/users.go
|
||||
|
@ -1557,15 +1557,15 @@ export interface WorkspaceAgent {
|
|||
readonly expanded_directory?: string;
|
||||
readonly version: string;
|
||||
readonly api_version: string;
|
||||
readonly apps: WorkspaceApp[];
|
||||
readonly apps: readonly WorkspaceApp[];
|
||||
readonly latency?: Record<string, DERPRegion>;
|
||||
readonly connection_timeout_seconds: number;
|
||||
readonly troubleshooting_url: string;
|
||||
readonly subsystems: AgentSubsystem[];
|
||||
readonly subsystems: readonly AgentSubsystem[];
|
||||
readonly health: WorkspaceAgentHealth;
|
||||
readonly display_apps: DisplayApp[];
|
||||
readonly log_sources: WorkspaceAgentLogSource[];
|
||||
readonly scripts: WorkspaceAgentScript[];
|
||||
readonly display_apps: readonly DisplayApp[];
|
||||
readonly log_sources: readonly WorkspaceAgentLogSource[];
|
||||
readonly scripts: readonly WorkspaceAgentScript[];
|
||||
readonly startup_script_behavior: WorkspaceAgentStartupScriptBehavior;
|
||||
}
|
||||
|
||||
|
@ -1584,7 +1584,7 @@ export interface WorkspaceAgentListeningPort {
|
|||
|
||||
// From codersdk/workspaceagents.go
|
||||
export interface WorkspaceAgentListeningPortsResponse {
|
||||
readonly ports: WorkspaceAgentListeningPort[];
|
||||
readonly ports: readonly WorkspaceAgentListeningPort[];
|
||||
}
|
||||
|
||||
// From codersdk/workspaceagents.go
|
||||
|
@ -1639,7 +1639,7 @@ export interface WorkspaceAgentPortShare {
|
|||
|
||||
// From codersdk/workspaceagentportshare.go
|
||||
export interface WorkspaceAgentPortShares {
|
||||
readonly shares: WorkspaceAgentPortShare[];
|
||||
readonly shares: readonly WorkspaceAgentPortShare[];
|
||||
}
|
||||
|
||||
// From codersdk/workspaceagents.go
|
||||
|
@ -1688,7 +1688,7 @@ export interface WorkspaceBuild {
|
|||
readonly initiator_name: string;
|
||||
readonly job: ProvisionerJob;
|
||||
readonly reason: BuildReason;
|
||||
readonly resources: WorkspaceResource[];
|
||||
readonly resources: readonly WorkspaceResource[];
|
||||
readonly deadline?: string;
|
||||
readonly max_deadline?: string;
|
||||
readonly status: WorkspaceStatus;
|
||||
|
@ -1732,7 +1732,7 @@ export interface WorkspaceFilter {
|
|||
// From codersdk/workspaces.go
|
||||
export interface WorkspaceHealth {
|
||||
readonly healthy: boolean;
|
||||
readonly failing_agents: string[];
|
||||
readonly failing_agents: readonly string[];
|
||||
}
|
||||
|
||||
// From codersdk/workspaces.go
|
||||
|
@ -1780,8 +1780,8 @@ export interface WorkspaceResource {
|
|||
readonly name: string;
|
||||
readonly hide: boolean;
|
||||
readonly icon: string;
|
||||
readonly agents?: WorkspaceAgent[];
|
||||
readonly metadata?: WorkspaceResourceMetadata[];
|
||||
readonly agents?: readonly WorkspaceAgent[];
|
||||
readonly metadata?: readonly WorkspaceResourceMetadata[];
|
||||
readonly daily_cost: number;
|
||||
}
|
||||
|
||||
|
@ -1799,7 +1799,7 @@ export interface WorkspacesRequest extends Pagination {
|
|||
|
||||
// From codersdk/workspaces.go
|
||||
export interface WorkspacesResponse {
|
||||
readonly workspaces: Workspace[];
|
||||
readonly workspaces: readonly Workspace[];
|
||||
readonly count: number;
|
||||
}
|
||||
|
||||
|
@ -2282,38 +2282,39 @@ export type RegionTypes = Region | WorkspaceProxy;
|
|||
// The code below is generated from codersdk/healthsdk.
|
||||
|
||||
// From healthsdk/healthsdk.go
|
||||
export interface AccessURLReport {
|
||||
export interface AccessURLReport extends BaseReport {
|
||||
readonly healthy: boolean;
|
||||
readonly severity: HealthSeverity;
|
||||
readonly warnings: HealthMessage[];
|
||||
readonly dismissed: boolean;
|
||||
readonly access_url: string;
|
||||
readonly reachable: boolean;
|
||||
readonly status_code: number;
|
||||
readonly healthz_response: string;
|
||||
readonly error?: string;
|
||||
}
|
||||
|
||||
// From healthsdk/healthsdk.go
|
||||
export interface DERPHealthReport {
|
||||
readonly healthy: boolean;
|
||||
export interface BaseReport {
|
||||
readonly error?: string;
|
||||
readonly severity: HealthSeverity;
|
||||
readonly warnings: HealthMessage[];
|
||||
readonly warnings: readonly HealthMessage[];
|
||||
readonly dismissed: boolean;
|
||||
}
|
||||
|
||||
// From healthsdk/healthsdk.go
|
||||
export interface DERPHealthReport extends BaseReport {
|
||||
readonly healthy: boolean;
|
||||
readonly regions: Record<number, DERPRegionReport>;
|
||||
// Named type "tailscale.com/net/netcheck.Report" unknown, using "any"
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- External type
|
||||
readonly netcheck?: any;
|
||||
readonly netcheck_err?: string;
|
||||
readonly netcheck_logs: string[];
|
||||
readonly error?: string;
|
||||
readonly netcheck_logs: readonly string[];
|
||||
}
|
||||
|
||||
// From healthsdk/healthsdk.go
|
||||
export interface DERPNodeReport {
|
||||
readonly healthy: boolean;
|
||||
readonly severity: HealthSeverity;
|
||||
readonly warnings: HealthMessage[];
|
||||
readonly warnings: readonly HealthMessage[];
|
||||
readonly error?: string;
|
||||
// Named type "tailscale.com/tailcfg.DERPNode" unknown, using "any"
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- External type
|
||||
readonly node?: any;
|
||||
|
@ -2324,9 +2325,8 @@ export interface DERPNodeReport {
|
|||
readonly round_trip_ping: string;
|
||||
readonly round_trip_ping_ms: number;
|
||||
readonly uses_websocket: boolean;
|
||||
readonly client_logs: string[][];
|
||||
readonly client_errs: string[][];
|
||||
readonly error?: string;
|
||||
readonly client_logs: readonly (readonly string[])[];
|
||||
readonly client_errs: readonly (readonly string[])[];
|
||||
readonly stun: STUNReport;
|
||||
}
|
||||
|
||||
|
@ -2334,30 +2334,26 @@ export interface DERPNodeReport {
|
|||
export interface DERPRegionReport {
|
||||
readonly healthy: boolean;
|
||||
readonly severity: HealthSeverity;
|
||||
readonly warnings: HealthMessage[];
|
||||
readonly warnings: readonly HealthMessage[];
|
||||
readonly error?: string;
|
||||
// Named type "tailscale.com/tailcfg.DERPRegion" unknown, using "any"
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- External type
|
||||
readonly region?: any;
|
||||
readonly node_reports: DERPNodeReport[];
|
||||
readonly error?: string;
|
||||
readonly node_reports: readonly DERPNodeReport[];
|
||||
}
|
||||
|
||||
// From healthsdk/healthsdk.go
|
||||
export interface DatabaseReport {
|
||||
export interface DatabaseReport extends BaseReport {
|
||||
readonly healthy: boolean;
|
||||
readonly severity: HealthSeverity;
|
||||
readonly warnings: HealthMessage[];
|
||||
readonly dismissed: boolean;
|
||||
readonly reachable: boolean;
|
||||
readonly latency: string;
|
||||
readonly latency_ms: number;
|
||||
readonly threshold_ms: number;
|
||||
readonly error?: string;
|
||||
}
|
||||
|
||||
// From healthsdk/healthsdk.go
|
||||
export interface HealthSettings {
|
||||
readonly dismissed_healthchecks: HealthSection[];
|
||||
readonly dismissed_healthchecks: readonly HealthSection[];
|
||||
}
|
||||
|
||||
// From healthsdk/healthsdk.go
|
||||
|
@ -2365,7 +2361,7 @@ export interface HealthcheckReport {
|
|||
readonly time: string;
|
||||
readonly healthy: boolean;
|
||||
readonly severity: HealthSeverity;
|
||||
readonly failing_sections: HealthSection[];
|
||||
readonly failing_sections: readonly HealthSection[];
|
||||
readonly derp: DERPHealthReport;
|
||||
readonly access_url: AccessURLReport;
|
||||
readonly websocket: WebsocketReport;
|
||||
|
@ -2376,18 +2372,14 @@ export interface HealthcheckReport {
|
|||
}
|
||||
|
||||
// From healthsdk/healthsdk.go
|
||||
export interface ProvisionerDaemonsReport {
|
||||
readonly severity: HealthSeverity;
|
||||
readonly warnings: HealthMessage[];
|
||||
readonly dismissed: boolean;
|
||||
readonly error?: string;
|
||||
readonly items: ProvisionerDaemonsReportItem[];
|
||||
export interface ProvisionerDaemonsReport extends BaseReport {
|
||||
readonly items: readonly ProvisionerDaemonsReportItem[];
|
||||
}
|
||||
|
||||
// From healthsdk/healthsdk.go
|
||||
export interface ProvisionerDaemonsReportItem {
|
||||
readonly provisioner_daemon: ProvisionerDaemon;
|
||||
readonly warnings: HealthMessage[];
|
||||
readonly warnings: readonly HealthMessage[];
|
||||
}
|
||||
|
||||
// From healthsdk/healthsdk.go
|
||||
|
@ -2399,27 +2391,19 @@ export interface STUNReport {
|
|||
|
||||
// From healthsdk/healthsdk.go
|
||||
export interface UpdateHealthSettings {
|
||||
readonly dismissed_healthchecks: HealthSection[];
|
||||
readonly dismissed_healthchecks: readonly HealthSection[];
|
||||
}
|
||||
|
||||
// From healthsdk/healthsdk.go
|
||||
export interface WebsocketReport {
|
||||
export interface WebsocketReport extends BaseReport {
|
||||
readonly healthy: boolean;
|
||||
readonly severity: HealthSeverity;
|
||||
readonly warnings: string[];
|
||||
readonly dismissed: boolean;
|
||||
readonly body: string;
|
||||
readonly code: number;
|
||||
readonly error?: string;
|
||||
}
|
||||
|
||||
// From healthsdk/healthsdk.go
|
||||
export interface WorkspaceProxyReport {
|
||||
export interface WorkspaceProxyReport extends BaseReport {
|
||||
readonly healthy: boolean;
|
||||
readonly severity: HealthSeverity;
|
||||
readonly warnings: HealthMessage[];
|
||||
readonly dismissed: boolean;
|
||||
readonly error?: string;
|
||||
readonly workspace_proxies: RegionsResponse<WorkspaceProxy>;
|
||||
}
|
||||
|
||||
|
@ -2520,13 +2504,13 @@ export interface SerpentOption {
|
|||
readonly value?: any;
|
||||
readonly annotations?: SerpentAnnotations;
|
||||
readonly group?: SerpentGroup;
|
||||
readonly use_instead?: SerpentOption[];
|
||||
readonly use_instead?: readonly SerpentOption[];
|
||||
readonly hidden?: boolean;
|
||||
readonly value_source?: SerpentValueSource;
|
||||
}
|
||||
|
||||
// From serpent/option.go
|
||||
export type SerpentOptionSet = SerpentOption[];
|
||||
export type SerpentOptionSet = readonly SerpentOption[];
|
||||
|
||||
// From serpent/option.go
|
||||
export type SerpentValueSource = "" | "default" | "env" | "flag" | "yaml";
|
||||
|
|
|
@ -42,7 +42,7 @@ ChartJS.register(
|
|||
const USER_LIMIT_DISPLAY_THRESHOLD = 60;
|
||||
|
||||
export interface ActiveUserChartProps {
|
||||
data: Array<{ date: string; amount: number }>;
|
||||
data: readonly { date: string; amount: number }[];
|
||||
interval: "day" | "week";
|
||||
userLimit: number | undefined;
|
||||
}
|
||||
|
|
|
@ -41,7 +41,11 @@ const styles = {
|
|||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
export const EnabledBadge: FC = () => {
|
||||
return <span css={[styles.badge, styles.enabledBadge]}>Enabled</span>;
|
||||
return (
|
||||
<span css={[styles.badge, styles.enabledBadge]} className="option-enabled">
|
||||
Enabled
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export const EntitledBadge: FC = () => {
|
||||
|
@ -95,6 +99,7 @@ export const DisabledBadge: FC = forwardRef<
|
|||
color: theme.experimental.l1.text,
|
||||
}),
|
||||
]}
|
||||
className="option-disabled"
|
||||
>
|
||||
Disabled
|
||||
</span>
|
||||
|
|
|
@ -4,7 +4,7 @@ import { TimelineDateRow } from "components/Timeline/TimelineDateRow";
|
|||
type GetDateFn<TData> = (data: TData) => Date;
|
||||
|
||||
const groupByDate = <TData,>(
|
||||
items: TData[],
|
||||
items: readonly TData[],
|
||||
getDate: GetDateFn<TData>,
|
||||
): Record<string, TData[]> => {
|
||||
const itemsByDate: Record<string, TData[]> = {};
|
||||
|
@ -23,7 +23,7 @@ const groupByDate = <TData,>(
|
|||
};
|
||||
|
||||
export interface TimelineProps<TData> {
|
||||
items: TData[];
|
||||
items: readonly TData[];
|
||||
getDate: GetDateFn<TData>;
|
||||
row: (item: TData) => JSX.Element;
|
||||
}
|
||||
|
|
|
@ -41,7 +41,7 @@ export interface ProxyContextValue {
|
|||
// WorkspaceProxy[] is returned if the user is an admin. WorkspaceProxy extends Region with
|
||||
// more information about the proxy and the status. More information includes the error message if
|
||||
// the proxy is unhealthy.
|
||||
proxies?: Region[] | WorkspaceProxy[];
|
||||
proxies?: readonly Region[] | readonly WorkspaceProxy[];
|
||||
// isFetched is true when the 'proxies' api call is complete.
|
||||
isFetched: boolean;
|
||||
isLoading: boolean;
|
||||
|
@ -117,7 +117,7 @@ export const ProxyProvider: FC<PropsWithChildren> = ({ children }) => {
|
|||
});
|
||||
|
||||
const { permissions } = useAuthenticated();
|
||||
const query = async (): Promise<Region[]> => {
|
||||
const query = async (): Promise<readonly Region[]> => {
|
||||
const endpoint = permissions.editWorkspaceProxies
|
||||
? getWorkspaceProxies
|
||||
: getWorkspaceProxyRegions;
|
||||
|
@ -218,7 +218,7 @@ export const useProxy = (): ProxyContextValue => {
|
|||
* If not, `primary` is always the best default.
|
||||
*/
|
||||
export const getPreferredProxy = (
|
||||
proxies: Region[],
|
||||
proxies: readonly Region[],
|
||||
selectedProxy?: Region,
|
||||
latencies?: Record<string, ProxyLatencyReport>,
|
||||
autoSelectBasedOnLatency = true,
|
||||
|
@ -245,7 +245,7 @@ export const getPreferredProxy = (
|
|||
};
|
||||
|
||||
const selectByLatency = (
|
||||
proxies: Region[],
|
||||
proxies: readonly Region[],
|
||||
latencies?: Record<string, ProxyLatencyReport>,
|
||||
): Region | undefined => {
|
||||
if (!latencies) {
|
||||
|
|
|
@ -37,7 +37,7 @@ const proxyLatenciesReducer = (
|
|||
};
|
||||
|
||||
export const useProxyLatency = (
|
||||
proxies?: Region[],
|
||||
proxies?: readonly Region[],
|
||||
): {
|
||||
// Refetch can be called to refetch the proxy latencies.
|
||||
// Until the new values are loaded, the old values will still be used.
|
||||
|
@ -265,7 +265,7 @@ const updateStoredLatencies = (action: ProxyLatencyAction): void => {
|
|||
// garbageCollectStoredLatencies will remove any latencies that are older then 1 week or latencies of proxies
|
||||
// that no longer exist. This is intended to keep the size of local storage down.
|
||||
const garbageCollectStoredLatencies = (
|
||||
regions: Region[],
|
||||
regions: readonly Region[],
|
||||
maxStored: number,
|
||||
): void => {
|
||||
const latencies = loadStoredLatencies();
|
||||
|
@ -282,7 +282,7 @@ const garbageCollectStoredLatencies = (
|
|||
|
||||
const cleanupLatencies = (
|
||||
stored: Record<string, ProxyLatencyReport[]>,
|
||||
regions: Region[],
|
||||
regions: readonly Region[],
|
||||
now: Date,
|
||||
maxStored: number,
|
||||
): Record<string, ProxyLatencyReport[]> => {
|
||||
|
|
|
@ -27,8 +27,8 @@ const styles = {
|
|||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
export interface LicenseBannerViewProps {
|
||||
errors: string[];
|
||||
warnings: string[];
|
||||
errors: readonly string[];
|
||||
warnings: readonly string[];
|
||||
}
|
||||
|
||||
export const LicenseBannerView: FC<LicenseBannerViewProps> = ({
|
||||
|
|
|
@ -30,7 +30,7 @@ export interface NavbarViewProps {
|
|||
logo_url?: string;
|
||||
user?: TypesGen.User;
|
||||
buildInfo?: TypesGen.BuildInfoResponse;
|
||||
supportLinks?: TypesGen.LinkConfig[];
|
||||
supportLinks?: readonly TypesGen.LinkConfig[];
|
||||
onSignOut: () => void;
|
||||
canViewAuditLog: boolean;
|
||||
canViewDeployment: boolean;
|
||||
|
@ -342,57 +342,58 @@ const ProxyMenu: FC<ProxyMenuProps> = ({ proxyContextValue }) => {
|
|||
|
||||
<Divider css={{ borderColor: theme.palette.divider }} />
|
||||
|
||||
{proxyContextValue.proxies
|
||||
?.sort((a, b) => {
|
||||
const latencyA = latencies?.[a.id]?.latencyMS ?? Infinity;
|
||||
const latencyB = latencies?.[b.id]?.latencyMS ?? Infinity;
|
||||
return latencyA - latencyB;
|
||||
})
|
||||
.map((proxy) => (
|
||||
<MenuItem
|
||||
key={proxy.id}
|
||||
selected={proxy.id === selectedProxy?.id}
|
||||
css={{ fontSize: 14 }}
|
||||
onClick={() => {
|
||||
if (!proxy.healthy) {
|
||||
displayError("Please select a healthy workspace proxy.");
|
||||
closeMenu();
|
||||
return;
|
||||
}
|
||||
{proxyContextValue.proxies &&
|
||||
[...proxyContextValue.proxies]
|
||||
.sort((a, b) => {
|
||||
const latencyA = latencies?.[a.id]?.latencyMS ?? Infinity;
|
||||
const latencyB = latencies?.[b.id]?.latencyMS ?? Infinity;
|
||||
return latencyA - latencyB;
|
||||
})
|
||||
.map((proxy) => (
|
||||
<MenuItem
|
||||
key={proxy.id}
|
||||
selected={proxy.id === selectedProxy?.id}
|
||||
css={{ fontSize: 14 }}
|
||||
onClick={() => {
|
||||
if (!proxy.healthy) {
|
||||
displayError("Please select a healthy workspace proxy.");
|
||||
closeMenu();
|
||||
return;
|
||||
}
|
||||
|
||||
proxyContextValue.setProxy(proxy);
|
||||
closeMenu();
|
||||
}}
|
||||
>
|
||||
<div
|
||||
css={{
|
||||
display: "flex",
|
||||
gap: 24,
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
proxyContextValue.setProxy(proxy);
|
||||
closeMenu();
|
||||
}}
|
||||
>
|
||||
<div css={{ width: 14, height: 14, lineHeight: 0 }}>
|
||||
<img
|
||||
src={proxy.icon_url}
|
||||
alt=""
|
||||
css={{
|
||||
objectFit: "contain",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
}}
|
||||
<div
|
||||
css={{
|
||||
display: "flex",
|
||||
gap: 24,
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<div css={{ width: 14, height: 14, lineHeight: 0 }}>
|
||||
<img
|
||||
src={proxy.icon_url}
|
||||
alt=""
|
||||
css={{
|
||||
objectFit: "contain",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{proxy.display_name}
|
||||
|
||||
<Latency
|
||||
latency={latencies?.[proxy.id]?.latencyMS}
|
||||
isLoading={proxyLatencyLoading(proxy)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{proxy.display_name}
|
||||
|
||||
<Latency
|
||||
latency={latencies?.[proxy.id]?.latencyMS}
|
||||
isLoading={proxyLatencyLoading(proxy)}
|
||||
/>
|
||||
</div>
|
||||
</MenuItem>
|
||||
))}
|
||||
</MenuItem>
|
||||
))}
|
||||
|
||||
<Divider css={{ borderColor: theme.palette.divider }} />
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ import { UserDropdownContent } from "./UserDropdownContent";
|
|||
export interface UserDropdownProps {
|
||||
user: TypesGen.User;
|
||||
buildInfo?: TypesGen.BuildInfoResponse;
|
||||
supportLinks?: TypesGen.LinkConfig[];
|
||||
supportLinks?: readonly TypesGen.LinkConfig[];
|
||||
onSignOut: () => void;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
|
|
@ -83,7 +83,7 @@ const styles = {
|
|||
export interface UserDropdownContentProps {
|
||||
user: TypesGen.User;
|
||||
buildInfo?: TypesGen.BuildInfoResponse;
|
||||
supportLinks?: TypesGen.LinkConfig[];
|
||||
supportLinks?: readonly TypesGen.LinkConfig[];
|
||||
onSignOut: () => void;
|
||||
}
|
||||
|
||||
|
|
|
@ -20,8 +20,8 @@ type AgentLogsProps = Omit<
|
|||
ComponentProps<typeof List>,
|
||||
"children" | "itemSize" | "itemCount"
|
||||
> & {
|
||||
logs: LineWithID[];
|
||||
sources: WorkspaceAgentLogSource[];
|
||||
logs: readonly LineWithID[];
|
||||
sources: readonly WorkspaceAgentLogSource[];
|
||||
};
|
||||
|
||||
export const AgentLogs = forwardRef<List, AgentLogsProps>(
|
||||
|
|
|
@ -16,7 +16,7 @@ import Stack from "@mui/material/Stack";
|
|||
import TextField from "@mui/material/TextField";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import { type FormikContextType, useFormik } from "formik";
|
||||
import type { FC } from "react";
|
||||
import { useState, type FC } from "react";
|
||||
import { useQuery, useMutation } from "react-query";
|
||||
import * as Yup from "yup";
|
||||
import { getAgentListeningPorts } from "api/api";
|
||||
|
@ -48,7 +48,11 @@ import { type ClassName, useClassName } from "hooks/useClassName";
|
|||
import { useDashboard } from "modules/dashboard/useDashboard";
|
||||
import { docs } from "utils/docs";
|
||||
import { getFormHelpers } from "utils/formUtils";
|
||||
import { portForwardURL } from "utils/portForward";
|
||||
import {
|
||||
getWorkspaceListeningPortsProtocol,
|
||||
portForwardURL,
|
||||
saveWorkspaceListeningPortsProtocol,
|
||||
} from "utils/portForward";
|
||||
|
||||
export interface PortForwardButtonProps {
|
||||
host: string;
|
||||
|
@ -116,7 +120,7 @@ const getValidationSchema = (): Yup.AnyObjectSchema =>
|
|||
});
|
||||
|
||||
interface PortForwardPopoverViewProps extends PortForwardButtonProps {
|
||||
listeningPorts?: WorkspaceAgentListeningPort[];
|
||||
listeningPorts?: readonly WorkspaceAgentListeningPort[];
|
||||
portSharingExperimentEnabled: boolean;
|
||||
portSharingControlsEnabled: boolean;
|
||||
}
|
||||
|
@ -135,6 +139,9 @@ export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
|
|||
portSharingControlsEnabled,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const [listeningPortProtocol, setListeningPortProtocol] = useState(
|
||||
getWorkspaceListeningPortsProtocol(workspaceID),
|
||||
);
|
||||
|
||||
const sharedPortsQuery = useQuery({
|
||||
...workspacePortShares(workspaceID),
|
||||
|
@ -189,15 +196,9 @@ export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
|
|||
(port) => port.agent_name === agent.name,
|
||||
);
|
||||
// we don't want to show listening ports if it's a shared port
|
||||
const filteredListeningPorts = listeningPorts?.filter((port) => {
|
||||
for (let i = 0; i < filteredSharedPorts.length; i++) {
|
||||
if (filteredSharedPorts[i].port === port.port) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
const filteredListeningPorts = (listeningPorts ?? []).filter((port) =>
|
||||
filteredSharedPorts.every((sharedPort) => sharedPort.port !== port.port),
|
||||
);
|
||||
// only disable the form if shared port controls are entitled and the template doesn't allow sharing ports
|
||||
const canSharePorts =
|
||||
portSharingExperimentEnabled &&
|
||||
|
@ -224,95 +225,117 @@ export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
|
|||
overflowY: "auto",
|
||||
}}
|
||||
>
|
||||
<header
|
||||
css={(theme) => ({
|
||||
<Stack
|
||||
direction="column"
|
||||
css={{
|
||||
padding: 20,
|
||||
paddingBottom: 10,
|
||||
position: "sticky",
|
||||
top: 0,
|
||||
background: theme.palette.background.paper,
|
||||
// For some reason the Share button label has a higher z-index than
|
||||
// the header. Probably some tricky stuff from MUI.
|
||||
zIndex: 1,
|
||||
})}
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="start"
|
||||
>
|
||||
<HelpTooltipTitle>Listening ports</HelpTooltipTitle>
|
||||
<HelpTooltipTitle>Listening Ports</HelpTooltipTitle>
|
||||
<HelpTooltipLink
|
||||
href={docs("/networking/port-forwarding#dashboard")}
|
||||
>
|
||||
Learn more
|
||||
</HelpTooltipLink>
|
||||
</Stack>
|
||||
<HelpTooltipText css={{ color: theme.palette.text.secondary }}>
|
||||
{filteredListeningPorts?.length === 0
|
||||
? "No open ports were detected."
|
||||
: "The listening ports are exclusively accessible to you."}
|
||||
</HelpTooltipText>
|
||||
<form
|
||||
css={styles.newPortForm}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.currentTarget);
|
||||
const port = Number(formData.get("portNumber"));
|
||||
const url = portForwardURL(
|
||||
host,
|
||||
port,
|
||||
agent.name,
|
||||
workspaceName,
|
||||
username,
|
||||
);
|
||||
window.open(url, "_blank");
|
||||
}}
|
||||
>
|
||||
<input
|
||||
aria-label="Port number"
|
||||
name="portNumber"
|
||||
type="number"
|
||||
placeholder="Connect to port..."
|
||||
min={9}
|
||||
max={65535}
|
||||
required
|
||||
css={styles.newPortInput}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
size="small"
|
||||
variant="text"
|
||||
<Stack direction="column" gap={1}>
|
||||
<HelpTooltipText css={{ color: theme.palette.text.secondary }}>
|
||||
The listening ports are exclusively accessible to you. Selecting
|
||||
HTTP/S will change the protocol for all listening ports.
|
||||
</HelpTooltipText>
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={2}
|
||||
css={{
|
||||
paddingLeft: 12,
|
||||
paddingRight: 12,
|
||||
minWidth: 0,
|
||||
paddingBottom: 8,
|
||||
}}
|
||||
>
|
||||
<OpenInNewOutlined
|
||||
css={{
|
||||
flexShrink: 0,
|
||||
width: 14,
|
||||
height: 14,
|
||||
color: theme.palette.text.primary,
|
||||
<FormControl size="small" css={styles.protocolFormControl}>
|
||||
<Select
|
||||
css={styles.listeningPortProtocol}
|
||||
value={listeningPortProtocol}
|
||||
onChange={async (event) => {
|
||||
const selectedProtocol = event.target.value as
|
||||
| "http"
|
||||
| "https";
|
||||
setListeningPortProtocol(selectedProtocol);
|
||||
saveWorkspaceListeningPortsProtocol(
|
||||
workspaceID,
|
||||
selectedProtocol,
|
||||
);
|
||||
}}
|
||||
>
|
||||
<MenuItem value="http">HTTP</MenuItem>
|
||||
<MenuItem value="https">HTTPS</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<form
|
||||
css={styles.newPortForm}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.currentTarget);
|
||||
const port = Number(formData.get("portNumber"));
|
||||
const url = portForwardURL(
|
||||
host,
|
||||
port,
|
||||
agent.name,
|
||||
workspaceName,
|
||||
username,
|
||||
listeningPortProtocol,
|
||||
);
|
||||
window.open(url, "_blank");
|
||||
}}
|
||||
/>
|
||||
</Button>
|
||||
</form>
|
||||
</header>
|
||||
<div
|
||||
css={{
|
||||
padding: 20,
|
||||
paddingTop: 0,
|
||||
}}
|
||||
>
|
||||
{filteredListeningPorts?.map((port) => {
|
||||
>
|
||||
<input
|
||||
aria-label="Port number"
|
||||
name="portNumber"
|
||||
type="number"
|
||||
placeholder="Connect to port..."
|
||||
min={9}
|
||||
max={65535}
|
||||
required
|
||||
css={styles.newPortInput}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
size="small"
|
||||
variant="text"
|
||||
css={{
|
||||
paddingLeft: 12,
|
||||
paddingRight: 12,
|
||||
minWidth: 0,
|
||||
}}
|
||||
>
|
||||
<OpenInNewOutlined
|
||||
css={{
|
||||
flexShrink: 0,
|
||||
width: 14,
|
||||
height: 14,
|
||||
color: theme.palette.text.primary,
|
||||
}}
|
||||
/>
|
||||
</Button>
|
||||
</form>
|
||||
</Stack>
|
||||
</Stack>
|
||||
{filteredListeningPorts.length === 0 && (
|
||||
<HelpTooltipText css={styles.noPortText}>
|
||||
No open ports were detected.
|
||||
</HelpTooltipText>
|
||||
)}
|
||||
{filteredListeningPorts.map((port) => {
|
||||
const url = portForwardURL(
|
||||
host,
|
||||
port.port,
|
||||
agent.name,
|
||||
workspaceName,
|
||||
username,
|
||||
listeningPortProtocol,
|
||||
);
|
||||
const label =
|
||||
port.process_name !== "" ? port.process_name : port.port;
|
||||
|
@ -323,22 +346,7 @@ export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
|
|||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Link
|
||||
underline="none"
|
||||
css={styles.portLink}
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<SensorsIcon css={{ width: 14, height: 14 }} />
|
||||
{label}
|
||||
</Link>
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={2}
|
||||
justifyContent="flex-end"
|
||||
alignItems="center"
|
||||
>
|
||||
<Stack direction="row" gap={3}>
|
||||
<Link
|
||||
underline="none"
|
||||
css={styles.portLink}
|
||||
|
@ -346,8 +354,25 @@ export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
|
|||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<span css={styles.portNumber}>{port.port}</span>
|
||||
<SensorsIcon css={{ width: 14, height: 14 }} />
|
||||
{port.port}
|
||||
</Link>
|
||||
<Link
|
||||
underline="none"
|
||||
css={styles.portLink}
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{label}
|
||||
</Link>
|
||||
</Stack>
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={2}
|
||||
justifyContent="flex-end"
|
||||
alignItems="center"
|
||||
>
|
||||
{canSharePorts && (
|
||||
<Button
|
||||
size="small"
|
||||
|
@ -356,7 +381,7 @@ export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
|
|||
await upsertSharedPortMutation.mutateAsync({
|
||||
agent_name: agent.name,
|
||||
port: port.port,
|
||||
protocol: "http",
|
||||
protocol: listeningPortProtocol,
|
||||
share_level: "authenticated",
|
||||
});
|
||||
await sharedPortsQuery.refetch();
|
||||
|
@ -369,7 +394,7 @@ export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
|
|||
</Stack>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Stack>
|
||||
</div>
|
||||
{portSharingExperimentEnabled && (
|
||||
<div
|
||||
|
@ -393,7 +418,7 @@ export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
|
|||
agent.name,
|
||||
workspaceName,
|
||||
username,
|
||||
share.protocol === "https",
|
||||
share.protocol,
|
||||
);
|
||||
const label = share.port;
|
||||
return (
|
||||
|
@ -619,6 +644,22 @@ const styles = {
|
|||
"&:focus-within": {
|
||||
borderColor: theme.palette.primary.main,
|
||||
},
|
||||
width: "100%",
|
||||
}),
|
||||
|
||||
listeningPortProtocol: (theme) => ({
|
||||
boxShadow: "none",
|
||||
".MuiOutlinedInput-notchedOutline": { border: 0 },
|
||||
"&.MuiOutlinedInput-root:hover .MuiOutlinedInput-notchedOutline": {
|
||||
border: 0,
|
||||
},
|
||||
"&.MuiOutlinedInput-root.Mui-focused .MuiOutlinedInput-notchedOutline": {
|
||||
border: 0,
|
||||
},
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
borderRadius: "4px",
|
||||
marginTop: 8,
|
||||
minWidth: "100px",
|
||||
}),
|
||||
|
||||
newPortInput: (theme) => ({
|
||||
|
@ -633,6 +674,12 @@ const styles = {
|
|||
display: "block",
|
||||
width: "100%",
|
||||
}),
|
||||
noPortText: (theme) => ({
|
||||
color: theme.palette.text.secondary,
|
||||
paddingTop: 20,
|
||||
paddingBottom: 10,
|
||||
textAlign: "center",
|
||||
}),
|
||||
sharedPortLink: () => ({
|
||||
minWidth: 80,
|
||||
}),
|
||||
|
|
|
@ -15,7 +15,7 @@ export interface VSCodeDesktopButtonProps {
|
|||
workspaceName: string;
|
||||
agentName?: string;
|
||||
folderPath?: string;
|
||||
displayApps: DisplayApp[];
|
||||
displayApps: readonly DisplayApp[];
|
||||
}
|
||||
|
||||
type VSCodeVariant = "vscode" | "vscode-insiders";
|
||||
|
|
|
@ -32,7 +32,7 @@ export const Language = {
|
|||
};
|
||||
|
||||
export interface AuditPageViewProps {
|
||||
auditLogs?: AuditLog[];
|
||||
auditLogs?: readonly AuditLog[];
|
||||
isNonInitialPage: boolean;
|
||||
isAuditLogVisible: boolean;
|
||||
error?: unknown;
|
||||
|
|
|
@ -34,19 +34,35 @@ export const OptionValue: FC<OptionValueProps> = (props) => {
|
|||
const theme = useTheme();
|
||||
|
||||
if (typeof value === "boolean") {
|
||||
return value ? <EnabledBadge /> : <DisabledBadge />;
|
||||
return (
|
||||
<div className="option-value-boolean">
|
||||
{value ? <EnabledBadge /> : <DisabledBadge />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof value === "number") {
|
||||
return <span css={styles.option}>{value}</span>;
|
||||
return (
|
||||
<span css={styles.option} className="option-value-number">
|
||||
{value}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (!value || value.length === 0) {
|
||||
return <span css={styles.option}>Not set</span>;
|
||||
return (
|
||||
<span css={styles.option} className="option-value-empty">
|
||||
Not set
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof value === "string") {
|
||||
return <span css={styles.option}>{value}</span>;
|
||||
return (
|
||||
<span css={styles.option} className="option-value-string">
|
||||
{value}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof value === "object" && !Array.isArray(value)) {
|
||||
|
@ -94,7 +110,7 @@ export const OptionValue: FC<OptionValueProps> = (props) => {
|
|||
|
||||
if (Array.isArray(value)) {
|
||||
return (
|
||||
<ul css={{ listStylePosition: "inside" }}>
|
||||
<ul css={{ listStylePosition: "inside" }} className="option-array">
|
||||
{value.map((item) => (
|
||||
<li key={item} css={styles.option}>
|
||||
{item}
|
||||
|
@ -104,7 +120,11 @@ export const OptionValue: FC<OptionValueProps> = (props) => {
|
|||
);
|
||||
}
|
||||
|
||||
return <span css={styles.option}>{JSON.stringify(value)}</span>;
|
||||
return (
|
||||
<span css={styles.option} className="option-value-json">
|
||||
{JSON.stringify(value)}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
type OptionConfigProps = HTMLAttributes<HTMLDivElement> & { isSource: boolean };
|
||||
|
|
|
@ -17,8 +17,8 @@ import {
|
|||
import { optionValue } from "./optionValue";
|
||||
|
||||
interface OptionsTableProps {
|
||||
options: SerpentOption[];
|
||||
additionalValues?: string[];
|
||||
options: readonly SerpentOption[];
|
||||
additionalValues?: readonly string[];
|
||||
}
|
||||
|
||||
const OptionsTable: FC<OptionsTableProps> = ({ options, additionalValues }) => {
|
||||
|
|
|
@ -4,7 +4,7 @@ import type { SerpentOption } from "api/typesGenerated";
|
|||
// optionValue is a helper function to format the value of a specific deployment options
|
||||
export function optionValue(
|
||||
option: SerpentOption,
|
||||
additionalValues?: string[],
|
||||
additionalValues?: readonly string[],
|
||||
) {
|
||||
// If option annotations are present, use them to format the value.
|
||||
if (option.annotations) {
|
||||
|
|
|
@ -214,7 +214,7 @@ export const BooleanPill: FC<BooleanPillProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
type LogsProps = { lines: string[] } & HTMLAttributes<HTMLDivElement>;
|
||||
type LogsProps = HTMLAttributes<HTMLDivElement> & { lines: readonly string[] };
|
||||
|
||||
export const Logs: FC<LogsProps> = ({ lines, ...divProps }) => {
|
||||
const theme = useTheme();
|
||||
|
|
|
@ -41,8 +41,8 @@ export const WebsocketPage = () => {
|
|||
|
||||
{websocket.warnings.map((warning) => {
|
||||
return (
|
||||
<Alert key={warning} severity="warning">
|
||||
{warning}
|
||||
<Alert key={warning.code} severity="warning">
|
||||
{warning.message}
|
||||
</Alert>
|
||||
);
|
||||
})}
|
||||
|
|
|
@ -296,7 +296,7 @@ const UsersLatencyPanel: FC<UsersLatencyPanelProps> = ({
|
|||
{!data && <Loader css={{ height: "100%" }} />}
|
||||
{users && users.length === 0 && <NoDataAvailable />}
|
||||
{users &&
|
||||
users
|
||||
[...users]
|
||||
.sort((a, b) => b.latency_ms.p50 - a.latency_ms.p50)
|
||||
.map((row) => (
|
||||
<div
|
||||
|
@ -367,7 +367,7 @@ const UsersActivityPanel: FC<UsersActivityPanelProps> = ({
|
|||
{!data && <Loader css={{ height: "100%" }} />}
|
||||
{users && users.length === 0 && <NoDataAvailable />}
|
||||
{users &&
|
||||
users
|
||||
[...users]
|
||||
.sort((a, b) => b.seconds - a.seconds)
|
||||
.map((row) => (
|
||||
<div
|
||||
|
@ -405,7 +405,7 @@ const UsersActivityPanel: FC<UsersActivityPanelProps> = ({
|
|||
};
|
||||
|
||||
interface TemplateUsagePanelProps extends PanelProps {
|
||||
data: TemplateAppUsage[] | undefined;
|
||||
data: readonly TemplateAppUsage[] | undefined;
|
||||
}
|
||||
|
||||
const TemplateUsagePanel: FC<TemplateUsagePanelProps> = ({
|
||||
|
@ -508,7 +508,7 @@ const TemplateUsagePanel: FC<TemplateUsagePanelProps> = ({
|
|||
};
|
||||
|
||||
interface TemplateParametersUsagePanelProps extends PanelProps {
|
||||
data: TemplateParameterUsage[] | undefined;
|
||||
data: readonly TemplateParameterUsage[] | undefined;
|
||||
}
|
||||
|
||||
const TemplateParametersUsagePanel: FC<TemplateParametersUsagePanelProps> = ({
|
||||
|
@ -579,7 +579,7 @@ const TemplateParametersUsagePanel: FC<TemplateParametersUsagePanelProps> = ({
|
|||
<div>Count</div>
|
||||
</Tooltip>
|
||||
</ParameterUsageRow>
|
||||
{parameter.values
|
||||
{[...parameter.values]
|
||||
.sort((a, b) => b.count - a.count)
|
||||
.filter((usage) => filterOrphanValues(usage, parameter))
|
||||
.map((usage, usageIndex) => (
|
||||
|
|
|
@ -61,6 +61,7 @@ export interface TemplateSettingsForm {
|
|||
// Helpful to show field errors on Storybook
|
||||
initialTouched?: FormikTouched<UpdateTemplateMeta>;
|
||||
accessControlEnabled: boolean;
|
||||
advancedSchedulingEnabled: boolean;
|
||||
portSharingExperimentEnabled: boolean;
|
||||
portSharingControlsEnabled: boolean;
|
||||
}
|
||||
|
@ -73,6 +74,7 @@ export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
|
|||
isSubmitting,
|
||||
initialTouched,
|
||||
accessControlEnabled,
|
||||
advancedSchedulingEnabled,
|
||||
portSharingExperimentEnabled,
|
||||
portSharingControlsEnabled,
|
||||
}) => {
|
||||
|
@ -195,39 +197,54 @@ export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
|
|||
</Stack>
|
||||
</Stack>
|
||||
</label>
|
||||
<label htmlFor="require_active_version">
|
||||
<Stack direction="row" spacing={1}>
|
||||
<Checkbox
|
||||
id="require_active_version"
|
||||
name="require_active_version"
|
||||
checked={form.values.require_active_version}
|
||||
onChange={form.handleChange}
|
||||
/>
|
||||
<Stack spacing={2}>
|
||||
<label htmlFor="require_active_version">
|
||||
<Stack direction="row" spacing={1}>
|
||||
<Checkbox
|
||||
id="require_active_version"
|
||||
name="require_active_version"
|
||||
checked={form.values.require_active_version}
|
||||
onChange={form.handleChange}
|
||||
disabled={
|
||||
!template.require_active_version &&
|
||||
!advancedSchedulingEnabled
|
||||
}
|
||||
/>
|
||||
|
||||
<Stack direction="column" spacing={0.5}>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
spacing={0.5}
|
||||
css={styles.optionText}
|
||||
>
|
||||
Require workspaces automatically update when started.
|
||||
<HelpTooltip>
|
||||
<HelpTooltipTrigger />
|
||||
<HelpTooltipContent>
|
||||
<HelpTooltipText>
|
||||
This setting is not enforced for template admins.
|
||||
</HelpTooltipText>
|
||||
</HelpTooltipContent>
|
||||
</HelpTooltip>
|
||||
<Stack direction="column" spacing={0.5}>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
spacing={0.5}
|
||||
css={styles.optionText}
|
||||
>
|
||||
Require workspaces automatically update when started.
|
||||
<HelpTooltip>
|
||||
<HelpTooltipTrigger />
|
||||
<HelpTooltipContent>
|
||||
<HelpTooltipText>
|
||||
This setting is not enforced for template admins.
|
||||
</HelpTooltipText>
|
||||
</HelpTooltipContent>
|
||||
</HelpTooltip>
|
||||
</Stack>
|
||||
<span css={styles.optionHelperText}>
|
||||
Workspaces that are manually started or auto-started will
|
||||
use the active template version.
|
||||
</span>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</label>
|
||||
|
||||
{!advancedSchedulingEnabled && (
|
||||
<Stack direction="row">
|
||||
<EnterpriseBadge />
|
||||
<span css={styles.optionHelperText}>
|
||||
Workspaces that are manually started or auto-started will use
|
||||
the active template version.
|
||||
Enterprise license required to enabled.
|
||||
</span>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</label>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</FormSection>
|
||||
|
||||
|
@ -241,7 +258,9 @@ export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
|
|||
helperText:
|
||||
"Leave the message empty to keep the template active. Any message provided will mark the template as deprecated. Use this message to inform users of the deprecation and how to migrate to a new template.",
|
||||
})}
|
||||
disabled={isSubmitting || !accessControlEnabled}
|
||||
disabled={
|
||||
isSubmitting || (!template.deprecated && !accessControlEnabled)
|
||||
}
|
||||
fullWidth
|
||||
label="Deprecation Message"
|
||||
/>
|
||||
|
@ -250,6 +269,8 @@ export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
|
|||
<EnterpriseBadge />
|
||||
<span css={styles.optionHelperText}>
|
||||
Enterprise license required to deprecate templates.
|
||||
{template.deprecated &&
|
||||
" You cannot change the message, but you may remove it to mark this template as no longer deprecated."}
|
||||
</span>
|
||||
</Stack>
|
||||
)}
|
||||
|
|
|
@ -20,6 +20,8 @@ export const TemplateSettingsPage: FC = () => {
|
|||
const queryClient = useQueryClient();
|
||||
const { entitlements, experiments } = useDashboard();
|
||||
const accessControlEnabled = entitlements.features.access_control.enabled;
|
||||
const advancedSchedulingEnabled =
|
||||
entitlements.features.advanced_template_scheduling.enabled;
|
||||
const sharedPortsExperimentEnabled = experiments.includes("shared-ports");
|
||||
const sharedPortControlsEnabled =
|
||||
entitlements.features.control_shared_ports.enabled;
|
||||
|
@ -70,6 +72,7 @@ export const TemplateSettingsPage: FC = () => {
|
|||
});
|
||||
}}
|
||||
accessControlEnabled={accessControlEnabled}
|
||||
advancedSchedulingEnabled={advancedSchedulingEnabled}
|
||||
sharedPortsExperimentEnabled={sharedPortsExperimentEnabled}
|
||||
sharedPortControlsEnabled={sharedPortControlsEnabled}
|
||||
/>
|
||||
|
|
|
@ -8,6 +8,7 @@ const meta: Meta<typeof TemplateSettingsPageView> = {
|
|||
args: {
|
||||
template: MockTemplate,
|
||||
accessControlEnabled: true,
|
||||
advancedSchedulingEnabled: true,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -36,5 +37,19 @@ export const SaveTemplateSettingsError: Story = {
|
|||
export const NoEntitlements: Story = {
|
||||
args: {
|
||||
accessControlEnabled: false,
|
||||
advancedSchedulingEnabled: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const NoEntitlementsExpiredSettings: Story = {
|
||||
args: {
|
||||
template: {
|
||||
...MockTemplate,
|
||||
deprecated: true,
|
||||
deprecation_message: "This template tastes bad",
|
||||
require_active_version: true,
|
||||
},
|
||||
accessControlEnabled: false,
|
||||
advancedSchedulingEnabled: false,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -13,6 +13,7 @@ export interface TemplateSettingsPageViewProps {
|
|||
typeof TemplateSettingsForm
|
||||
>["initialTouched"];
|
||||
accessControlEnabled: boolean;
|
||||
advancedSchedulingEnabled: boolean;
|
||||
sharedPortsExperimentEnabled: boolean;
|
||||
sharedPortControlsEnabled: boolean;
|
||||
}
|
||||
|
@ -25,6 +26,7 @@ export const TemplateSettingsPageView: FC<TemplateSettingsPageViewProps> = ({
|
|||
submitError,
|
||||
initialTouched,
|
||||
accessControlEnabled,
|
||||
advancedSchedulingEnabled,
|
||||
sharedPortsExperimentEnabled,
|
||||
sharedPortControlsEnabled,
|
||||
}) => {
|
||||
|
@ -42,6 +44,7 @@ export const TemplateSettingsPageView: FC<TemplateSettingsPageViewProps> = ({
|
|||
onCancel={onCancel}
|
||||
error={submitError}
|
||||
accessControlEnabled={accessControlEnabled}
|
||||
advancedSchedulingEnabled={advancedSchedulingEnabled}
|
||||
portSharingExperimentEnabled={sharedPortsExperimentEnabled}
|
||||
portSharingControlsEnabled={sharedPortControlsEnabled}
|
||||
/>
|
||||
|
|
|
@ -113,7 +113,7 @@ const ProxyMessagesRow: FC<ProxyMessagesRowProps> = ({ proxy }) => {
|
|||
|
||||
interface ProxyMessagesListProps {
|
||||
title: ReactNode;
|
||||
messages?: string[];
|
||||
messages?: readonly string[];
|
||||
}
|
||||
|
||||
const ProxyMessagesList: FC<ProxyMessagesListProps> = ({ title, messages }) => {
|
||||
|
|
|
@ -15,7 +15,7 @@ import type { ProxyLatencyReport } from "contexts/useProxyLatency";
|
|||
import { ProxyRow } from "./WorkspaceProxyRow";
|
||||
|
||||
export interface WorkspaceProxyViewProps {
|
||||
proxies?: Region[];
|
||||
proxies?: readonly Region[];
|
||||
proxyLatencies?: Record<string, ProxyLatencyReport>;
|
||||
getWorkspaceProxiesError?: unknown;
|
||||
isLoading: boolean;
|
||||
|
|
|
@ -9,7 +9,7 @@ import { UsersFilter } from "./UsersFilter";
|
|||
import { UsersTable } from "./UsersTable/UsersTable";
|
||||
|
||||
export interface UsersPageViewProps {
|
||||
users?: TypesGen.User[];
|
||||
users?: readonly TypesGen.User[];
|
||||
roles?: TypesGen.AssignableRoles[];
|
||||
isUpdatingUserRoles?: boolean;
|
||||
canEditUsers: boolean;
|
||||
|
|
|
@ -69,7 +69,7 @@ const Option: FC<OptionProps> = ({
|
|||
|
||||
export interface EditRolesButtonProps {
|
||||
isLoading: boolean;
|
||||
roles: Role[];
|
||||
roles: readonly Role[];
|
||||
selectedRoleNames: Set<string>;
|
||||
onChange: (roles: Role["name"][]) => void;
|
||||
isDefaultOpen?: boolean;
|
||||
|
|
|
@ -160,7 +160,7 @@ const roleNamesByAccessLevel: readonly string[] = [
|
|||
"auditor",
|
||||
];
|
||||
|
||||
function sortRolesByAccessLevel(roles: Role[]) {
|
||||
function sortRolesByAccessLevel(roles: readonly Role[]): readonly Role[] {
|
||||
if (roles.length === 0) {
|
||||
return roles;
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ export const Language = {
|
|||
} as const;
|
||||
|
||||
export interface UsersTableProps {
|
||||
users: TypesGen.User[] | undefined;
|
||||
users: readonly TypesGen.User[] | undefined;
|
||||
roles: TypesGen.AssignableRoles[] | undefined;
|
||||
groupsByUserId: GroupsByUserId | undefined;
|
||||
isUpdatingUserRoles?: boolean;
|
||||
|
|
|
@ -36,7 +36,7 @@ import { UserRoleCell } from "./UserRoleCell";
|
|||
dayjs.extend(relativeTime);
|
||||
|
||||
interface UsersTableBodyProps {
|
||||
users: TypesGen.User[] | undefined;
|
||||
users: readonly TypesGen.User[] | undefined;
|
||||
groupsByUserId: GroupsByUserId | undefined;
|
||||
authMethods?: TypesGen.AuthMethods;
|
||||
roles?: TypesGen.AssignableRoles[];
|
||||
|
|
|
@ -13,7 +13,7 @@ import { getResourceIconPath } from "utils/workspace";
|
|||
dayjs.extend(relativeTime);
|
||||
|
||||
type BatchDeleteConfirmationProps = {
|
||||
checkedWorkspaces: Workspace[];
|
||||
checkedWorkspaces: readonly Workspace[];
|
||||
open: boolean;
|
||||
isLoading: boolean;
|
||||
onClose: () => void;
|
||||
|
@ -111,7 +111,7 @@ export const BatchDeleteConfirmation: FC<BatchDeleteConfirmationProps> = ({
|
|||
};
|
||||
|
||||
interface StageProps {
|
||||
workspaces: Workspace[];
|
||||
workspaces: readonly Workspace[];
|
||||
}
|
||||
|
||||
const Consequences: FC = () => {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { action } from "@storybook/addon-actions";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { useQueryClient } from "react-query";
|
||||
import type { Workspace } from "api/typesGenerated";
|
||||
import { chromatic } from "testHelpers/chromatic";
|
||||
import {
|
||||
MockWorkspace,
|
||||
|
@ -29,23 +30,33 @@ const workspaces = [
|
|||
},
|
||||
];
|
||||
|
||||
const updates = new Map<string, Update>();
|
||||
for (const it of workspaces) {
|
||||
const versionId = it.template_active_version_id;
|
||||
const version = updates.get(versionId);
|
||||
function getPopulatedUpdates(): Map<string, Update> {
|
||||
type MutableUpdate = Omit<Update, "affected_workspaces"> & {
|
||||
affected_workspaces: Workspace[];
|
||||
};
|
||||
|
||||
if (version) {
|
||||
version.affected_workspaces.push(it);
|
||||
continue;
|
||||
const updates = new Map<string, MutableUpdate>();
|
||||
for (const it of workspaces) {
|
||||
const versionId = it.template_active_version_id;
|
||||
const version = updates.get(versionId);
|
||||
|
||||
if (version) {
|
||||
version.affected_workspaces.push(it);
|
||||
continue;
|
||||
}
|
||||
|
||||
updates.set(versionId, {
|
||||
...MockTemplateVersion,
|
||||
template_display_name: it.template_display_name,
|
||||
affected_workspaces: [it],
|
||||
});
|
||||
}
|
||||
|
||||
updates.set(versionId, {
|
||||
...MockTemplateVersion,
|
||||
template_display_name: it.template_display_name,
|
||||
affected_workspaces: [it],
|
||||
});
|
||||
return updates as Map<string, Update>;
|
||||
}
|
||||
|
||||
const updates = getPopulatedUpdates();
|
||||
|
||||
const meta: Meta<typeof BatchUpdateConfirmation> = {
|
||||
title: "pages/WorkspacesPage/BatchUpdateConfirmation",
|
||||
parameters: { chromatic },
|
||||
|
|
|
@ -18,7 +18,7 @@ import { Stack } from "components/Stack/Stack";
|
|||
dayjs.extend(relativeTime);
|
||||
|
||||
type BatchUpdateConfirmationProps = {
|
||||
checkedWorkspaces: Workspace[];
|
||||
checkedWorkspaces: readonly Workspace[];
|
||||
open: boolean;
|
||||
isLoading: boolean;
|
||||
onClose: () => void;
|
||||
|
@ -27,7 +27,7 @@ type BatchUpdateConfirmationProps = {
|
|||
|
||||
export interface Update extends TemplateVersion {
|
||||
template_display_name: string;
|
||||
affected_workspaces: Workspace[];
|
||||
affected_workspaces: readonly Workspace[];
|
||||
}
|
||||
|
||||
export const BatchUpdateConfirmation: FC<BatchUpdateConfirmationProps> = ({
|
||||
|
@ -90,11 +90,13 @@ export const BatchUpdateConfirmation: FC<BatchUpdateConfirmationProps> = ({
|
|||
// Figure out which new versions everything will be updated to so that we can
|
||||
// show update messages and such.
|
||||
const newVersions = useMemo(() => {
|
||||
const newVersions = new Map<
|
||||
string,
|
||||
Pick<Update, "id" | "template_display_name" | "affected_workspaces">
|
||||
>();
|
||||
type MutableUpdateInfo = {
|
||||
id: string;
|
||||
template_display_name: string;
|
||||
affected_workspaces: Workspace[];
|
||||
};
|
||||
|
||||
const newVersions = new Map<string, MutableUpdateInfo>();
|
||||
for (const it of workspacesToUpdate) {
|
||||
const versionId = it.template_active_version_id;
|
||||
const version = newVersions.get(versionId);
|
||||
|
@ -111,7 +113,11 @@ export const BatchUpdateConfirmation: FC<BatchUpdateConfirmationProps> = ({
|
|||
});
|
||||
}
|
||||
|
||||
return newVersions;
|
||||
type ReadonlyUpdateInfo = Readonly<MutableUpdateInfo> & {
|
||||
affected_workspaces: readonly Workspace[];
|
||||
};
|
||||
|
||||
return newVersions as Map<string, ReadonlyUpdateInfo>;
|
||||
}, [workspacesToUpdate]);
|
||||
|
||||
// Not all of the information we want is included in the `Workspace` type, so we
|
||||
|
@ -401,7 +407,7 @@ const TemplateVersionMessages: FC<TemplateVersionMessagesProps> = ({
|
|||
};
|
||||
|
||||
interface UsedByProps {
|
||||
workspaces: Workspace[];
|
||||
workspaces: readonly Workspace[];
|
||||
}
|
||||
|
||||
const UsedBy: FC<UsedByProps> = ({ workspaces }) => {
|
||||
|
|
|
@ -54,7 +54,9 @@ const WorkspacesPage: FC = () => {
|
|||
});
|
||||
|
||||
const updateWorkspace = useWorkspaceUpdate(queryKey);
|
||||
const [checkedWorkspaces, setCheckedWorkspaces] = useState<Workspace[]>([]);
|
||||
const [checkedWorkspaces, setCheckedWorkspaces] = useState<
|
||||
readonly Workspace[]
|
||||
>([]);
|
||||
const [confirmingBatchAction, setConfirmingBatchAction] = useState<
|
||||
"delete" | "update" | null
|
||||
>(null);
|
||||
|
|
|
@ -44,15 +44,15 @@ type TemplateQuery = UseQueryResult<Template[]>;
|
|||
|
||||
export interface WorkspacesPageViewProps {
|
||||
error: unknown;
|
||||
workspaces?: Workspace[];
|
||||
checkedWorkspaces: Workspace[];
|
||||
workspaces?: readonly Workspace[];
|
||||
checkedWorkspaces: readonly Workspace[];
|
||||
count?: number;
|
||||
filterProps: ComponentProps<typeof WorkspacesFilter>;
|
||||
page: number;
|
||||
limit: number;
|
||||
onPageChange: (page: number) => void;
|
||||
onUpdateWorkspace: (workspace: Workspace) => void;
|
||||
onCheckChange: (checkedWorkspaces: Workspace[]) => void;
|
||||
onCheckChange: (checkedWorkspaces: readonly Workspace[]) => void;
|
||||
isRunningBatchAction: boolean;
|
||||
onDeleteAll: () => void;
|
||||
onUpdateAll: () => void;
|
||||
|
|
|
@ -30,12 +30,12 @@ import { getDisplayWorkspaceTemplateName } from "utils/workspace";
|
|||
import { WorkspacesEmpty } from "./WorkspacesEmpty";
|
||||
|
||||
export interface WorkspacesTableProps {
|
||||
workspaces?: Workspace[];
|
||||
checkedWorkspaces: Workspace[];
|
||||
workspaces?: readonly Workspace[];
|
||||
checkedWorkspaces: readonly Workspace[];
|
||||
error?: unknown;
|
||||
isUsingFilter: boolean;
|
||||
onUpdateWorkspace: (workspace: Workspace) => void;
|
||||
onCheckChange: (checkedWorkspaces: Workspace[]) => void;
|
||||
onCheckChange: (checkedWorkspaces: readonly Workspace[]) => void;
|
||||
canCheckWorkspaces: boolean;
|
||||
templates?: Template[];
|
||||
canCreateTemplate: boolean;
|
||||
|
|
|
@ -18,7 +18,7 @@ export function useBatchActions(options: UseBatchActionsProps) {
|
|||
const { onSuccess } = options;
|
||||
|
||||
const startAllMutation = useMutation({
|
||||
mutationFn: (workspaces: Workspace[]) => {
|
||||
mutationFn: (workspaces: readonly Workspace[]) => {
|
||||
return Promise.all(
|
||||
workspaces.map((w) =>
|
||||
startWorkspace(w.id, w.latest_build.template_version_id),
|
||||
|
@ -32,7 +32,7 @@ export function useBatchActions(options: UseBatchActionsProps) {
|
|||
});
|
||||
|
||||
const stopAllMutation = useMutation({
|
||||
mutationFn: (workspaces: Workspace[]) => {
|
||||
mutationFn: (workspaces: readonly Workspace[]) => {
|
||||
return Promise.all(workspaces.map((w) => stopWorkspace(w.id)));
|
||||
},
|
||||
onSuccess,
|
||||
|
@ -42,7 +42,7 @@ export function useBatchActions(options: UseBatchActionsProps) {
|
|||
});
|
||||
|
||||
const deleteAllMutation = useMutation({
|
||||
mutationFn: (workspaces: Workspace[]) => {
|
||||
mutationFn: (workspaces: readonly Workspace[]) => {
|
||||
return Promise.all(workspaces.map((w) => deleteWorkspace(w.id)));
|
||||
},
|
||||
onSuccess,
|
||||
|
@ -52,7 +52,7 @@ export function useBatchActions(options: UseBatchActionsProps) {
|
|||
});
|
||||
|
||||
const updateAllMutation = useMutation({
|
||||
mutationFn: (workspaces: Workspace[]) => {
|
||||
mutationFn: (workspaces: readonly Workspace[]) => {
|
||||
return Promise.all(
|
||||
workspaces
|
||||
.filter((w) => w.outdated && !w.dormant_at)
|
||||
|
@ -66,7 +66,7 @@ export function useBatchActions(options: UseBatchActionsProps) {
|
|||
});
|
||||
|
||||
const favoriteAllMutation = useMutation({
|
||||
mutationFn: (workspaces: Workspace[]) => {
|
||||
mutationFn: (workspaces: readonly Workspace[]) => {
|
||||
return Promise.all(
|
||||
workspaces
|
||||
.filter((w) => !w.favorite)
|
||||
|
@ -80,7 +80,7 @@ export function useBatchActions(options: UseBatchActionsProps) {
|
|||
});
|
||||
|
||||
const unfavoriteAllMutation = useMutation({
|
||||
mutationFn: (workspaces: Workspace[]) => {
|
||||
mutationFn: (workspaces: readonly Workspace[]) => {
|
||||
return Promise.all(
|
||||
workspaces
|
||||
.filter((w) => w.favorite)
|
||||
|
|
|
@ -3261,7 +3261,7 @@ export const MockHealth: TypesGen.HealthcheckReport = {
|
|||
export const MockListeningPortsResponse: TypesGen.WorkspaceAgentListeningPortsResponse =
|
||||
{
|
||||
ports: [
|
||||
{ process_name: "webb", network: "", port: 3000 },
|
||||
{ process_name: "webb", network: "", port: 30000 },
|
||||
{ process_name: "gogo", network: "", port: 8080 },
|
||||
{ process_name: "", network: "", port: 8081 },
|
||||
],
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
import type { WorkspaceAgentPortShareProtocol } from "api/typesGenerated";
|
||||
|
||||
export const portForwardURL = (
|
||||
host: string,
|
||||
port: number,
|
||||
agentName: string,
|
||||
workspaceName: string,
|
||||
username: string,
|
||||
https = false,
|
||||
protocol: WorkspaceAgentPortShareProtocol,
|
||||
): string => {
|
||||
const { location } = window;
|
||||
const suffix = https ? "s" : "";
|
||||
const suffix = protocol === "https" ? "s" : "";
|
||||
|
||||
const subdomain = `${port}${suffix}--${agentName}--${workspaceName}--${username}`;
|
||||
return `${location.protocol}//${host}`.replace("*", subdomain);
|
||||
|
@ -56,9 +58,28 @@ export const openMaybePortForwardedURL = (
|
|||
agentName,
|
||||
workspaceName,
|
||||
username,
|
||||
url.protocol.replace(":", "") as WorkspaceAgentPortShareProtocol,
|
||||
) + url.pathname,
|
||||
);
|
||||
} catch (ex) {
|
||||
open(uri);
|
||||
}
|
||||
};
|
||||
|
||||
export const saveWorkspaceListeningPortsProtocol = (
|
||||
workspaceID: string,
|
||||
protocol: WorkspaceAgentPortShareProtocol,
|
||||
) => {
|
||||
localStorage.setItem(
|
||||
`listening-ports-protocol-workspace-${workspaceID}`,
|
||||
protocol,
|
||||
);
|
||||
};
|
||||
|
||||
export const getWorkspaceListeningPortsProtocol = (
|
||||
workspaceID: string,
|
||||
): WorkspaceAgentPortShareProtocol => {
|
||||
return (localStorage.getItem(
|
||||
`listening-ports-protocol-workspace-${workspaceID}`,
|
||||
) || "http") as WorkspaceAgentPortShareProtocol;
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue