190 lines
5.8 KiB
Go
190 lines
5.8 KiB
Go
package runtime
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"galaxy/util"
|
|
)
|
|
|
|
// EngineVersionService implements the engine-version registry CRUD
|
|
// surface consumed by the admin endpoints under
|
|
// `/api/v1/admin/engine-versions/*`. Mutations are write-through: a
|
|
// successful Postgres write is followed by a cache update so warm
|
|
// reads observe the new state immediately.
|
|
type EngineVersionService struct {
|
|
store *Store
|
|
cache *Cache
|
|
now func() time.Time
|
|
}
|
|
|
|
// NewEngineVersionService constructs the service. now defaults to
|
|
// time.Now when nil.
|
|
func NewEngineVersionService(store *Store, cache *Cache, now func() time.Time) *EngineVersionService {
|
|
if now == nil {
|
|
now = time.Now
|
|
}
|
|
return &EngineVersionService{store: store, cache: cache, now: now}
|
|
}
|
|
|
|
// List returns every engine_versions row ordered by created_at DESC.
|
|
// Cache-first when warm; falls back to a Postgres read otherwise.
|
|
func (s *EngineVersionService) List(ctx context.Context) ([]EngineVersion, error) {
|
|
if s.cache != nil && s.cache.Ready() {
|
|
out := s.cache.ListEngineVersions()
|
|
sort.SliceStable(out, func(i, j int) bool {
|
|
if !out[i].CreatedAt.Equal(out[j].CreatedAt) {
|
|
return out[i].CreatedAt.After(out[j].CreatedAt)
|
|
}
|
|
return out[i].Version > out[j].Version
|
|
})
|
|
return out, nil
|
|
}
|
|
return s.store.ListEngineVersions(ctx)
|
|
}
|
|
|
|
// Get returns the row for version. Returns ErrNotFound on miss.
|
|
func (s *EngineVersionService) Get(ctx context.Context, version string) (EngineVersion, error) {
|
|
version = strings.TrimSpace(version)
|
|
if version == "" {
|
|
return EngineVersion{}, fmt.Errorf("%w: version must not be empty", ErrInvalidInput)
|
|
}
|
|
if s.cache != nil {
|
|
if v, ok := s.cache.GetEngineVersion(version); ok {
|
|
return v, nil
|
|
}
|
|
}
|
|
v, err := s.store.GetEngineVersion(ctx, version)
|
|
if err != nil {
|
|
return EngineVersion{}, err
|
|
}
|
|
if s.cache != nil {
|
|
s.cache.PutEngineVersion(v)
|
|
}
|
|
return v, nil
|
|
}
|
|
|
|
// RegisterInput is the parameter struct for Register.
|
|
type RegisterInput struct {
|
|
Version string
|
|
ImageRef string
|
|
Enabled *bool
|
|
}
|
|
|
|
// Validate normalises the request and rejects empty / malformed
|
|
// fields. Semver is enforced via `pkg/util.ParseSemver`.
|
|
func (in *RegisterInput) Validate() error {
|
|
in.Version = strings.TrimSpace(in.Version)
|
|
in.ImageRef = strings.TrimSpace(in.ImageRef)
|
|
if in.Version == "" {
|
|
return fmt.Errorf("%w: version must not be empty", ErrInvalidInput)
|
|
}
|
|
if _, err := util.ParseSemver(in.Version); err != nil {
|
|
return fmt.Errorf("%w: version %q is not a valid semver: %v", ErrInvalidInput, in.Version, err)
|
|
}
|
|
if in.ImageRef == "" {
|
|
return fmt.Errorf("%w: image_ref must not be empty", ErrInvalidInput)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Register persists a fresh engine_versions row. Returns
|
|
// ErrEngineVersionTaken on duplicate version.
|
|
func (s *EngineVersionService) Register(ctx context.Context, in RegisterInput) (EngineVersion, error) {
|
|
if err := (&in).Validate(); err != nil {
|
|
return EngineVersion{}, err
|
|
}
|
|
enabled := true
|
|
if in.Enabled != nil {
|
|
enabled = *in.Enabled
|
|
}
|
|
now := s.now().UTC()
|
|
v, err := s.store.InsertEngineVersion(ctx, in.Version, in.ImageRef, enabled, now)
|
|
if err != nil {
|
|
return EngineVersion{}, err
|
|
}
|
|
if s.cache != nil {
|
|
s.cache.PutEngineVersion(v)
|
|
}
|
|
return v, nil
|
|
}
|
|
|
|
// UpdateInput is the parameter struct for Update. Nil pointers leave
|
|
// the corresponding column alone.
|
|
type UpdateInput struct {
|
|
ImageRef *string
|
|
Enabled *bool
|
|
}
|
|
|
|
// Update patches mutable fields on an existing row.
|
|
func (s *EngineVersionService) Update(ctx context.Context, version string, in UpdateInput) (EngineVersion, error) {
|
|
version = strings.TrimSpace(version)
|
|
if version == "" {
|
|
return EngineVersion{}, fmt.Errorf("%w: version must not be empty", ErrInvalidInput)
|
|
}
|
|
patch := engineVersionUpdate{Enabled: in.Enabled}
|
|
if in.ImageRef != nil {
|
|
trimmed := strings.TrimSpace(*in.ImageRef)
|
|
if trimmed == "" {
|
|
return EngineVersion{}, fmt.Errorf("%w: image_ref must not be empty", ErrInvalidInput)
|
|
}
|
|
patch.ImageRef = &trimmed
|
|
}
|
|
now := s.now().UTC()
|
|
v, err := s.store.UpdateEngineVersion(ctx, version, patch, now)
|
|
if err != nil {
|
|
return EngineVersion{}, err
|
|
}
|
|
if s.cache != nil {
|
|
s.cache.PutEngineVersion(v)
|
|
}
|
|
return v, nil
|
|
}
|
|
|
|
// Disable flips the enabled flag to false. Idempotent.
|
|
func (s *EngineVersionService) Disable(ctx context.Context, version string) (EngineVersion, error) {
|
|
disabled := false
|
|
return s.Update(ctx, version, UpdateInput{Enabled: &disabled})
|
|
}
|
|
|
|
// Resolve returns the row for version, rejecting disabled rows with
|
|
// ErrEngineVersionDisabled. Used by `Service.StartGame` /
|
|
// `AdminPatch` / `AdminRestart` before the docker pull.
|
|
func (s *EngineVersionService) Resolve(ctx context.Context, version string) (EngineVersion, error) {
|
|
v, err := s.Get(ctx, version)
|
|
if err != nil {
|
|
return EngineVersion{}, err
|
|
}
|
|
if !v.Enabled {
|
|
return EngineVersion{}, fmt.Errorf("%w: %s", ErrEngineVersionDisabled, v.Version)
|
|
}
|
|
return v, nil
|
|
}
|
|
|
|
// CheckPatchCompatible verifies the requested target version stays
|
|
// inside the same major+minor line as `currentVersion`. Returns
|
|
// ErrPatchSemverIncompatible otherwise.
|
|
func CheckPatchCompatible(currentVersion, targetVersion string) error {
|
|
current, err := util.ParseSemver(currentVersion)
|
|
if err != nil {
|
|
return fmt.Errorf("%w: current version %q: %v", ErrInvalidInput, currentVersion, err)
|
|
}
|
|
target, err := util.ParseSemver(targetVersion)
|
|
if err != nil {
|
|
return fmt.Errorf("%w: target version %q: %v", ErrInvalidInput, targetVersion, err)
|
|
}
|
|
if current.Major != target.Major || current.Minor != target.Minor {
|
|
return fmt.Errorf("%w: %s -> %s", ErrPatchSemverIncompatible, currentVersion, targetVersion)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// IsKnownEngineVersion is a small helper used by tests and handlers.
|
|
func IsKnownEngineVersion(err error) bool {
|
|
return errors.Is(err, ErrEngineVersionDisabled) || errors.Is(err, ErrPatchSemverIncompatible)
|
|
}
|