179 lines
5.0 KiB
Go
179 lines
5.0 KiB
Go
// 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(),
|
|
})
|
|
}
|