rdev/internal/adapter/zot/client.go
jordan 9a1309a0c5 feat: fix composable monorepo CI builds + health endpoint improvements
Composable monorepo CI fixes:
- Add empty go.sum.tmpl files for pkg, service, worker, and cli components
- Fix Dockerfile.tmpl glob patterns (COPY go.work.sum* is invalid in Kaniko)
- Add deps step to CI that runs go work sync and go mod tidy before builds
- Fix scalar-go dependency version (v0.1.2 doesn't exist, use v0.13.0)

Health endpoint improvements:
- Add registry health check (zot OCI /v2/ endpoint)
- Add health metrics for CI, registry, and Git
- Add /health/ci endpoint for Woodpecker health

Visual verification scaffolding:
- Add Playwright pod and scripts ConfigMap
- Add vision.md and implementation breakdown plan

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 18:46:51 -07:00

75 lines
1.8 KiB
Go

// Package zot provides a client for checking zot container registry health.
package zot
import (
"context"
"fmt"
"net/http"
"time"
"github.com/orchard9/rdev/internal/domain"
)
// Client checks zot registry health via the OCI /v2/ endpoint.
type Client struct {
url string
httpClient *http.Client
}
// NewClient creates a new zot health checker.
// The URL should be the registry base URL (e.g., "https://registry.threesix.ai").
func NewClient(url string) *Client {
return &Client{
url: url,
httpClient: &http.Client{
Timeout: 5 * time.Second,
},
}
}
// Check returns the health status of the registry.
// A 200 or 401 response indicates the registry is healthy (401 means auth required but registry is up).
func (c *Client) Check(ctx context.Context) domain.RegistryStatus {
start := time.Now()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.url+"/v2/", nil)
if err != nil {
return domain.RegistryStatus{
Healthy: false,
URL: c.url,
Error: fmt.Sprintf("failed to create request: %v", err),
LastChecked: time.Now().UTC(),
}
}
resp, err := c.httpClient.Do(req)
latency := time.Since(start)
if err != nil {
return domain.RegistryStatus{
Healthy: false,
URL: c.url,
Latency: latency.String(),
Error: fmt.Sprintf("connection error: %v", err),
LastChecked: time.Now().UTC(),
}
}
defer func() { _ = resp.Body.Close() }()
// 200 = healthy, 401 = healthy but requires auth
healthy := resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusUnauthorized
status := domain.RegistryStatus{
Healthy: healthy,
URL: c.url,
Latency: latency.String(),
LastChecked: time.Now().UTC(),
}
if !healthy {
status.Error = fmt.Sprintf("unexpected status code: %d", resp.StatusCode)
}
return status
}