diff --git a/cmd/groundcontrol/server/main.go b/cmd/groundcontrol/server/main.go index 0c84a5cd2..54235c3f8 100644 --- a/cmd/groundcontrol/server/main.go +++ b/cmd/groundcontrol/server/main.go @@ -12,7 +12,7 @@ import ( "time" "github.com/container-registry/harbor-satellite/internal/env" - "github.com/container-registry/harbor-satellite/internal/groundcontrol/harborhealth" + "github.com/container-registry/harbor-satellite/internal/groundcontrol/harbor" "github.com/container-registry/harbor-satellite/internal/groundcontrol/migrator" "github.com/container-registry/harbor-satellite/internal/groundcontrol/server" "github.com/joho/godotenv" @@ -25,7 +25,7 @@ func main() { log.Fatalf("failed to load environment: %v", err) } - err := harborhealth.CheckHealth() + err := harbor.CheckHealth() if err != nil { log.Fatalf("health check failed: %v", err) } diff --git a/internal/groundcontrol/harbor/health.go b/internal/groundcontrol/harbor/health.go new file mode 100644 index 000000000..fd9d72389 --- /dev/null +++ b/internal/groundcontrol/harbor/health.go @@ -0,0 +1,87 @@ +package harbor + +import ( + "context" + "fmt" + "log" + "net/url" + "time" + + "github.com/container-registry/harbor-satellite/internal/env" + v2client "github.com/goharbor/go-client/pkg/sdk/v2.0/client" + "github.com/goharbor/go-client/pkg/sdk/v2.0/client/health" + "github.com/goharbor/go-client/pkg/sdk/v2.0/models" +) + +const defaultHealthCheckTimeout = 5 * time.Second + +var ignoredHealthComponents = map[string]struct{}{ + "portal": {}, + "trivy": {}, + "registryctl": {}, + "jobservice": {}, +} + +// CheckHealth verifies that all required Harbor components are healthy. +func CheckHealth() error { + if env.GC.Harbor.SkipHealthCheck { + log.Println("WARNING: Harbor health check skipped (SKIP_HARBOR_HEALTH_CHECK=true)") + return nil + } + + client, err := newHealthClient(env.GC.Harbor.URL) + if err != nil { + return fmt.Errorf("create Harbor client: %w", err) + } + return checkHealth(client.Health) +} + +func newHealthClient(rawURL string) (*v2client.HarborAPI, error) { + harborURL, err := url.Parse(rawURL) + if err != nil { + return nil, err + } + + harborURL.Path = v2client.DefaultBasePath + + return v2client.New(v2client.Config{URL: harborURL}), nil +} + +func checkHealth(client health.API) error { + params := health.NewGetHealthParamsWithTimeout(defaultHealthCheckTimeout) + response, err := client.GetHealth(context.Background(), params) + if err != nil { + return fmt.Errorf("failed to get Harbor health: %w", err) + } + if response == nil || response.Payload == nil { + return fmt.Errorf("failed to get Harbor health: empty response") + } + + unhealthy := getUnhealthyComponents(response.Payload.Components, ignoredHealthComponents) + if len(unhealthy) > 0 { + return fmt.Errorf("unhealthy components: %v", unhealthy) + } + + return nil +} + +func getUnhealthyComponents(components []*models.ComponentHealthStatus, ignored map[string]struct{}) []string { + var unhealthy []string + for _, component := range components { + if component == nil { + continue + } + if _, ignore := ignored[component.Name]; ignore { + continue + } + if component.Status != "healthy" { + entry := component.Name + if component.Error != "" { + entry += ": " + component.Error + } + unhealthy = append(unhealthy, entry) + } + } + + return unhealthy +} diff --git a/internal/groundcontrol/harborhealth/check.go b/internal/groundcontrol/harborhealth/check.go deleted file mode 100644 index d70d5bf50..000000000 --- a/internal/groundcontrol/harborhealth/check.go +++ /dev/null @@ -1,85 +0,0 @@ -package harborhealth - -import ( - "encoding/json" - "fmt" - "log" - "net/http" - "net/url" - "time" - - "github.com/container-registry/harbor-satellite/internal/env" -) - -type config struct { - HarborURL string - Timeout time.Duration - SkipComponents map[string]struct{} -} - -func defaultConfig() *config { - return &config{ - HarborURL: env.GC.Harbor.URL, - Timeout: 5 * time.Second, - SkipComponents: map[string]struct{}{ - "portal": {}, - "trivy": {}, - "registryctl": {}, - "jobservice": {}, - }, - } -} - -func CheckHealth() error { - cfg := env.GC - // Allow skipping health check for development/testing - if cfg.Harbor.SkipHealthCheck { - log.Println("WARNING: Harbor health check skipped (SKIP_HARBOR_HEALTH_CHECK=true)") - return nil - } - - config := defaultConfig() - return checkhealth(config) -} - -func checkhealth(config *config) error { - parsed, err := url.ParseRequestURI(config.HarborURL) - if err != nil { - return fmt.Errorf("invalid URL format: %w", err) - } - - if parsed.Scheme != "http" && parsed.Scheme != "https" { - return fmt.Errorf("unsupported URL scheme: %s (must be http or https)", parsed.Scheme) - } - - client := &http.Client{ - Timeout: config.Timeout, - } - - resp, err := client.Get(config.HarborURL + "/api/v2.0/health") - if err != nil { - return fmt.Errorf("failed to call API: %w", err) - } - - defer func() { - if err := resp.Body.Close(); err != nil { - log.Printf("failed to close response body: %v", err) - } - }() - - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("unexpected HTTP status: %s", resp.Status) - } - - var health HealthResponse - if err := json.NewDecoder(resp.Body).Decode(&health); err != nil { - return fmt.Errorf("failed to parse response: %w", err) - } - - unhealthyComponents := health.GetUnhealthyComponents(config.SkipComponents) - - if len(unhealthyComponents) > 0 { - return fmt.Errorf("unhealthy components: %v", unhealthyComponents) - } - return nil -} diff --git a/internal/groundcontrol/harborhealth/types.go b/internal/groundcontrol/harborhealth/types.go deleted file mode 100644 index 9a467bfaa..000000000 --- a/internal/groundcontrol/harborhealth/types.go +++ /dev/null @@ -1,29 +0,0 @@ -package harborhealth - -type Component struct { - Name string `json:"name"` - Status string `json:"status"` - Error string `json:"error,omitempty"` -} - -func (c *Component) IsHealthy() bool { - return c.Status == "healthy" -} - -type HealthResponse struct { - Components []Component `json:"components"` - Status string `json:"status"` -} - -func (h *HealthResponse) GetUnhealthyComponents(skip map[string]struct{}) []string { - var unhealthy []string - for _, c := range h.Components { - if _, ignore := skip[c.Name]; ignore { - continue - } - if !c.IsHealthy() { - unhealthy = append(unhealthy, c.Name, c.Error) - } - } - return unhealthy -}