devconfig

package
v0.1.0-rc.5 Latest Latest
Warning

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

Go to latest
Published: Sep 1, 2026 License: Apache-2.0 Imports: 9 Imported by: 0

Documentation

Overview

Package devconfig owns w17ctl's dev-machine-local state — `~/.w17/config.yaml`. This is the THIRD config home in the w17 model, alongside:

  • proto — code-aware (WHAT gets generated)
  • w17/lock.yaml — code-agnostic, SHARED + signed (reproducible codegen across the team)
  • ~/.w17/config.yaml — dev-machine-LOCAL (this package)

It holds the registry of installed projects, the unique host-port assignments w17ctl hands each project (so two projects' exposed ports never collide on one machine), and per-project run presets.

None of this belongs in the signed lock: port maps must differ machine-to-machine (that is the whole point), and a "run only the admin binary on my weak laptop" preset is a personal-machine concern, not a shared, reproducible build input.

Index

Constants

View Source
const (
	// BindLoopback binds 127.0.0.1: ports reach only the local box (or, in
	// remote mode, only through the SSH tunnel). The secure default.
	BindLoopback = "loopback"
	// BindPublic binds 0.0.0.0: ports reach any interface — on a remote
	// host with a public IP that means the internet (subject to firewall).
	BindPublic = "public"
)

Bind modes — the host interface docker publishes ports on.

View Source
const DefaultPortBase = 14000

DefaultPortBase is where w17ctl starts allocating host ports when the config does not pin its own port_base. Chosen well above the conventional dialect/gateway defaults (5432 / 6379 / 3306 / 8080 / 16686 / 4222) so a freshly-allocated stack never lands on a port a hand-run `docker compose` would also pick. The 13xxx band is deliberately left free for hand-run localhost services (e.g. the dockerized console on :13000) — allocation only ever climbs from 14000 upward, so it never treads on that reserved band.

Variables

This section is empty.

Functions

func BindHost

func BindHost(bind string) string

BindHost maps an EXPLICIT bind mode to the docker host-publish interface — "public" → 0.0.0.0, "loopback" → 127.0.0.1. Used for the `stack bind` confirmation message; the unset ("") default is mode-aware, so use ResolveBindHost when a project has not pinned a bind.

func BuildUpEnv

func BuildUpEnv(p *Project, preset *Preset) []string

BuildUpEnv merges a project's managed host-port assignments with a preset's extra env into a sorted KEY=VALUE list for the compose subprocess. Preset env wins on conflict (it is layered last).

This is the single source of truth for "what env does `stack up` / the UI inject" — both the w17ctl CLI and the local UI call it, so a human clicking the UI and an agent driving the CLI produce identical compose invocations.

func DefaultDir

func DefaultDir() (string, error)

DefaultDir is the w17ctl home — `$W17_HOME` if set, else `$HOME/.w17`. The w17ctl binary itself lives here (~/.w17/w17ctl).

func DefaultPath

func DefaultPath() (string, error)

DefaultPath is the config file path — `<DefaultDir>/config.yaml`.

func ParseBind

func ParseBind(s string) (string, error)

ParseBind validates a bind mode. "" is the loopback default.

func PortInUse

func PortInUse(port int) bool

PortInUse exposes the shared OS port probe to callers outside the package (the `stack up` preflight). Kept as a thin wrapper over the same package var the allocator uses so both see identical behaviour (and the same test stub).

func PresetServices

func PresetServices(explicit []string, preset *Preset) []string

PresetServices picks the services to bring up: an explicit list wins; otherwise the preset's selection; otherwise nil (the whole stack).

func ResolveBindHost

func ResolveBindHost(mode Mode, bind string) string

ResolveBindHost is the mode-aware host-publish interface w17ctl injects as W17_BIND_HOST at `stack up`. An explicit bind wins; when unset the default depends on the mode:

  • remote → 127.0.0.1 (secure: the stack runs on a shared server, and the only consumer is the SSH tunnel, which dials loopback);
  • local → 0.0.0.0 (historical: local container consumers — e2e test containers via host.docker.internal, LAN devices — need it, and a laptop behind NAT is not internet-exposed).

The generated compose defaults W17_BIND_HOST to 0.0.0.0, so a raw `docker compose up` (e.g. the e2e harness) keeps the historical bind; the loopback security on remote comes from w17ctl always injecting it.

func Save

func Save(path string, c *Config) error

Save writes the config to path atomically (temp file + rename), creating the parent directory if needed.

func SaveDefault

func SaveDefault(c *Config) error

SaveDefault writes to DefaultPath.

Types

type Config

type Config struct {
	// Version is the schema version (1 today). Forward-compat: an
	// unknown future version still loads — fields we don't know are
	// preserved on save only if round-tripped, which we do not do;
	// keep additions backward-compatible.
	Version int `yaml:"version"`

	// PortBase overrides DefaultPortBase for this machine. Zero =
	// DefaultPortBase.
	PortBase int `yaml:"port_base,omitempty"`

	// Projects is the registry, keyed by project name (lock.project).
	Projects map[string]*Project `yaml:"projects,omitempty"`

	// Remotes are named remote docker hosts, keyed by a user-chosen name.
	// Populated by `stack remote add`; consumed in remote mode to run the
	// docker stack on a beefy SSH-reachable box (see
	// docs/experiments/remote-stack.md). Empty = local-only, the default.
	Remotes map[string]*Remote `yaml:"remotes,omitempty"`

	// DefaultMode is the global default execution mode when a project does
	// not pin its own: "local" (default) or "remote". Empty == local.
	DefaultMode string `yaml:"default_mode,omitempty"`

	// DefaultRemote is the Remotes key used in remote mode when a project
	// does not pin one. Empty == no default (an explicit pin/flag is then
	// required to resolve a remote).
	DefaultRemote string `yaml:"default_remote,omitempty"`
}

Config is the whole `~/.w17/config.yaml` document.

func Load

func Load(path string) (*Config, error)

Load reads the config at path. A missing file is NOT an error — it returns a fresh, empty config (first run). The returned config has non-nil Projects and a resolved Version so callers can mutate it directly.

func LoadDefault

func LoadDefault() (*Config, error)

LoadDefault loads from DefaultPath.

func (*Config) AllocatePorts

func (c *Config) AllocatePorts(name string, slots []Slot) bool

AllocatePorts ensures every slot in `slots` has a unique host port assigned to the named project, allocating new ones from the config's port base upward while never colliding with a port already handed to ANY registered project. It is idempotent: an existing slot keeps its port across runs (so a project's gateway stays on the same host port even as other slots are added), and slots no longer present are pruned. Returns true when the project's Ports map changed.

Beyond avoiding cross-project assignments in this config, it also skips any port currently bound on the OS (via [portInUse]) so a fresh slot never lands on a port a hand-run service (e.g. the dockerized console) happens to hold right now. The probe is best-effort and only consulted when picking a NEW port — existing sticky assignments are never disturbed.

func (*Config) DeleteRemote

func (c *Config) DeleteRemote(name string) bool

DeleteRemote removes the named remote and cascades: it clears DefaultRemote when it pointed at the removed one, and clears every project's Remote pin that referenced it (so nothing dangles at a now-unknown remote). Returns false — changing nothing — if no such remote exists.

func (*Config) EffectivePortBase

func (c *Config) EffectivePortBase() int

EffectivePortBase returns PortBase or DefaultPortBase.

func (*Config) FindByPath

func (c *Config) FindByPath(absPath string) (string, *Project)

FindByPath returns the registered project (and its name) whose Path matches the given absolute path, or "", nil if none.

func (*Config) ResolveMode

func (c *Config) ResolveMode(absPath, flag string) (Mode, error)

ResolveMode walks the resolution chain for the stack execution mode of the project at absPath, mirroring core.ResolveConsoleAddr. Precedence (first non-empty wins):

  1. flag — kong's --mode value (the W17_STACK_MODE env var is folded in by kong's `env` tag, so this single argument covers both flag+env).
  2. the project's pinned Mode (`stack use-local` / `stack use-remote`).
  3. the global DefaultMode.
  4. local — the bottom default.

Every source is validated; an invalid value anywhere is a hard error rather than a silent fall-through, so a typo in config surfaces.

func (*Config) ResolveRemote

func (c *Config) ResolveRemote(absPath, flag string) (string, *Remote, error)

ResolveRemote resolves which registered remote the project at absPath uses in remote mode. Precedence (first non-empty wins): flag → the project's pinned Remote → DefaultRemote. The resolved name must exist in Remotes; an empty resolution or an unknown name is an actionable error.

func (*Config) SetRemote

func (c *Config) SetRemote(name string, r *Remote)

SetRemote stores (or replaces) a named remote, allocating the Remotes map on first use — mirrors Project.SetPreset.

type Mode

type Mode string

Mode is a stack execution mode — where `w17ctl stack` builds and runs the docker stack.

const (
	// ModeLocal runs the docker stack on the local machine — the default,
	// historical behaviour.
	ModeLocal Mode = "local"

	// ModeRemote runs the docker stack on a registered remote docker host
	// over SSH, tunneling each published port back to the identical
	// localhost port so the dev experience is unchanged
	// (docs/experiments/remote-stack.md).
	ModeRemote Mode = "remote"
)

func ParseMode

func ParseMode(s string) (Mode, error)

ParseMode validates a mode string as it appears in config or on a flag. The empty string is a valid "unset" (callers treat it as "fall through to the next resolution source"); any value other than "", "local", or "remote" is rejected.

type Preset

type Preset struct {
	// Services is the list of compose service names to bring up. Empty
	// = the whole stack.
	Services []string `yaml:"services,omitempty"`

	// Env is extra environment layered onto the compose subprocess on
	// top of the managed port vars (e.g. W17_OTEL_ENDPOINT="" to skip
	// the tracing backend on a weak machine). A preset CAN override a
	// managed port var here, but that is rarely wanted.
	Env map[string]string `yaml:"env,omitempty"`
}

Preset is a named run profile: a subset of services to start plus extra env overrides layered on top of the w17ctl-managed port env.

type Project

type Project struct {
	// Path is the absolute filesystem path to the project root (the
	// directory holding w17/lock.yaml).
	Path string `yaml:"path"`

	// Ports maps a port-slot key (the override env-var name, e.g.
	// SHOP_GATEWAY_HOST_PORT) to the unique host port w17ctl assigned
	// it. Rewritten into the compose subprocess env on `stack up`.
	Ports map[string]int `yaml:"ports,omitempty"`

	// ActivePreset is the preset applied by a bare `stack up` (empty =
	// full stack, no env overlay, all services).
	ActivePreset string `yaml:"active_preset,omitempty"`

	// Presets are named run profiles for this project.
	Presets map[string]*Preset `yaml:"presets,omitempty"`

	// Autosync is this dev's local override of the project's
	// dev-DB workflow mode (docs/specs/storage/dev-db-lifecycle.md
	// §workflow modes). nil = no override (follow the lock's project
	// default); non-nil wins over the lock. true = Mode A (autosync —
	// the initiative follows the git branch, with auto snapshot/restore),
	// false = Mode B (manual — active initiative set via `initiative
	// activate`).
	Autosync *bool `yaml:"autosync,omitempty"`

	// ActiveInitiative is the dev's currently-active initiative in manual
	// (autosync-off) mode — the persistent pointer `initiative activate`
	// sets, which `stack build` + `db snapshot` then scope to. Empty =
	// the implicit "default" initiative. Ignored in autosync mode (the
	// git branch drives the initiative there).
	ActiveInitiative string `yaml:"active_initiative,omitempty"`

	// Mode pins this project's stack execution mode, winning over the
	// global DefaultMode. "" = follow DefaultMode; "local" | "remote".
	// Set by `stack use-local` / `stack use-remote`.
	Mode string `yaml:"mode,omitempty"`

	// Remote pins which registered remote this project uses in remote
	// mode, winning over DefaultRemote. "" = follow DefaultRemote; else a
	// Remotes key.
	Remote string `yaml:"remote,omitempty"`

	// Bind is the host interface docker publishes this project's ports on,
	// injected as W17_BIND_HOST into the compose env (like the port map).
	// "" = loopback (127.0.0.1, secure default) | "loopback" | "public"
	// (0.0.0.0 — reachable off-box). Set by `stack bind`. It is a
	// dev-machine runtime concern (not a codegen input), so it lives here
	// next to the ports, NOT in the lock.
	Bind string `yaml:"bind,omitempty"`

	// PublicAck records that the developer has acknowledged (once, by
	// typing the confirmation phrase) that this project's containers are
	// exposed to the network — the guard for the dangerous public+remote
	// combination. Reset whenever Bind leaves "public".
	PublicAck bool `yaml:"public_ack,omitempty"`
}

Project is one registered project's local state.

func (*Project) Activate

func (p *Project) Activate(name string) bool

Activate marks the named preset as the one a bare `stack up` applies. Returns false — changing nothing — if no such preset exists. Pass "" via ClearActivePreset to reset to the full stack.

func (*Project) ClearActivePreset

func (p *Project) ClearActivePreset()

ClearActivePreset resets the project to the full stack (no active preset) — what a bare `stack up` runs.

func (*Project) DeletePreset

func (p *Project) DeletePreset(name string) bool

DeletePreset removes the named preset and clears ActivePreset when it pointed at the removed one (a removed active preset falls back to the full stack). Returns false — changing nothing — if no such preset exists.

func (*Project) HasPreset

func (p *Project) HasPreset(name string) bool

HasPreset reports whether the project has a run preset by that name.

func (*Project) SetPreset

func (p *Project) SetPreset(name string, preset *Preset)

SetPreset stores (or replaces) the named run preset, allocating the Presets map on first use.

type Remote

type Remote struct {
	// SSH is the destination passed to ssh/rsync: user@host[:port]. An
	// ssh_config alias (a plain host name) is fine.
	SSH string `yaml:"ssh"`

	// Path is the base directory on the server; each project gets a
	// subdir <Path>/<project> that w17ctl rsyncs the tree into.
	Path string `yaml:"path"`
}

Remote is one registered remote docker host — the SSH-reachable box w17ctl offloads the docker stack to in remote mode. The server stays dumb (dockerd + sshd only); all orchestration runs from w17ctl.

type Slot

type Slot struct {
	// Key is the stable slot identifier — equal to EnvVar, so a slot's
	// assigned port survives across re-introspection.
	Key string

	// EnvVar is the environment variable that overrides the host port
	// (e.g. SHOP_GATEWAY_HOST_PORT). w17ctl sets this in the compose
	// subprocess env on `stack up`.
	EnvVar string

	// Default is the compose default (the `:-N` value), 0 if the
	// mapping used `${VAR}` with no default.
	Default int

	// Container is the in-container port the host port maps to (for
	// display only).
	Container int

	// Service is the compose service name the port belongs to.
	Service string

	// Source is the project-relative compose file the slot came from.
	Source string
}

Slot is one host-published, w17ctl-overridable port found in the project's rendered compose tree. The override knob is an env var of the form `${VAR:-default}` in a service's `ports:` mapping — that is the single source of truth for "which ports can w17ctl rewrite".

func IntrospectSlots

func IntrospectSlots(root, servicesDir string) ([]Slot, []string, error)

IntrospectSlots scans a project's rendered compose tree for managed host-port slots and the full set of compose service names. It reads `<root>/compose.w17.yaml` (infra: DBs, NATS, tracing) plus every `<root>/<servicesDir>/*/compose.yaml` (the per-bundle binaries).

Best-effort: a compose file that is missing or unparsable is skipped rather than failing the scan — slots only exist once codegen has run, and a half-generated tree should still let the rest be managed. Returns slots sorted by (Source, Service, EnvVar) and service names sorted alphabetically.

Jump to

Keyboard shortcuts

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