-
Notifications
You must be signed in to change notification settings - Fork 881
feat(coderd/healthcheck): add health check for proxy #10846
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 9 commits
a17ec4c
49b6e19
c7e202c
89bae7e
bcdc08c
340a276
d3478c8
c746021
05d4b09
56be002
1112b1c
fed35bd
7f8760a
5f5c486
b9cbdd8
ae30089
21f52bb
79dc190
2530320
f192488
b45e9c5
63fbafb
6de94bc
b57cdeb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -135,7 +135,13 @@ type Options struct { | |
AccessControlStore *atomic.Pointer[dbauthz.AccessControlStore] | ||
// AppSecurityKey is the crypto key used to sign and encrypt tokens related to | ||
// workspace applications. It consists of both a signing and encryption key. | ||
AppSecurityKey workspaceapps.SecurityKey | ||
AppSecurityKey workspaceapps.SecurityKey | ||
|
||
// The following two functions are dependencies of HealthcheckFunc but are only implemented | ||
// in enterprise. Stubbing them out here. | ||
FetchWorkspaceProxiesFunc *atomic.Pointer[func(context.Context) (codersdk.RegionsResponse[codersdk.WorkspaceProxy], error)] | ||
UpdateProxyHealthFunc *atomic.Pointer[func(context.Context) error] | ||
johnstcn marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
HealthcheckFunc func(ctx context.Context, apiKey string) *healthcheck.Report | ||
HealthcheckTimeout time.Duration | ||
HealthcheckRefresh time.Duration | ||
|
@@ -396,9 +402,19 @@ func New(options *Options) *API { | |
*options.UpdateCheckOptions, | ||
) | ||
} | ||
|
||
if options.FetchWorkspaceProxiesFunc == nil { | ||
options.FetchWorkspaceProxiesFunc = &atomic.Pointer[func(context.Context) (codersdk.RegionsResponse[codersdk.WorkspaceProxy], error)]{} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Random comment: This looks pretty gnarly (generics within generics), I kind of get why the Go team delayed generics for as long as they did. 😄 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To be fair, generics within generics look gnarly in any language! |
||
} | ||
|
||
if options.UpdateProxyHealthFunc == nil { | ||
options.UpdateProxyHealthFunc = &atomic.Pointer[func(context.Context) error]{} | ||
} | ||
|
||
if options.HealthcheckFunc == nil { | ||
options.HealthcheckFunc = func(ctx context.Context, apiKey string) *healthcheck.Report { | ||
return healthcheck.Run(ctx, &healthcheck.ReportOptions{ | ||
authCtx := dbauthz.AsSystemRestricted(ctx) //nolint:gocritic // internal function | ||
return healthcheck.Run(authCtx, &healthcheck.ReportOptions{ | ||
Database: healthcheck.DatabaseReportOptions{ | ||
DB: options.Database, | ||
Threshold: options.DeploymentValues.Healthcheck.ThresholdDatabase.Value(), | ||
|
@@ -413,9 +429,15 @@ func New(options *Options) *API { | |
DerpHealth: derphealth.ReportOptions{ | ||
DERPMap: api.DERPMap(), | ||
}, | ||
WorkspaceProxy: healthcheck.WorkspaceProxyReportOptions{ | ||
CurrentVersion: buildinfo.Version(), | ||
FetchWorkspaceProxies: options.FetchWorkspaceProxiesFunc.Load(), | ||
UpdateProxyHealth: options.UpdateProxyHealthFunc.Load(), | ||
}, | ||
}) | ||
} | ||
} | ||
|
||
if options.HealthcheckTimeout == 0 { | ||
options.HealthcheckTimeout = 30 * time.Second | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,17 +13,19 @@ import ( | |
) | ||
|
||
const ( | ||
SectionDERP string = "DERP" | ||
SectionAccessURL string = "AccessURL" | ||
SectionWebsocket string = "Websocket" | ||
SectionDatabase string = "Database" | ||
SectionDERP string = "DERP" | ||
SectionAccessURL string = "AccessURL" | ||
SectionWebsocket string = "Websocket" | ||
SectionDatabase string = "Database" | ||
SectionWorkspaceProxy string = "WorkspaceProxy" | ||
) | ||
|
||
type Checker interface { | ||
DERP(ctx context.Context, opts *derphealth.ReportOptions) derphealth.Report | ||
AccessURL(ctx context.Context, opts *AccessURLReportOptions) AccessURLReport | ||
Websocket(ctx context.Context, opts *WebsocketReportOptions) WebsocketReport | ||
Database(ctx context.Context, opts *DatabaseReportOptions) DatabaseReport | ||
WorkspaceProxy(ctx context.Context, opts *WorkspaceProxyReportOptions) WorkspaceProxyReport | ||
} | ||
|
||
// @typescript-generate Report | ||
|
@@ -38,20 +40,22 @@ type Report struct { | |
// FailingSections is a list of sections that have failed their healthcheck. | ||
FailingSections []string `json:"failing_sections"` | ||
|
||
DERP derphealth.Report `json:"derp"` | ||
AccessURL AccessURLReport `json:"access_url"` | ||
Websocket WebsocketReport `json:"websocket"` | ||
Database DatabaseReport `json:"database"` | ||
DERP derphealth.Report `json:"derp"` | ||
AccessURL AccessURLReport `json:"access_url"` | ||
Websocket WebsocketReport `json:"websocket"` | ||
Database DatabaseReport `json:"database"` | ||
WorkspaceProxy WorkspaceProxyReport `json:"workspace_proxy"` | ||
|
||
// The Coder version of the server that the report was generated on. | ||
CoderVersion string `json:"coder_version"` | ||
} | ||
|
||
type ReportOptions struct { | ||
AccessURL AccessURLReportOptions | ||
Database DatabaseReportOptions | ||
DerpHealth derphealth.ReportOptions | ||
Websocket WebsocketReportOptions | ||
AccessURL AccessURLReportOptions | ||
Database DatabaseReportOptions | ||
DerpHealth derphealth.ReportOptions | ||
Websocket WebsocketReportOptions | ||
WorkspaceProxy WorkspaceProxyReportOptions | ||
|
||
Checker Checker | ||
} | ||
|
@@ -78,6 +82,11 @@ func (defaultChecker) Database(ctx context.Context, opts *DatabaseReportOptions) | |
return report | ||
} | ||
|
||
func (defaultChecker) WorkspaceProxy(ctx context.Context, opts *WorkspaceProxyReportOptions) (report WorkspaceProxyReport) { | ||
report.Run(ctx, opts) | ||
return report | ||
} | ||
|
||
func Run(ctx context.Context, opts *ReportOptions) *Report { | ||
var ( | ||
wg sync.WaitGroup | ||
|
@@ -136,6 +145,18 @@ func Run(ctx context.Context, opts *ReportOptions) *Report { | |
report.Database = opts.Checker.Database(ctx, &opts.Database) | ||
}() | ||
|
||
wg.Add(1) | ||
johnstcn marked this conversation as resolved.
Show resolved
Hide resolved
|
||
go func() { | ||
defer wg.Done() | ||
defer func() { | ||
if err := recover(); err != nil { | ||
report.WorkspaceProxy.Error = ptr.Ref(fmt.Sprint(err)) | ||
} | ||
}() | ||
|
||
report.WorkspaceProxy = opts.Checker.WorkspaceProxy(ctx, &opts.WorkspaceProxy) | ||
}() | ||
|
||
report.CoderVersion = buildinfo.Version() | ||
wg.Wait() | ||
|
||
|
@@ -153,6 +174,9 @@ func Run(ctx context.Context, opts *ReportOptions) *Report { | |
if !report.Database.Healthy { | ||
report.FailingSections = append(report.FailingSections, SectionDatabase) | ||
} | ||
if !report.WorkspaceProxy.Healthy { | ||
report.FailingSections = append(report.FailingSections, SectionWorkspaceProxy) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. side thought: frontend does not use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 Will file a follow-up PR Edit: #10854 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it's beneficial to have a structure that can be navigated in a generic way. for (let section of report.failing_sections) {
console.log(report[section].reason);
} Given a good design, it will "always works", even when new fields and structures are added. Granted, I'm not a proponent for the current design, just for the ability to do so. |
||
} | ||
|
||
report.Healthy = len(report.FailingSections) == 0 | ||
|
||
|
@@ -171,6 +195,9 @@ func Run(ctx context.Context, opts *ReportOptions) *Report { | |
if report.Database.Severity.Value() > report.Severity.Value() { | ||
report.Severity = report.Database.Severity | ||
} | ||
if report.WorkspaceProxy.Severity.Value() > report.Severity.Value() { | ||
report.Severity = report.WorkspaceProxy.Severity | ||
} | ||
return &report | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe move the comment to the place where you assign these no-op
{}