Files
kandev/apps/backend/internal/agent/managedruntime/selection.go
T

118 lines
3.8 KiB
Go

package managedruntime
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
)
var (
ErrInvalidSelection = errors.New("invalid managed runtime selection")
errSettingsMissing = errors.New("managed runtime settings store is unavailable")
)
const selectionKeyPrefix = "managed_runtime.active."
// SettingsStore is the small persistence seam required by the install-wide
// managed-runtime selection store.
type SettingsStore interface {
Get(context.Context, string) ([]byte, bool, error)
Save(context.Context, string, []byte) error
Delete(context.Context, string) error
}
// SelectionStore is the runtime-facing seam for reading and persisting an
// active managed-runtime selection.
type SelectionStore interface {
Get(context.Context, string, string) (Selection, bool, error)
Save(context.Context, string, string, string) error
Delete(context.Context, string, string) error
}
// SelectionReader is the read-only seam used by runtime command builders.
type SelectionReader interface {
Get(context.Context, string, string) (Selection, bool, error)
}
// Selection is the trusted package identity and exact active version for one
// built-in agent.
type Selection struct {
Package string `json:"package"`
Version string `json:"version"`
}
type Store struct {
settings SettingsStore
}
// NewStore creates the install-wide active-version store.
func NewStore(settings SettingsStore) *Store {
return &Store{settings: settings}
}
// Get returns the selection only when it belongs to the package currently
// trusted for the agent. A package replacement therefore starts unselected.
func (s *Store) Get(ctx context.Context, agentID, packageName string) (Selection, bool, error) {
if s == nil || s.settings == nil {
return Selection{}, false, errSettingsMissing
}
if agentID == "" || packageName == "" {
return Selection{}, false, fmt.Errorf("%w: agent and package are required", ErrInvalidSelection)
}
raw, found, err := s.settings.Get(ctx, selectionKey(agentID))
if err != nil || !found {
return Selection{}, found, err
}
var selection Selection
if err := json.Unmarshal(raw, &selection); err != nil {
return Selection{}, false, fmt.Errorf("%w: decode: %v", ErrInvalidSelection, err)
}
if selection.Package != packageName {
return Selection{}, false, nil
}
if selection.Version == "" {
return Selection{}, false, fmt.Errorf("%w: version is empty", ErrInvalidSelection)
}
if _, err := ParseStableVersion(selection.Version); err != nil {
return Selection{}, false, fmt.Errorf("%w: %v", ErrInvalidSelection, err)
}
return selection, true, nil
}
// Save persists a validated exact version for one trusted package.
func (s *Store) Save(ctx context.Context, agentID, packageName, version string) error {
if s == nil || s.settings == nil {
return errSettingsMissing
}
if agentID == "" || packageName == "" {
return fmt.Errorf("%w: agent and package are required", ErrInvalidSelection)
}
if _, err := ParseStableVersion(version); err != nil {
return fmt.Errorf("%w: %v", ErrInvalidSelection, err)
}
raw, err := json.Marshal(Selection{Package: packageName, Version: version})
if err != nil {
return fmt.Errorf("marshal managed runtime selection: %w", err)
}
return s.settings.Save(ctx, selectionKey(agentID), raw)
}
// Delete removes the operator selection for one trusted package. The built-in
// default remains outside the settings store and becomes effective after this
// operation succeeds.
func (s *Store) Delete(ctx context.Context, agentID, packageName string) error {
if s == nil || s.settings == nil {
return errSettingsMissing
}
if agentID == "" || packageName == "" {
return fmt.Errorf("%w: agent and package are required", ErrInvalidSelection)
}
return s.settings.Delete(ctx, selectionKey(agentID))
}
func selectionKey(agentID string) string {
return selectionKeyPrefix + strings.TrimSpace(agentID)
}