Files
anchorage/internal/pkg/maintenance/maintenance.go
T
57_Wolve 12bf35caf8 anchorage v1.0 initial tree
Greenfield Go multi-tenant IPFS Pinning Service wire-compatible with the
IPFS Pinning Services API spec. Paired 1:1 with Kubo over localhost RPC,
clustered via embedded NATS JetStream, Postgres source-of-truth with
RLS-enforced tenancy, Fiber + huma v2 for the HTTP surface, Authentik
OIDC for session login with kid-rotated HS256 JWT API tokens.

Feature-complete against the 22-milestone build plan, including the
ship-it v1.0 gap items:

  * admin CLIs: drain/uncordon, maintenance, mint-token, rotate-key,
    prune-denylist, rebalance --dry-run, cache-stats, cluster-presences
  * TTL leader election via NATS KV, fence tokens, JetStream dedup
  * rebalancer (plan/apply split), reconciler, requeue sweeper
  * ristretto caches with NATS-backed cross-node invalidation
    (placements live-nodes + token denylist)
  * maintenance watchdog for stuck cluster-pause flag
  * Prometheus /metrics with CIDR ACL, HTTP/pin/scheduler/cache gauges
  * rate limiting: session (10/min) + anonymous global (120/min)
  * integration tests: rebalance, refcount multi-org, RLS belt
  * goreleaser (tar + deb/rpm/apk + Alpine Docker) targeting Gitea

Stack: Cobra/Viper, Fiber v2 + huma v2, embedded NATS JetStream,
pgx/sqlc/golang-migrate, ristretto, TypeID, prometheus/client_golang,
testcontainers-go.
2026-04-16 18:13:36 -05:00

88 lines
2.4 KiB
Go

// Package maintenance owns the cluster-wide maintenance flag stored in
// NATS JetStream KV.
//
// The flag is a single key ("maintenance") in the ANCHORAGE_CLUSTER
// bucket. Value is a JSON blob with the reason, who set it, and when.
// When present and not expired, the rebalancer and requeue sweeper
// pause so rolling upgrades don't thrash.
package maintenance
import (
"context"
"encoding/json"
"errors"
"fmt"
"time"
"github.com/nats-io/nats.go/jetstream"
)
// Bucket is the JetStream KV bucket name.
const Bucket = "ANCHORAGE_CLUSTER"
// Key is the single KV key for the cluster-wide flag.
const Key = "maintenance"
// Flag is the value stored in the KV when maintenance is on.
type Flag struct {
Reason string `json:"reason"`
SetBy string `json:"set_by"`
SetAt time.Time `json:"set_at"`
ExpiresAt time.Time `json:"expires_at,omitempty"`
}
// Manager reads and writes the maintenance flag.
type Manager struct {
kv jetstream.KeyValue
}
// NewManager opens (or creates) the bucket. MaxDuration is a soft
// rail: callers read it via IsOn and Warn if SetAt is older than the
// rail. The KV itself has no TTL (so a legitimate 24h window works).
func NewManager(ctx context.Context, js jetstream.JetStream) (*Manager, error) {
kv, err := js.CreateOrUpdateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: Bucket})
if err != nil {
return nil, fmt.Errorf("open %s kv: %w", Bucket, err)
}
return &Manager{kv: kv}, nil
}
// IsOn reports whether maintenance is currently enabled. Returns the
// Flag body on true, or nil on false. Expired flags (ExpiresAt in past)
// are treated as off.
func (m *Manager) IsOn(ctx context.Context) (*Flag, bool, error) {
entry, err := m.kv.Get(ctx, Key)
if err != nil {
if errors.Is(err, jetstream.ErrKeyNotFound) {
return nil, false, nil
}
return nil, false, err
}
var f Flag
if err := json.Unmarshal(entry.Value(), &f); err != nil {
return nil, false, fmt.Errorf("decode maintenance flag: %w", err)
}
if !f.ExpiresAt.IsZero() && f.ExpiresAt.Before(time.Now()) {
return nil, false, nil
}
return &f, true, nil
}
// Set enables cluster-wide maintenance.
func (m *Manager) Set(ctx context.Context, f Flag) error {
if f.SetAt.IsZero() {
f.SetAt = time.Now().UTC()
}
b, err := json.Marshal(f)
if err != nil {
return err
}
_, err = m.kv.Put(ctx, Key, b)
return err
}
// Clear disables maintenance.
func (m *Manager) Clear(ctx context.Context) error {
return m.kv.Delete(ctx, Key)
}