controlplane

package
v1.1.6 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 10, 2026 License: MIT Imports: 13 Imported by: 0

Documentation

Overview

Package controlplane implements the shared control-plane core consumed by both the nSelf CLI and the nSelf Admin companion. It provides declarative multi-server inventory management, capability resolution, topology-aware deployment pipelines, and load-balancer lifecycle hooks.

Design invariants:

  • Secrets are never stored inline. SSHKeyRef holds an env-var NAME whose value is the path to the private key. Host holds user@host only.
  • Capability is derived from runtime probes, never from stored authority.
  • All file-system writes (inventory, state cache) are created mode 0600.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Migrate

func Migrate(inv *Inventory) error

Migrate upgrades inv in-place to currentSchemaVersion. It is idempotent: calling it multiple times on the same inventory produces the same result. Currently the only supported input version is 1 (same as current); future versions will add migration steps here.

func ValidateServerName

func ValidateServerName(name string) error

ValidateServerName returns an error when name contains characters that would be dangerous in a remote SSH argument position.

func Write

func Write(projectRoot string, inv *Inventory) error

Write persists inv to .nself/control-plane.yaml inside projectRoot. The file and its parent directory are created with mode 0600 / 0700 respectively if they do not already exist.

Types

type Capability

type Capability string

Capability describes the level of control the current host can exercise over a remote server, as determined by runtime probes.

const (
	// CapManage indicates full SSH + Docker access; deploy operations proceed.
	CapManage Capability = "manage"

	// CapReadOnly indicates the server is known but not fully reachable; the
	// pipeline reports a SKIPPED line for this server.
	CapReadOnly Capability = "read-only"

	// CapHidden indicates the server should not appear in normal output
	// (e.g. Host is empty, or not relevant to the current operation).
	CapHidden Capability = "hidden"
)

type DeployResult

type DeployResult struct {
	// Servers contains one entry per server in the resolved target set.
	Servers []ServerResult

	// PrimarySkipped is true when at least one primary app server was skipped
	// due to read-only capability. Callers should return a non-zero exit code.
	PrimarySkipped bool
}

DeployResult is the top-level result of a pipeline run.

func Run

func Run(ctx context.Context, inv *Inventory, prober Prober, composePath string) (*DeployResult, error)

Run executes the topology-aware deployment pipeline for the given inventory and compose file path.

Ordering (per §6 of the architecture spec):

  1. Build local environment once (kind == "local").
  2. Deploy observability servers.
  3. Deploy app servers in rolling fashion: if an LB is present for the env, drain the server, deploy, health-check, then re-add. Otherwise deploy directly.
  4. Reload LB config.

Servers with CapReadOnly are skipped with a SKIPPED log line. Servers with CapHidden are silently omitted. If a primary app server is skipped, DeployResult.PrimarySkipped is set.

The composePath argument is the local path to the generated docker-compose.yml produced by `nself build`. Run reuses deploy.DeployViaSsh for every remote server.

type DrainResult

type DrainResult int

DrainResult indicates the outcome of a Drain or Enable call.

const (
	// DrainOK means the helper ran and the command succeeded.
	DrainOK DrainResult = iota

	// DrainHelperAbsent means the helper binary was not found on the LB server
	// (SSH exit 127). Callers should log a WARN and degrade gracefully.
	DrainHelperAbsent

	// DrainFailed means the helper was found but returned a non-zero exit code.
	DrainFailed
)

func Drain

func Drain(ctx context.Context, srv Server, appName string) (DrainResult, error)

Drain removes appName from the active upstream pool on the LB server by running the remote nself-lb helper:

ssh ... <remote_path>/bin/nself-lb drain <appName>

The ctx controls the lifetime of the SSH subprocess; cancelling ctx or reaching its deadline aborts the in-flight connection.

If the helper binary is absent (SSH exit 127), Drain returns DrainHelperAbsent and a nil error so the caller can degrade gracefully (log WARN + continue). Other SSH failures return DrainFailed with a descriptive error.

func Enable

func Enable(ctx context.Context, srv Server, appName string) (DrainResult, error)

Enable re-adds appName to the active upstream pool on the LB server by running the remote nself-lb helper:

ssh ... <remote_path>/bin/nself-lb enable <appName>

The ctx controls the lifetime of the SSH subprocess. Same error semantics as Drain.

type Environment

type Environment struct {
	// Name is a well-known identifier: "local", "staging", "prod", or a
	// custom label defined by the operator.
	Name string `yaml:"name"`

	// Kind is "local" for loopback environments and "remote" for SSH-accessed
	// environments. Capability resolution uses this to short-circuit probes.
	Kind string `yaml:"kind"`

	// Servers is the ordered list of hosts in this environment.
	Servers []Server `yaml:"servers"`
}

Environment groups the servers that form a single deployment target. Kind distinguishes local environments (no SSH needed) from remote ones.

type Inventory

type Inventory struct {
	// SchemaVersion is incremented when the YAML structure changes in a
	// backward-incompatible way. Current supported version: 1.
	SchemaVersion int `yaml:"schema_version"`

	// Project is the canonical project name, matching the nSelf project
	// identifier (e.g. "nself", "ummat", "unity").
	Project string `yaml:"project"`

	// Environments maps environment name to its definition. Keys must be unique
	// and match the Environment.Name field of their value.
	Environments map[string]Environment `yaml:"environments"`
}

Inventory is the top-level declarative model persisted in .nself/control-plane.yaml (mode 0600). SchemaVersion enables forward-compatible migrations. Secrets are never inlined here.

func Load

func Load(projectRoot string) (*Inventory, error)

Load reads the control-plane inventory for the given project root.

If .nself/control-plane.yaml exists it is unmarshalled and Migrate is applied to bring it to the current schema version. If the file is absent, Load synthesizes a single-server inventory from NSELF_DEPLOY_HOST_<TARGET> environment variables so that legacy configurations continue to work without modification (back-compat guarantee).

type Prober

type Prober interface {
	// SSHReachable attempts an SSH connection to s and returns (true, nil) on
	// success or (false, err) with a descriptive error on failure. The err
	// value is used to populate TargetStatus.Reason; callers must not treat
	// non-nil err as fatal — it may indicate a transient network condition.
	SSHReachable(s Server) (reachable bool, latencyMS int, err error)

	// DockerOK runs "docker info" over SSH and returns true when the Docker
	// daemon responds. It is only called when SSHReachable returns true.
	DockerOK(s Server) (bool, error)

	// KeyState checks whether the key referenced by s.SSHKeyRef exists on the
	// local filesystem. It returns the resolved key path for logging.
	// KeyState must not perform any network I/O.
	KeyState(s Server) (present bool, path string)
}

Prober is the interface through which the resolver obtains runtime facts about a server. By injecting this interface the resolver itself contains zero os/exec calls, making it fully unit-testable without network access.

Implementations must be safe for concurrent use from multiple goroutines.

type SSHProber

type SSHProber struct {
	// contains filtered or unexported fields
}

SSHProber is the production implementation of Prober. It performs real SSH and docker probes, enforces a global concurrency limit, maintains a per-host mutex to prevent duplicate concurrent probes to the same host, and caches results to .nself/state/capability.json (0600) for probeCacheTTL.

func NewSSHProber

func NewSSHProber(projectRoot string, refresh bool) *SSHProber

NewSSHProber constructs a production Prober. projectRoot is the working directory (parent of .nself/). When refresh is true the on-disk cache is ignored for this run.

func (*SSHProber) DockerOK

func (sp *SSHProber) DockerOK(s Server) (bool, error)

DockerOK runs "docker info" over SSH to verify the Docker daemon is running on s. It is only called by the resolver when SSHReachable returned true.

func (*SSHProber) KeyState

func (sp *SSHProber) KeyState(s Server) (present bool, path string)

KeyState checks whether the SSH key referenced by s.SSHKeyRef exists on the local filesystem. It does not perform any network I/O.

func (*SSHProber) SSHReachable

func (sp *SSHProber) SSHReachable(s Server) (bool, int, error)

SSHReachable performs an SSH probe to s.Host using the key from s.SSHKeyRef. The exact argument vector is:

ssh -i <key> -o BatchMode=yes -o StrictHostKeyChecking=accept-new
    -o ConnectTimeout=5 -o ForwardAgent=no <user@host> "true"

Results are cached to .nself/state/capability.json for probeCacheTTL. The global semaphore limits concurrent probes to probeConcurrencyLimit. A per-host mutex prevents duplicate probes to the same host.

type Server

type Server struct {
	// Name is a human-readable identifier unique within the environment.
	Name string `yaml:"name"`

	// Role classifies the server's function (app, lb, observability, db, worker).
	Role ServerRole `yaml:"role"`

	// Host is the SSH target in user@host form. Empty string means local-only
	// (capability is always "manage" for local environments).
	Host string `yaml:"host,omitempty"`

	// SSHKeyRef is the name of an environment variable whose value holds the
	// absolute path to the SSH private key. Never the key path itself.
	SSHKeyRef string `yaml:"ssh_key_ref,omitempty"`

	// RemotePath is the absolute path on the remote host where the nSelf
	// stack is installed (e.g. /opt/nself). Used to locate nself-lb helper.
	RemotePath string `yaml:"remote_path,omitempty"`

	// Primary marks the authoritative app server. The pipeline returns a
	// non-zero exit code if this server is skipped due to read-only capability.
	Primary bool `yaml:"primary,omitempty"`

	// Upstreams lists the names of app-server backends this LB routes to.
	// Applicable only when Role == RoleLB.
	Upstreams []string `yaml:"upstreams,omitempty"`
}

Server describes a single remote (or local) host within an environment.

Host must be in user@host form. No key paths or passwords are stored here; SSHKeyRef holds the name of an environment variable whose value is the absolute path to the private key file.

type ServerResult

type ServerResult struct {
	// Env is the environment name.
	Env string

	// Server is the server name.
	Server string

	// Role is the server's role (used for ordering and reporting).
	Role ServerRole

	// Status is one of "ok", "skipped", "failed".
	Status string

	// Err is non-nil when Status == "failed".
	Err error

	// Primary marks whether this server is the primary app server.
	Primary bool
}

ServerResult records the outcome of deploying to one server.

type ServerRole

type ServerRole string

ServerRole classifies a server's function within an environment. The topology-aware pipeline uses roles to order operations correctly.

const (
	// RoleApp identifies a primary application server.
	RoleApp ServerRole = "app"
	// RoleLB identifies a load-balancer server.
	RoleLB ServerRole = "lb"
	// RoleObservability identifies a monitoring / observability server.
	RoleObservability ServerRole = "observability"
	// RoleDB identifies a dedicated database server.
	RoleDB ServerRole = "db"
	// RoleWorker identifies a background-worker server.
	RoleWorker ServerRole = "worker"
)

type TargetStatus

type TargetStatus struct {
	// Env is the environment name this status belongs to.
	Env string

	// Server is the server name within that environment.
	Server string

	// Capability is the resolved access level for this target.
	Capability Capability

	// SSHReachable is true when the SSH probe completed without error.
	SSHReachable bool

	// KeyPresent is true when the key file referenced by SSHKeyRef exists on
	// the local filesystem.
	KeyPresent bool

	// DockerOK is true when the docker-info probe over SSH succeeded.
	DockerOK bool

	// LatencyMS is the round-trip time of the SSH probe in milliseconds.
	// Zero if the probe was skipped (local environment, key absent, etc.).
	LatencyMS int

	// Reason provides the human-readable explanation for the assigned
	// Capability. Empty string means capability is CapManage (no caveat).
	Reason string

	// ProbedAt records when these probe results were obtained.
	ProbedAt time.Time
}

TargetStatus is the resolved state of a single server after the capability resolver has run. It is the unit of work for the deployment pipeline.

The Reason field uses the exact strings defined in §5.3 of the architecture spec so that callers can pattern-match without parsing free text.

func Resolve

func Resolve(inv *Inventory, p Prober) []TargetStatus

Resolve evaluates the capability of every server in inv by running probes through p and returns one TargetStatus per server.

The decision algorithm follows §5.1 of the architecture spec:

  1. Local kind env → CapManage, no probe.
  2. Host empty → CapHidden.
  3. SSHKeyRef unset → KeyPresent=false → CapReadOnly + reason "no SSH key for <ref>".
  4. KeyState present=false → CapReadOnly + reason "no SSH key for <ref>".
  5. SSHReachable → if false → CapReadOnly + reason "SSH unreachable (<err class>)".
  6. DockerOK → if false → CapReadOnly + reason "SSH ok, docker not reachable".
  7. Both SSH and Docker ok → CapManage.

Resolve never calls os/exec directly; all I/O is delegated to p.

Directories

Path Synopsis
Package sim provides a multi-server Docker sshd simulation harness for integration-testing the control-plane pipeline without touching live hosts.
Package sim provides a multi-server Docker sshd simulation harness for integration-testing the control-plane pipeline without touching live hosts.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL