Files
garage-ui/backend/internal/services/admin_factory.go
T

133 lines
3.7 KiB
Go

package services
import (
"context"
"errors"
"fmt"
"net/http"
"time"
"Noooste/garage-ui/internal/config"
"Noooste/garage-ui/pkg/logger"
"github.com/Noooste/azuretls-client"
)
var ErrUnsupported = errors.New("operation not supported by this Garage version")
type Capabilities struct {
ClusterStatistics bool `json:"clusterStatistics"`
NodeInfo bool `json:"nodeInfo"`
NodeStatistics bool `json:"nodeStatistics"`
}
func CapabilitiesV2() Capabilities {
return Capabilities{
ClusterStatistics: true,
NodeInfo: true,
NodeStatistics: true,
}
}
func CapabilitiesV1() Capabilities {
return Capabilities{}
}
type AdminServiceResult struct {
Service AdminService
Capabilities Capabilities
APIVersion string
}
// errProbeNotFound means the probed route returned 404. Garage v2.x also serves
// /v1/health, so a 404 on /v2 is the only reliable "this is a v1 server" signal.
var errProbeNotFound = errors.New("probe endpoint not found")
func NewAdminService(cfg *config.GarageConfig, logLevel string) (*AdminServiceResult, error) {
// retry so a startup fails doesn't lock us to a v1 client.
err := probeEndpointWithRetry(cfg, "/v2/GetClusterHealth")
if err == nil {
logger.Info().Str("api_version", "v2").Msg("Detected Garage admin API v2")
svc := NewGarageV2AdminService(cfg, logLevel)
return &AdminServiceResult{
Service: svc,
Capabilities: CapabilitiesV2(),
APIVersion: "v2",
}, nil
}
// only fall back to v1 on a real 404
// other errors mean the server is up but the probe failed transiently; picking v1 against
// a v2.x server breaks /v1/status with "v1/ endpoint is no longer supported" (issue #78).
if !errors.Is(err, errProbeNotFound) {
return nil, fmt.Errorf(
"could not detect Garage admin API version at %s: %w. Ensure Garage v1.1+ is running and the admin API is reachable",
cfg.AdminEndpoint, err,
)
}
if err := probeEndpointWithRetry(cfg, "/v1/health"); err == nil {
logger.Info().
Str("api_version", "v1").
Msg("Detected Garage admin API v1 — cluster statistics and per-node details will be unavailable")
svc := NewGarageV1AdminService(cfg, logLevel)
return &AdminServiceResult{
Service: svc,
Capabilities: CapabilitiesV1(),
APIVersion: "v1",
}, nil
}
return nil, fmt.Errorf(
"could not connect to Garage admin API at %s. Ensure Garage v1.1+ is running and the admin API is enabled",
cfg.AdminEndpoint,
)
}
const probeAttempts = 4
// probeEndpointWithRetry retries transient failures with backoff, but returns a
// 404 immediately, a missing route won't appear on a retry.
func probeEndpointWithRetry(cfg *config.GarageConfig, path string) error {
var err error
backoff := 250 * time.Millisecond
for attempt := range probeAttempts {
err = probeEndpoint(cfg, path)
if err == nil || errors.Is(err, errProbeNotFound) {
return err
}
if attempt < probeAttempts-1 {
time.Sleep(backoff)
backoff *= 2
}
}
return err
}
func probeEndpoint(cfg *config.GarageConfig, path string) error {
session := azuretls.NewSession()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
resp, err := session.Do(&azuretls.Request{
Method: http.MethodGet,
Url: cfg.AdminEndpoint + path,
IgnoreBody: true,
OrderedHeaders: azuretls.OrderedHeaders{
{"Authorization", fmt.Sprintf("Bearer %s", cfg.AdminToken)},
},
}, ctx)
if err != nil {
return err
}
defer resp.RawBody.Close()
if resp.StatusCode == http.StatusNotFound {
return fmt.Errorf("probe %s returned status 404: %w", path, errProbeNotFound)
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("probe %s returned status %d", path, resp.StatusCode)
}
return nil
}