coder/coderd/healthcheck/workspaceproxy.go

135 lines
4.1 KiB
Go

package healthcheck
import (
"context"
"fmt"
"sort"
"strings"
"github.com/coder/coder/v2/coderd/healthcheck/health"
"github.com/coder/coder/v2/coderd/util/ptr"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/codersdk/healthsdk"
)
type WorkspaceProxyReport healthsdk.WorkspaceProxyReport
type WorkspaceProxyReportOptions struct {
WorkspaceProxiesFetchUpdater WorkspaceProxiesFetchUpdater
Dismissed bool
}
type WorkspaceProxiesFetchUpdater interface {
Fetch(context.Context) (codersdk.RegionsResponse[codersdk.WorkspaceProxy], error)
Update(context.Context) error
}
// AGPLWorkspaceProxiesFetchUpdater implements WorkspaceProxiesFetchUpdater
// to the extent required by AGPL code. Which isn't that much.
type AGPLWorkspaceProxiesFetchUpdater struct{}
func (*AGPLWorkspaceProxiesFetchUpdater) Fetch(context.Context) (codersdk.RegionsResponse[codersdk.WorkspaceProxy], error) {
return codersdk.RegionsResponse[codersdk.WorkspaceProxy]{}, nil
}
func (*AGPLWorkspaceProxiesFetchUpdater) Update(context.Context) error {
return nil
}
func (r *WorkspaceProxyReport) Run(ctx context.Context, opts *WorkspaceProxyReportOptions) {
r.Healthy = true
r.Severity = health.SeverityOK
r.Warnings = make([]health.Message, 0)
r.Dismissed = opts.Dismissed
if opts.WorkspaceProxiesFetchUpdater == nil {
opts.WorkspaceProxiesFetchUpdater = &AGPLWorkspaceProxiesFetchUpdater{}
}
// If this fails, just mark it as a warning. It is still updated in the background.
if err := opts.WorkspaceProxiesFetchUpdater.Update(ctx); err != nil {
r.Severity = health.SeverityWarning
r.Warnings = append(r.Warnings, health.Messagef(health.CodeProxyUpdate, "update proxy health: %s", err))
return
}
proxies, err := opts.WorkspaceProxiesFetchUpdater.Fetch(ctx)
if err != nil {
r.Healthy = false
r.Severity = health.SeverityError
r.Error = health.Errorf(health.CodeProxyFetch, "fetch workspace proxies: %s", err)
return
}
for _, proxy := range proxies.Regions {
if !proxy.Deleted {
r.WorkspaceProxies.Regions = append(r.WorkspaceProxies.Regions, proxy)
}
}
if r.WorkspaceProxies.Regions == nil {
r.WorkspaceProxies.Regions = make([]codersdk.WorkspaceProxy, 0)
}
// Stable sort based on create timestamp.
sort.Slice(r.WorkspaceProxies.Regions, func(i int, j int) bool {
return r.WorkspaceProxies.Regions[i].CreatedAt.Before(r.WorkspaceProxies.Regions[j].CreatedAt)
})
var total, healthy, warning int
var errs []string
for _, proxy := range r.WorkspaceProxies.Regions {
total++
if proxy.Healthy {
// Warnings in the report are not considered unhealthy, only errors.
healthy++
}
if len(proxy.Status.Report.Warnings) > 0 {
warning++
}
for _, err := range proxy.Status.Report.Warnings {
r.Warnings = append(r.Warnings, health.Messagef(health.CodeProxyUnhealthy, "%s: %s", proxy.Name, err))
}
for _, err := range proxy.Status.Report.Errors {
errs = append(errs, fmt.Sprintf("%s: %s", proxy.Name, err))
}
}
r.Severity = calculateSeverity(total, healthy, warning)
r.Healthy = r.Severity.Value() < health.SeverityError.Value()
for _, err := range errs {
switch r.Severity {
case health.SeverityWarning, health.SeverityOK:
r.Warnings = append(r.Warnings, health.Messagef(health.CodeProxyUnhealthy, err))
case health.SeverityError:
r.appendError(*health.Errorf(health.CodeProxyUnhealthy, err))
}
}
}
// appendError appends errs onto r.Error.
// We only have one error, so multiple errors need to be squashed in there.
func (r *WorkspaceProxyReport) appendError(es ...string) {
if len(es) == 0 {
return
}
if r.Error != nil {
es = append([]string{*r.Error}, es...)
}
r.Error = ptr.Ref(strings.Join(es, "\n"))
}
// calculateSeverity returns:
// health.SeverityError if all proxies are unhealthy,
// health.SeverityOK if all proxies are healthy and there are no warnings,
// health.SeverityWarning otherwise.
func calculateSeverity(total, healthy, warning int) health.Severity {
if total == 0 || (total == healthy && warning == 0) {
return health.SeverityOK
}
if healthy == 0 {
return health.SeverityError
}
return health.SeverityWarning
}