Skip to content

feat(iaci): environment axis — env-scoped overlay orthogonal to profile (--environment / defaults.environment) #60

Description

@Zorlin

Problem

profile is being misused to select environments. In riffcc/infra, the prod and test k3s clusters share ONE site — same inventory (labs/london), same base secrets (../infra-secrets/london) — differing only by a cluster-identity var (k3s_group_prefix). That's the environment axis, not the site/context axis. But Jetpack's only inventory+secrets preset is profile, so to pick test without -e we wrote profiles: { test: { … } } — which is semantically wrong: profile's own tests (config_file.rs:281-308) use london and ci (a site and a CI context), not env names. There is no first-class way to say "this site, but the test environment."

The consequence: the moment a second site or a staging env appears, the model collapses — london-test, london-staging, nyc-test… all profiles, with site and env mashed into one token.

Proposed

A native environment axis, orthogonal to profile:

  • profile = location/site/context → selects inventory + secrets_inventory (unchanged).
  • environment = env-within (prod/test/staging) → layers an env-scoped vars overlay on top of whatever the profile selected, selected independently.

Selecting an env appends its overlay paths to the secrets load list (loaded last → later-wins, loading.rs:135), so an env can pin any per-env var (e.g. k3s_group_prefix: test-k8s) without touching the site inventory. --no-secrets skips it like any other secrets path.

Contract shape

version: 1
defaults:
  playbook: playbooks/k3s/main.yml
  inventory: [labs/london]
  secrets_inventory: [../infra-secrets/london]
  environment: prod            # default env (optional)
  roles: [roles]
environments:                  # env → vars-only overlay paths, loaded after the base
  test:
    secrets_inventory: [../infra-secrets/london-test]
jetpack apply                              # prod (default env, no overlay)
jetpack apply --environment test           # london inv+secrets + london-test overlay
jetpack apply --profile nyc --environment test   # orthogonal: nyc site + test env

Implementation anchors

Schema — src/cli/config_file.rs:

  • JetpackFileConfig (:44-58): add environments: Option<BTreeMap<String, JetpackEnvironment>> beside profiles (:54).
  • JetpackDefaults (:60-71): add environment: Option<String> beside profile (:70).
  • New JetpackEnvironment { secrets_inventory: Option<Vec<String>> }, paralleling JetpackProfile (:76-81). (Some(vec![]) clears; None inherits.)
  • EffectiveDefaults (:102-111): add environment: Option<String>; surface it in effective() (:119-139).

CLI — src/cli/parser.rs (mirror the --profile machinery end-to-end):

  • Fields environment + active_environment beside profile/active_profile (:91-97).
  • ARGUMENT_ENVIRONMENT (--environment / -E) through the flag tables (:288-289, :332-333, :384-385), the dispatch arm (:718-719), and store_environment (mirror store_profile :1135).

Application — src/cli/parser.rs:1197-1216 (the profile-resolution block):

  • After profile resolution, resolve let env_name = self.environment.clone().or(defaults.environment.clone());
  • If Some(name): look up config.environments[name] (clear error if absent — catches typos), and append its secrets_inventory paths to defaults.secrets_inventory (after the profile/base paths, via resolve_repo_relative_path). Order = profile first, env last, so the env overlay wins.
  • self.active_environment = env_name; (surface for the summary, like active_profile at :1220).
  • The appended paths flow through inventory_load_paths (:964-968) → skipped by --no-secrets (:966) for free.

Resolution summary: add an Environment: row beside Profile:/Secrets: (~parser.rs:1292).

No change to loading.rs — vars-only overlays (:78-81) and later-wins merge (:134-135) already do the right thing.

Considerations

  • Orthogonality, not nesting. Environment is a separate axis (flag + defaults.environment + environments: map), NOT a profiles.X.environment attribute — nesting would re-collapse the two axes. (A future profiles.X.environment pin is possible but out of scope for v1.)
  • Env overlay rides the secrets path (skipped by --no-secrets). An env's non-secret vars (e.g. k3s_group_prefix) therefore don't load under --no-secrets. Acceptable for now (you select an env precisely to get its truth); a future non-secret vars_inventory overlay would refine this.
  • No auto var injection. The env overlay defines its own vars (environment: test, k3s_group_prefix: test-k8s, …); Jetpack just loads the overlay. Keeps it general.
  • Additive within v1. All fields optional; SUPPORTED_SCHEMA_VERSION stays 1. Existing contracts unchanged.

Tests (TDD)

  • Schema: defaults.environment + environments: parse; effective().environment == Some("prod"); JetpackEnvironment.secrets_inventory round-trips; Some(vec![]) vs None distinction (mirror profile_secrets_empty_vec_is_distinct_from_absent :314).
  • CLI: --environment test overrides defaults.environment; -E alias works.
  • Overlay merge: an env-overlay var shadows the base (later-wins) — integration mirroring group_vars_merge_across_multiple_inventory_paths (loading.rs:488).
  • Orthogonality: --profile X --environment Y loads X's inventory+secrets AND Y's overlay.
  • --no-secrets skips the env overlay.
  • Unknown env (--environment foo, no environments.foo) → clear error naming the env.
  • End-to-end (real binary): groups: ["{{ k3s_group_prefix }}-bootstrap"] resolves to the overlay's value under --environment test, base default otherwise (mirror the Template play.groups — parameterize a playbook's target groups from vars #52 e2e at traversal.rs:2019).

Context

Unblocks the clean prod/test model for riffcc/infra (currently misusing profiles: { test }), and scales to multi-site + staging. Natural sibling to profiles (#47/#54) and to the templated play.groups work (#52). After this ships, riffcc/infra migrates profiles: { test }defaults.environment: prod + environments: { test: { … } }, invoked via --environment test.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions