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.
88 lines
2.4 KiB
Go
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)
|
|
}
|