templates: add PV_LIFECYCLE_GOAL, render PV_STATUS_GOAL=STAGED as MOUNTED + lifecycle_goal, drop PV_STATUS_MOUNTED

Why

Pantavisor's STAGED status goal (full container, mounted with drivers loaded, not started until requested via pv-ctrl) is being reworked in pantavisor#827 (open) as a dual-goal design instead of the PV_STATUS_MOUNTED substate from !504 (merged) (merged as f1c440a1, not yet in any release tag):

{ "status_goal": "MOUNTED", "lifecycle_goal": "STAGED" }
  • status_goal is the fallback for older Pantavisor, which ignores lifecycle_goal and treats the container as plain MOUNTED: volumes mounted, never started. The revision still installs. Older Pantavisor rejects the whole revision for any status_goal value other than MOUNTED/STARTED/READY, so it must never be set to STAGED directly.
  • lifecycle_goal is the preferred goal on newer Pantavisor, which gives the full STAGED behaviour. Unknown values fall back to status_goal.

This MR replaces !504 (merged)'s PV_STATUS_MOUNTED template argument with this simpler design, decided with the pantavisor reviewers, and adds an explicit PV_LIFECYCLE_GOAL template argument so goals other than STAGED (future ones included) can use the same dual-goal mechanism without a template change.

What changed

templates/builtin-lxc-docker.go (templates/compile-template_test.go for tests, docs/PVR_TEMPLATES.md for docs):

PV_STATUS_GOAL PV_LIFECYCLE_GOAL Rendering
STAGED unset Shorthand for PV_LIFECYCLE_GOAL=STAGED. Full container (type, config, lxc.container.conf -- exactly as STARTED) plus "status_goal": "MOUNTED" and "lifecycle_goal": "STAGED".
any / unset set (any value) Full container whenever the value is not MOUNTED; data-only when it is MOUNTED. lifecycle_goal is always rendered verbatim (no validation -- future goal names pass through). status_goal is PV_STATUS_GOAL if it is MOUNTED/STARTED/READY, else MOUNTED (covers unset and STAGED).
MOUNTED unset Data-only, byte-identical to pre-!504 rendering (no type/config/lxc.container.conf/roles/exports/logs).
STARTED / READY unset Unchanged: full container, plain status_goal, no lifecycle_goal.

PV_STATUS_MOUNTED is removed entirely (it was never released -- see Compatibility below): its LXC_CONTAINER_CONF/RUN_JSON template branches, the "status_substate" output, the pvr_warn template func (added in !504 (merged) and used nowhere else), its tests and its docs row.

cmd/app/appadd.go: the --status-goal/PVR_STATUS_GOAL flag on pvr app add writes c.String("status-goal") straight into PV_STATUS_GOAL with no validation of allowed values anywhere in cmd/app or libpvr -- so --status-goal STAGED already renders the same pair with no code change needed. Its usage string was updated to list STAGED as a valid goal, and a matching --lifecycle-goal/ PVR_LIFECYCLE_GOAL flag was added (same pass-through pattern, into PV_LIFECYCLE_GOAL). pvr app update has no --status-goal flag to mirror, so it gets no --lifecycle-goal either.

PV_LIFECYCLE_GOAL

PV_LIFECYCLE_GOAL generalizes the STAGED shorthand to any goal name:

pvr app add myapp --from=nginx --lifecycle-goal STAGED
pvr app add myapp --from=nginx --lifecycle-goal STAGED --status-goal STARTED
pvr app add myapp --from=nginx --arg PV_LIFECYCLE_GOAL=MOUNTED
  • Set (non-empty) -> "lifecycle_goal" is rendered verbatim, no validation, so future goals pass through unchanged.
  • status_goal is then PV_STATUS_GOAL if it is one of MOUNTED/STARTED/READY, else MOUNTED as the safe fallback -- this also applies to the PV_STATUS_GOAL=STAGED shorthand, since STAGED itself is not one of the three allowed fallback values.
  • The container renders in full (type, config, lxc.container.conf, roles/exports/logs) whenever the preferred goal (PV_LIFECYCLE_GOAL if set, else PV_STATUS_GOAL) is not MOUNTED, even when status_goal itself falls back to MOUNTED. Data-only rendering is reserved for when that preferred goal is MOUNTED (the existing data runlevel rule is unchanged).
  • Neither set, or PV_STATUS_GOAL one of MOUNTED/STARTED/READY without PV_LIFECYCLE_GOAL, renders byte-identical to master before this branch: no lifecycle_goal key.

Compatibility

git tag --contains f1c440a1 is empty: !504 (merged)'s PV_STATUS_MOUNTED never shipped in a release, so removing it is not a breaking change for anyone consuming a tagged pvr.

Why lifecycle_goal, not status_goal_v2

The key is named lifecycle_goal, a name that shares no prefix with status_goal, rather than a status_goal-prefixed variant.

Pantavisor versions before pantavisor#828 look up run.json keys by prefix match: parser/parser_system1.c's do_lookup_json_key uses strncmp(curr, key, strlen(key)). Any rendered key that starts with status_goal -- status_goal_v2 included -- can therefore be matched by an old pantavisor's lookup for status_goal itself, which would hand it STAGED as if it were a status_goal value. Naming the new key lifecycle_goal removes that prefix relationship entirely, so an old pantavisor's status_goal lookup can only ever match the real status_goal key. This also means the rendered key order no longer matters (the earlier design required status_goal to render before status_goal_v2 in the raw template text to win the prefix match; that constraint is gone).

Rendered examples (throwaway go test, not committed)

PV_STATUS_GOAL=STAGED (shorthand, no PV_LIFECYCLE_GOAL):

{
	"#spec": "service-manifest-run@1",
	"name":"myapp",
	"config": "lxc.container.conf",
	"drivers": {
		"manual":[],
		"required":[],
		"optional":[]
	},
	"services": {
		"manual":[],
		"required":[],
		"optional":[]
	},
	"status_goal": "MOUNTED",
	"lifecycle_goal": "STAGED",
	"storage":{
		"lxc-overlay" : {
			"persistence": "boot"
		}
	},
	"type":"lxc",
	"root-volume": "root.squashfs",
	"volumes":[]
}

PV_LIFECYCLE_GOAL=STAGED + PV_STATUS_GOAL=STARTED (status_goal keeps the explicit STARTED fallback instead of defaulting to MOUNTED):

{
	"#spec": "service-manifest-run@1",
	"name":"cont",
	"config": "lxc.container.conf",
	"drivers": {
		"manual":[],
		"required":[],
		"optional":[]
	},
	"services": {
		"manual":[],
		"required":[],
		"optional":[]
	},
	"status_goal": "STARTED",
	"lifecycle_goal": "STAGED",
	"storage":{
		"lxc-overlay" : {
			"persistence": "boot"
		}
	},
	"type":"lxc",
	"root-volume": "root.squashfs",
	"volumes":[]
}

PV_LIFECYCLE_GOAL=STAGED alone (PV_STATUS_GOAL unset, falls back to MOUNTED -- same output as the shorthand above):

{
	"#spec": "service-manifest-run@1",
	"name":"cont",
	"config": "lxc.container.conf",
	"drivers": {
		"manual":[],
		"required":[],
		"optional":[]
	},
	"services": {
		"manual":[],
		"required":[],
		"optional":[]
	},
	"status_goal": "MOUNTED",
	"lifecycle_goal": "STAGED",
	"storage":{
		"lxc-overlay" : {
			"persistence": "boot"
		}
	},
	"type":"lxc",
	"root-volume": "root.squashfs",
	"volumes":[]
}

PV_STATUS_GOAL=MOUNTED (data-only, unchanged):

{
	"#spec": "service-manifest-run@1",
	"name":"myapp",
	"drivers": {
		"manual":[],
		"required":[],
		"optional":[]
	},
	"services": {
		"manual":[],
		"required":[],
		"optional":[]
	},
	"status_goal": "MOUNTED",
	"storage":{
		"lxc-overlay" : {
			"persistence": "boot"
		}
	},
	"root-volume": "root.squashfs",
	"volumes":[]
}

Test plan

  • go build ./...
  • go vet ./...
  • go test ./... (all packages pass, incl. the table-driven TestCompileTemplate/PV_STATUS_GOAL_/_PV_LIFECYCLE_GOAL_combinations, covering the STAGED shorthand, every PV_LIFECYCLE_GOAL + PV_STATUS_GOAL combination above, and unchanged plain MOUNTED/STARTED)
  • gofmt -l . clean
Edited by Alexander (asac) Sack

Merge request reports

Loading
Loading