173 lines · 5.9 KB · Go
RawDownload
package box import (	"context"	"errors"	"fmt" 	dockernetwork "github.com/docker/docker/api/types/network"	dockerclient "github.com/docker/docker/client" 	"github.com/davis7dotsh/tx9/internal/docker"	"github.com/davis7dotsh/tx9/internal/names") // ErrObjectsExist identifies a fresh box name that collides with Docker objects.var ErrObjectsExist = errors.New("box objects already exist") // PreflightFreshObjects rejects names whose containers have been removed but// whose durable volumes or network remain. Fresh create/import must call this// under the box lock, before writing a new token or changing Docker objects.// Upgrade intentionally reuses owned objects through ensureNetwork/Volume.func PreflightFreshObjects(ctx context.Context, cli *docker.Client, name string) error {	if err := names.Validate(name); err != nil {		return err	}	agent, executor := ContainerNames(name)	for _, container := range []string{agent, executor} {		if _, err := cli.ContainerInspect(ctx, container); err == nil {			return existingObjectError(name, "container", container)		} else if !dockerclient.IsErrNotFound(err) {			return err		}	}	network := NetworkName(name)	if _, err := cli.Raw().NetworkInspect(ctx, network, dockernetwork.InspectOptions{}); err == nil {		return existingObjectError(name, "network", network)	} else if !dockerclient.IsErrNotFound(err) {		return fmt.Errorf("inspect network %s: %w", network, err)	}	agentVolume, executorVolume := VolumeNames(name)	for _, volume := range []string{agentVolume, executorVolume} {		if _, err := cli.Raw().VolumeInspect(ctx, volume); err == nil {			return existingObjectError(name, "volume", volume)		} else if !dockerclient.IsErrNotFound(err) {			return fmt.Errorf("inspect volume %s: %w", volume, err)		}	}	return nil} func existingObjectError(box, kind, name string) error {	return fmt.Errorf("box %q already has Docker %s %q; choose a different box name or recover/remove the existing resources explicitly: %w", box, kind, name, ErrObjectsExist)} // PreflightExistingObjects verifies ownership of every existing object and// returns all container IDs upgrade must remove before recreating the box.// Missing objects remain recreatable. Callers must hold the box lock through// teardown and remove this complete set, including duplicate or unknown roles.func PreflightExistingObjects(ctx context.Context, cli *docker.Client, name string) ([]string, error) {	targets, err := inspectDestroyTargets(ctx, cli, name)	if err != nil {		return nil, err	}	return targets.containerIDs, nil} func ownedByBox(labels map[string]string, name string) bool {	return labels[docker.LabelManaged] == "1" && labels[docker.LabelBox] == name} type destroyTargets struct {	containerIDs []string	networkID    string	volumes      []string} // Inspect every deletion target before allowing any mutation. Derived names// are checked even when labels find a container, so a foreign sibling cannot// be missed in a partially created box. Labels also find renamed containers.func inspectDestroyTargets(ctx context.Context, cli *docker.Client, name string) (destroyTargets, error) {	if err := names.Validate(name); err != nil {		return destroyTargets{}, err	}	agent, executor := ContainerNames(name)	containerRefs := []string{agent, executor}	containers, err := cli.ListBoxContainers(ctx)	if err != nil {		return destroyTargets{}, err	}	for _, container := range containers {		if ownedByBox(container.Labels, name) {			containerRefs = append(containerRefs, container.ID)		}	}	var targets destroyTargets	seen := make(map[string]bool)	for _, ref := range containerRefs {		if ref == "" || seen[ref] {			continue		}		id, err := inspectOwnedContainer(ctx, cli, ref, name)		if err != nil {			return destroyTargets{}, err		}		if id != "" && !seen[id] {			targets.containerIDs = append(targets.containerIDs, id)			seen[id] = true		}	}	targets.networkID, err = inspectOwnedNetwork(ctx, cli, NetworkName(name), name)	if err != nil {		return destroyTargets{}, err	}	agentVolume, executorVolume := VolumeNames(name)	for _, ref := range []string{agentVolume, executorVolume} {		volume, err := inspectOwnedVolume(ctx, cli, ref, name)		if err != nil {			return destroyTargets{}, err		}		if volume != "" {			targets.volumes = append(targets.volumes, volume)		}	}	return targets, nil} func inspectOwnedContainer(ctx context.Context, cli *docker.Client, id, name string) (string, error) {	inspect, err := cli.ContainerInspect(ctx, id)	if dockerclient.IsErrNotFound(err) {		return "", nil	}	if err != nil {		return "", err	}	if inspect.Config == nil || !ownedByBox(inspect.Config.Labels, name) {		return "", fmt.Errorf("refusing to use container %s: it is not owned by box %s", id, name)	}	if inspect.ContainerJSONBase == nil || inspect.ID == "" {		return "", fmt.Errorf("inspect container %s: missing container ID", id)	}	return inspect.ID, nil} func inspectOwnedNetwork(ctx context.Context, cli *docker.Client, network, name string) (string, error) {	inspect, err := cli.Raw().NetworkInspect(ctx, network, dockernetwork.InspectOptions{})	if dockerclient.IsErrNotFound(err) {		return "", nil	}	if err != nil {		return "", fmt.Errorf("inspect network %s: %w", network, err)	}	if !ownedByBox(inspect.Labels, name) {		return "", fmt.Errorf("refusing to use network %s: it is not owned by box %s", network, name)	}	if inspect.ID == "" {		return "", fmt.Errorf("inspect network %s: missing network ID", network)	}	return inspect.ID, nil} func inspectOwnedVolume(ctx context.Context, cli *docker.Client, volume, name string) (string, error) {	inspect, err := cli.Raw().VolumeInspect(ctx, volume)	if dockerclient.IsErrNotFound(err) {		return "", nil	}	if err != nil {		return "", fmt.Errorf("inspect volume %s: %w", volume, err)	}	if !ownedByBox(inspect.Labels, name) {		return "", fmt.Errorf("refusing to use volume %s: it is not owned by box %s", volume, name)	}	return volume, nil}