~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
145 lines · 4.5 KB Raw History
package providermain

import (
	"context"
	"errors"
	"fmt"
	"io/fs"
	"log/slog"
	"os"
	"path/filepath"
	"slices"

	"github.com/johnandersen777/socialweb-computer-kcp/internal/clusterprovider"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/portallocator"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/subnetallocator"
)

type legacyKeys struct {
	StateDir string
	Ports    *portallocator.Allocator
	Networks *subnetallocator.Allocator
	Log      *slog.Logger
}

func migrateLegacyKeys(ctx context.Context, registry clusterprovider.Clusters, d legacyKeys) error {
	if d.Log == nil {
		d.Log = slog.Default()
	}
	refs, err := registry.Refs(ctx)
	if err != nil {
		return fmt.Errorf(
			"clusterprovider: listing the Cluster CRs before the legacy-key migration: %w. The "+
				"migration renames a state dir, a node dir and a mint credential from the bare "+
				"cluster name to the cluster's own key, and a list that failed is not an empty "+
				"tree: it would leave every legacy directory unclaimed and every later pass "+
				"reading an empty state dir over a cluster that is running", err)
	}

	byName := map[string][]clusterprovider.Ref{}
	for _, ref := range refs {
		byName[ref.Name] = append(byName[ref.Name], ref)
	}
	names := make([]string, 0, len(byName))
	for name := range byName {
		names = append(names, name)
	}
	slices.Sort(names)

	for _, name := range names {
		group := byName[name]
		if len(group) != 1 {
			homes := make([]string, 0, len(group))
			for _, ref := range group {
				homes = append(homes, ref.Home)
			}
			d.Log.Warn("cluster provider: two or more home workspaces hold a Cluster with this "+
				"name, so the legacy directories named by it belong to no one cluster and are left "+
				"where they are",
				"cluster", name, "homes", homes)
			continue
		}
		key, err := group[0].Key()
		if err != nil {
			return err
		}
		if err := migrateOne(d, name, key); err != nil {
			return err
		}
	}
	return nil
}

func migrateOne(d legacyKeys, name, key string) error {
	if name == key {
		return nil
	}
	instance, err := clusterprovider.InstanceName(name)
	if err != nil {
		return err
	}
	instanceKey, err := clusterprovider.InstanceName(key)
	if err != nil {
		return err
	}
	for _, move := range []struct{ parent, from, to string }{
		{d.StateDir, instance, instanceKey},
		{d.StateDir, name, key},
	} {
		if move.parent == "" || move.from == move.to {
			continue
		}
		if err := renameLegacy(d.Log, move.parent, move.from, move.to, name, key); err != nil {
			return err
		}
	}
	if d.Ports != nil {
		moved, err := d.Ports.Rekey(name, key)
		if err != nil {
			return fmt.Errorf("clusterprovider: re-keying the port leases of %s to %s: %w", name, key, err)
		}
		if moved > 0 {
			d.Log.Info("cluster provider: re-keyed the port leases of a legacy cluster name",
				"cluster", name, "key", key, "leases", moved)
		}
	}
	if d.Networks != nil {
		moved, err := d.Networks.Rekey(name, key)
		if err != nil {
			return fmt.Errorf("clusterprovider: re-keying the subnet lease of %s to %s: %w", name, key, err)
		}
		if moved > 0 {
			d.Log.Info("cluster provider: re-keyed the subnet lease of a legacy cluster name",
				"cluster", name, "key", key, "leases", moved)
		}
	}
	return nil
}

func renameLegacy(log *slog.Logger, parent, from, to, name, key string) error {
	fromPath := filepath.Join(parent, from)
	toPath := filepath.Join(parent, to)
	if _, err := os.Stat(fromPath); err != nil {
		if errors.Is(err, fs.ErrNotExist) {
			return nil
		}
		return fmt.Errorf("clusterprovider: reading %s before the legacy-key migration: %w", fromPath, err)
	}
	if _, err := os.Stat(toPath); err == nil {
		log.Warn("cluster provider: a legacy directory and this cluster's keyed directory both "+
			"exist, so the legacy one is left exactly where it is and nothing is merged into the "+
			"keyed one",
			"cluster", name, "key", key, "legacy", fromPath, "keyed", toPath)
		return nil
	}
	if err := os.Rename(fromPath, toPath); err != nil {
		return fmt.Errorf("clusterprovider: renaming %s to %s: %w. The cluster's identity inside "+
			"this provider is the key, and a state dir still named by the bare cluster name is one "+
			"no later pass finds: the node records, the instance token and the identity bundle "+
			"would all read as absent, and the first pass would provision over a cluster that is "+
			"already running", fromPath, toPath, err)
	}
	log.Info("cluster provider: migrated a directory named by a bare cluster name to this "+
		"cluster's key", "cluster", name, "key", key, "from", fromPath, "to", toPath)
	return nil
}