Persist engine mounts across seal/unseal cycles
- Add Registry.UnsealAll() that rediscovers mounted engines from the barrier on unseal, using stored metadata at engine/_mounts/ with a fallback discovery scan for pre-existing mounts (migration path) - Registry.Mount() now persists mount metadata to the barrier; Registry.Unmount() cleans it up - Call UnsealAll() from both REST and web unseal handlers - Change Unmount() signature to accept context.Context - Default CA key size changed from P-384 to P-521 - Add build-time version stamp via ldflags; display in dashboard status bar - Make metacrypt target .PHONY so make devserver always rebuilds - Redirect /pki to /dashboard when no CA engine is mounted Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,8 +4,10 @@ package engine
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"git.wntrmute.dev/kyle/metacrypt/internal/barrier"
|
||||
@@ -96,6 +98,14 @@ func (r *Registry) RegisterFactory(t EngineType, f Factory) {
|
||||
r.factories[t] = f
|
||||
}
|
||||
|
||||
// mountMeta is persisted to the barrier so mounts survive seal/unseal cycles.
|
||||
type mountMeta struct {
|
||||
Name string `json:"name"`
|
||||
Type EngineType `json:"type"`
|
||||
}
|
||||
|
||||
const mountsPrefix = "engine/_mounts/"
|
||||
|
||||
// Mount creates and initializes a new engine mount.
|
||||
func (r *Registry) Mount(ctx context.Context, name string, engineType EngineType, config map[string]interface{}) error {
|
||||
r.mu.Lock()
|
||||
@@ -117,6 +127,15 @@ func (r *Registry) Mount(ctx context.Context, name string, engineType EngineType
|
||||
return fmt.Errorf("engine: initialize %q: %w", name, err)
|
||||
}
|
||||
|
||||
// Persist mount metadata so it can be reloaded after unseal.
|
||||
meta, err := json.Marshal(mountMeta{Name: name, Type: engineType})
|
||||
if err != nil {
|
||||
return fmt.Errorf("engine: marshal mount metadata: %w", err)
|
||||
}
|
||||
if err := r.barrier.Put(ctx, mountsPrefix+name+".json", meta); err != nil {
|
||||
return fmt.Errorf("engine: persist mount metadata: %w", err)
|
||||
}
|
||||
|
||||
r.mounts[name] = &Mount{
|
||||
Name: name,
|
||||
Type: engineType,
|
||||
@@ -151,7 +170,7 @@ func (r *Registry) GetMount(name string) (*Mount, error) {
|
||||
}
|
||||
|
||||
// Unmount removes and seals an engine mount.
|
||||
func (r *Registry) Unmount(name string) error {
|
||||
func (r *Registry) Unmount(ctx context.Context, name string) error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
@@ -164,10 +183,120 @@ func (r *Registry) Unmount(name string) error {
|
||||
return fmt.Errorf("engine: seal %q: %w", name, err)
|
||||
}
|
||||
|
||||
// Remove persisted mount metadata.
|
||||
if err := r.barrier.Delete(ctx, mountsPrefix+name+".json"); err != nil {
|
||||
return fmt.Errorf("engine: delete mount metadata %q: %w", name, err)
|
||||
}
|
||||
|
||||
delete(r.mounts, name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnsealAll discovers persisted mounts from the barrier and reloads them into
|
||||
// memory. It must be called after the barrier is unsealed.
|
||||
//
|
||||
// It first loads mounts recorded in engine/_mounts/, then does a discovery
|
||||
// pass over each registered engine type to pick up any mounts that predate
|
||||
// the metadata mechanism (one-time migration).
|
||||
func (r *Registry) UnsealAll(ctx context.Context) error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
// Load mounts from persisted metadata.
|
||||
if err := r.loadFromMetadata(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Discovery pass: scan each registered engine type for mounts that exist
|
||||
// in the barrier but have no metadata entry yet (pre-migration mounts).
|
||||
for engineType, factory := range r.factories {
|
||||
prefix := fmt.Sprintf("engine/%s/", engineType)
|
||||
paths, err := r.barrier.List(ctx, prefix)
|
||||
if err != nil {
|
||||
continue // no entries for this type
|
||||
}
|
||||
|
||||
// Extract unique mount names from paths like "myca/config.json".
|
||||
seen := make(map[string]bool)
|
||||
for _, p := range paths {
|
||||
parts := strings.SplitN(p, "/", 2)
|
||||
if len(parts) == 0 || parts[0] == "" || parts[0] == "_mounts" {
|
||||
continue
|
||||
}
|
||||
seen[parts[0]] = true
|
||||
}
|
||||
|
||||
for name := range seen {
|
||||
if _, exists := r.mounts[name]; exists {
|
||||
continue // already loaded
|
||||
}
|
||||
|
||||
eng := factory()
|
||||
mountPath := fmt.Sprintf("engine/%s/%s/", engineType, name)
|
||||
if err := eng.Unseal(ctx, r.barrier, mountPath); err != nil {
|
||||
return fmt.Errorf("engine: unseal (discovered) %q: %w", name, err)
|
||||
}
|
||||
|
||||
// Write metadata so future restarts don't need the discovery pass.
|
||||
meta, _ := json.Marshal(mountMeta{Name: name, Type: engineType})
|
||||
_ = r.barrier.Put(ctx, mountsPrefix+name+".json", meta)
|
||||
|
||||
r.mounts[name] = &Mount{
|
||||
Name: name,
|
||||
Type: engineType,
|
||||
MountPath: mountPath,
|
||||
Engine: eng,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadFromMetadata loads mounts recorded under engine/_mounts/. Caller must hold r.mu.
|
||||
func (r *Registry) loadFromMetadata(ctx context.Context) error {
|
||||
paths, err := r.barrier.List(ctx, mountsPrefix)
|
||||
if err != nil {
|
||||
return nil // no metadata written yet
|
||||
}
|
||||
|
||||
for _, p := range paths {
|
||||
if !strings.HasSuffix(p, ".json") {
|
||||
continue
|
||||
}
|
||||
|
||||
data, err := r.barrier.Get(ctx, mountsPrefix+p)
|
||||
if err != nil {
|
||||
return fmt.Errorf("engine: read mount metadata %q: %w", p, err)
|
||||
}
|
||||
|
||||
var meta mountMeta
|
||||
if err := json.Unmarshal(data, &meta); err != nil {
|
||||
return fmt.Errorf("engine: parse mount metadata %q: %w", p, err)
|
||||
}
|
||||
|
||||
factory, ok := r.factories[meta.Type]
|
||||
if !ok {
|
||||
return fmt.Errorf("%w: %s (mount %q)", ErrUnknownType, meta.Type, meta.Name)
|
||||
}
|
||||
|
||||
eng := factory()
|
||||
mountPath := fmt.Sprintf("engine/%s/%s/", meta.Type, meta.Name)
|
||||
if err := eng.Unseal(ctx, r.barrier, mountPath); err != nil {
|
||||
return fmt.Errorf("engine: unseal %q: %w", meta.Name, err)
|
||||
}
|
||||
|
||||
r.mounts[meta.Name] = &Mount{
|
||||
Name: meta.Name,
|
||||
Type: meta.Type,
|
||||
MountPath: mountPath,
|
||||
Engine: eng,
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListMounts returns all current mounts.
|
||||
func (r *Registry) ListMounts() []Mount {
|
||||
r.mu.RLock()
|
||||
|
||||
Reference in New Issue
Block a user