// Package glm, "garage layout manager", implements the transition of garage // layout states based on the currently active layout and the desired layout // defined by the user. package glm import ( "context" "errors" "fmt" "io/fs" "isle/daemon/daecommon" "isle/garage" "isle/jsonutil" "isle/toolkit" "net/netip" "path/filepath" ) // GarageLayoutManager (GLM) tracks the currently active set of storage // allocations and calculates actions required to transition from the active set // into a target set. // // GLM will make sure that when allocations are removed they are properly // drained prior to being fully removed from the cluster. type GarageLayoutManager interface { // GetActiveAllocations returns the currently active set of allocations, as // recorded by the last call to CommitStateTransition. Returns an empty set // if CommitStateTransition has never been called. GetActiveAllocations(context.Context) ( []daecommon.ConfigStorageAllocation, error, ) // SetActiveAllocations overwrites the stored active allocations, if any, to // the given one. SetActiveAllocations(context.Context, []daecommon.ConfigStorageAllocation) error // Validate checks the target allocation set for any inconsistencies with // the currently active one, and returns an error if one is found. Validate( _ context.Context, targetAllocs []daecommon.ConfigStorageAllocation, ) error // CalculateStateTransition accepts a set of known nodes from an up-to-date // [garage.ClusterStatus] and the target allocation set for the host, and // returns a StateTransition describing the actions which should be taken. // // If the host is not running any garage instances, and therefore cannot // determine the known nodes, nil should be passed instead. CalculateStateTransition( _ context.Context, knownNodes []garage.KnownNode, targetAllocs []daecommon.ConfigStorageAllocation, ) ( StateTransition, error, ) // CommitStateTransition should be called after the CalculateStateTransition // returns a StateTransition, and the StateTransition's prescribed actions // have been successfully carried out. CommitStateTransition(context.Context, StateTransition) error } type garageLayoutManager struct { dir toolkit.Dir hostIP netip.Addr } // NewGarageLayoutManager initializes and returns a GarageLayoutManager which // will use the given directory to store state, and which is managing the layout // for the host with the given IP. func NewGarageLayoutManager( dir toolkit.Dir, hostIP netip.Addr, ) GarageLayoutManager { return &garageLayoutManager{dir, hostIP} } const glmStateFile = "glm.json" type glmState struct { ActiveAllocations []daecommon.ConfigStorageAllocation } func (glm *garageLayoutManager) get() (glmState, bool, error) { var ( path = filepath.Join(glm.dir.Path, glmStateFile) state glmState ) err := jsonutil.LoadFile(&state, path) if errors.Is(err, fs.ErrNotExist) { return glmState{}, false, nil } else if err != nil && !errors.Is(err, fs.ErrNotExist) { return glmState{}, false, err } return state, true, nil } func (glm *garageLayoutManager) set(state glmState) error { path := filepath.Join(glm.dir.Path, glmStateFile) return jsonutil.WriteFile(state, path, 0600) } func (glm *garageLayoutManager) GetActiveAllocations(context.Context) ( []daecommon.ConfigStorageAllocation, error, ) { state, _, err := glm.get() return state.ActiveAllocations, err } func (glm *garageLayoutManager) SetActiveAllocations( _ context.Context, allocs []daecommon.ConfigStorageAllocation, ) error { return glm.set(glmState{allocs}) } func (glm *garageLayoutManager) Validate( _ context.Context, targetAllocs []daecommon.ConfigStorageAllocation, ) error { state, ok, err := glm.get() if err != nil { return fmt.Errorf("reading state: %w", err) } else if !ok { // If there is no previously state then we assume the new state can't // conflict with it. return nil } return validateTargetAllocs( state.ActiveAllocations, targetAllocs, ) } func (glm *garageLayoutManager) CalculateStateTransition( _ context.Context, knownNodes []garage.KnownNode, targetAllocs []daecommon.ConfigStorageAllocation, ) ( StateTransition, error, ) { state, ok, err := glm.get() if err != nil { return StateTransition{}, fmt.Errorf("reading state: %w", err) } if ok { // If there is no previously state then we assume the new state can't // conflict with it. err = validateTargetAllocs(state.ActiveAllocations, targetAllocs) if err != nil { return StateTransition{}, fmt.Errorf( "validating target allocations: %w", err, ) } } allKnownNodes, knownNodes := knownNodes, knownNodes[:0] for _, node := range allKnownNodes { if node.Addr.Addr() == glm.hostIP { knownNodes = append(knownNodes, node) } } return calcStateTransition( state.ActiveAllocations, knownNodes, targetAllocs, ), nil } func (glm *garageLayoutManager) CommitStateTransition( _ context.Context, stateTransition StateTransition, ) error { return glm.set(glmState{ ActiveAllocations: stateTransition.ActiveAllocations(), }) }