Help us shape the future of Go! Take the 2026 Go Developer Survey

graph

package
v0.29.25 Latest Latest
Warning

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

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

Documentation

Overview

Package graph is the install/uninstall STEP GRAPH: the declarative description of what a local MemQL cluster install does, in what order, what each step must prove before the next one runs, and what uninstall has to take back afterwards.

WHY A GRAPH AND NOT A SCRIPT

The ten capability scripts under scripts/install/ each do one thing well. What was missing was the thing that says how they compose -- and that is exactly the knowledge that, written as an installer script, ends up duplicated in three places (a shell driver, an editor extension, an in-engine automation) and drifting in all three. Here it is DATA, loaded and validated by one loader, consumed by every executor.

Three properties follow from that choice, and they are the point of the package:

  1. PARALLELISM IS A PROPERTY OF THE DATA. TopoOrder returns WAVES, not a flat list. An executor runs a wave concurrently and waits for it; it never decides what is independent, so it cannot decide wrong.
  2. EVERY STEP PROVES ITSELF. `verify` is mandatory. The one predicate that proves nothing about the machine -- scriptOk, which reads only the exit code -- is permitted only on a step that declares readOnly, because on a step that changed something it is a rubber stamp.
  3. UNINSTALL IS DERIVABLE. Every install step that leaves an artifact behind declares a `receipt` and a `preExistingPath`; the uninstall graph declares which install step each of its steps `reverses`. graph_test.go (#3370) turns that into a hard structural assertion, so uninstall cannot silently fall behind install.

THE VERIFY PREDICATE VOCABULARY

A predicate reads the capability-script result envelope ({ok, capability, changed, result{...}, error}). The vocabulary is CLOSED -- five kinds, nothing else -- so a graph document can never grow an executor requirement that only one executor implements:

{"kind":"scriptOk"}
    The envelope's ok is true. Reads nothing about the machine, so it is
    ONLY valid on a step with "readOnly": true.

{"kind":"resultTrue","field":"result.<name>"}
    result.<name> is boolean true (or the string "true").

{"kind":"resultFalse","field":"result.<name>"}
    result.<name> is boolean false (or the string "false").

{"kind":"resultNonEmpty","field":"result.<name>"}
    result.<name> is present and not empty / zero / null.

{"kind":"resultEquals","field":"result.<name>","value":"<v>"}
    result.<name> stringifies to exactly <v>.

Every field is spelled with its `result.` prefix on purpose: the envelope's own bookkeeping (ok, changed, capability) describes the RUN, while result{} describes the MACHINE, and only the second is evidence that a step did its job. Refusing an unprefixed field keeps that distinction visible at the point where a reviewer reads the graph.

THE PRE-EXISTENCE PATH

A receipt is a promise that uninstall can take an artifact back. It is only safe to keep that promise when the receipt also records whether the installer CREATED the artifact or merely FOUND it: a developer who already had mkcert, or a k3d cluster, or a checkout at that path must not lose it because they uninstalled MemQL. So `receipt` and `preExistingPath` are required together.

"preExistingPath": "result.preExisting"    // pre-existed when that is truthy
"preExistingPath": "!result.cloned"        // pre-existed when cloned is FALSE
"preExistingPath": "none"                  // cannot pre-exist as a foreign
                                           // artifact (marker-owned), stated
                                           // explicitly rather than omitted

The executor copies the resolved boolean onto the receipt; uninstall passes it back as remove-artifact.sh's --pre-existing, whose refusal is the last wall between the flag and the damage.

ELEVATION IS DECLARED, NEVER DISCOVERED

Every step states the privilege it needs ("none" / "sudo" / "user-trust"). A step that quietly needs root is a step whose blast radius nobody reviewed, and an executor that discovers the need at run time has already started.

Refs: #3369 #3357

Index

Constants

View Source
const MaxLabelWords = 4

MaxLabelWords is the longest a step label may be. A label is a status line ("Creating the cluster"), and a status line that needs a fifth word is a sentence -- which is what Description is for.

View Source
const PreExistingNone = "none"

PreExistingNone is the explicit declaration that an artifact cannot pre-exist as a FOREIGN artifact -- it is delimited by our own marker, so finding it means finding our own earlier work. Stated rather than omitted: "there is no signal" and "nobody thought about it" must not look alike.

Variables

This section is empty.

Functions

This section is empty.

Types

type Elevation

type Elevation string

Elevation is the privilege a step declares up front.

const (
	// ElevationNone runs entirely as the invoking user.
	ElevationNone Elevation = "none"
	// ElevationSudo needs root (writing /etc/hosts).
	ElevationSudo Elevation = "sudo"
	// ElevationUserTrust mutates the user's trust store (mkcert -install),
	// which may prompt for a password on its own terms without being sudo.
	ElevationUserTrust Elevation = "user-trust"
)

type Graph

type Graph struct {
	Name        string `json:"name"`
	Kind        Kind   `json:"kind"`
	Description string `json:"description"`
	Steps       []Step `json:"steps"`
	// contains filtered or unexported fields
}

Graph is a loaded, validated step graph.

func Install

func Install() (*Graph, error)

Install returns the shipped install graph.

func InstallFromMain added in v0.19.7

func InstallFromMain() (*Graph, error)

InstallFromMain is the install graph for the FROM-SOURCE lane (memql#4430): a `main` checkout whose node images are BUILT from that checkout rather than pulled from a registry, because no `main` image is published and the lane exists so a developer can run main's engine.

IT IS A VARIANT, NOT A SECOND GRAPH. install_main_variant_test.go derives it from install.json and fails if the two differ by anything other than the three deltas below, so the sixteen shared steps cannot drift apart:

  1. `clusterUp` proves `argocdReady` rather than `workloadsReady`. On this lane the workloads CANNOT be Available when clusterUp returns -- the overlay names `memql-<node>:local` and nothing has built them yet -- so requiring it would burn the 900s budget and fail every from-source install. What clusterUp genuinely does here is create the cluster and register ArgoCD, and that is what it is asked to prove. The workloads are proven one step later, by the step that actually makes them possible. This is the same order `scripts/k3d/bringup.sh` has always used for `make up`: up, then build + import, then healthy.
  2. `buildImages` is inserted after it -- the k3d.dev capability rebuild.json drives, with `--image-source=checkout` pinned by the document so no caller can turn it into "build, but keep running released images".
  3. `seedBootstrap` waits for BOTH. TopoOrder returns WAVES, so a seedBootstrap that still depended only on clusterUp would run CONCURRENTLY with the image build -- writing bootstrap values into the cluster and restarting nodes that are still in ImagePullBackOff. Everything after seedBootstrap is already serial, so this one edge gates the whole tail.

func Load

func Load(data []byte, source string) (*Graph, error)

Load parses and fully validates a graph document. It refuses anything an executor would otherwise have to guess about: an unknown field, a step with no verify, a scriptOk stamp on a mutating step, a dangling or duplicated or self-referential dependency, an undeclared elevation, a receipt with no pre-existence signal, and any dependency cycle. A graph that loads is a graph an executor can run without re-deriving anything.

func Rebuild added in v0.19.7

func Rebuild() (*Graph, error)

Rebuild is the one-step graph the extension's "Rebuild from checkout" runs: k3d.dev over the recorded checkout with --image-source=checkout. Install- shaped (forward steps, a receipt) rather than a third kind, because a rebuild IS an install of images the operator built.

func Uninstall

func Uninstall() (*Graph, error)

Uninstall returns the shipped uninstall graph.

func UpdateRebuild added in v0.20.1

func UpdateRebuild() (*Graph, error)

UpdateRebuild is the two-step graph the extension's "Update from origin and rebuild" runs: bring the recorded checkout up to date, then rebuild from it (memql#4578).

IT IS NOT A MODE OF Rebuild. Two documents rather than one with a flag, because they are two things an operator asks for: "test what I have" and "test the latest with what I have". A flag would make the second reachable from the button labelled the first, which is the one mistake here that surprises somebody holding uncommitted work.

AND THE SECOND STEP IS Rebuild's STEP, in every field but the dependency edge below -- asserted by update_rebuild_variant_test.go rather than claimed here, because the failure is silent: change the build's params or its verify in one document and not the other and both buttons still work, they simply stop building the same way.

The edge from `rebuildFromCheckout` to `updateCheckout` is the whole safety story on this lane. TopoOrder returns WAVES, so without it the build would run CONCURRENTLY with the update -- reading a working tree while git is rewriting it, and producing an image nobody can name the source of.

func (*Graph) IDs

func (g *Graph) IDs() []string

IDs returns every step id in document order.

func (*Graph) Step

func (g *Graph) Step(id string) *Step

Step returns the step with the given id, or nil.

func (*Graph) TopoOrder

func (g *Graph) TopoOrder() ([][]string, error)

TopoOrder returns the graph's steps grouped into dependency WAVES: every id in waves[0] has no dependencies, and every id in waves[i] depends only on steps in earlier waves. An executor runs a wave concurrently, waits for all of it to verify, and moves on.

Returning waves rather than a flat topological list is the whole design choice: it makes "these three tool installs are independent" a fact stated by the graph document instead of a judgement each executor re-derives (and one of them gets wrong, serialising a two-minute install into six).

Ids within a wave are sorted so the order is deterministic across runs.

type Kind

type Kind string

Kind distinguishes the two shipped graph documents. It is not decoration: `receipt` is meaningful only on an install graph and `reverses` only on an uninstall one, and the loader enforces that split.

const (
	KindInstall   Kind = "install"
	KindUninstall Kind = "uninstall"
)

type Step

type Step struct {
	// ID names the step within its graph; receipts and `reverses` key off it.
	ID string `json:"id"`
	// Script is the capability-script id (the `script` arg of a shell.script
	// action), which must be in the engine's capability allowlist.
	Script string `json:"script"`

	// TimeoutSeconds is the step's own executor ceiling, replacing the
	// run-wide default for this step only (memql#4076). Carried here so the
	// Go gates see the same document the executor runs; the TS loader owns
	// validation (positive integer).
	TimeoutSeconds int `json:"timeoutSeconds,omitempty"`
	// Label is the step's SHORT name: sentence case, at most MaxLabelWords
	// words ("Creating the cluster"). It is what a progress display says while
	// the step runs and what the CLI prints as the step starts; Description
	// is the longer sentence behind it. Required, so every surface that shows
	// progress can name every step without inventing words of its own.
	Label string `json:"label"`
	// Description is the operator-facing sentence for this step.
	Description string `json:"description"`
	// Params are the graph-PINNED flags for the script: the values that are
	// policy rather than run input (which tool, which action, the confirmation
	// phrase). Run-specific values (a release tag, a key file path) are
	// supplied by the executor at invocation time, so this map is a SUBSET of
	// the script's declared flags -- loader_test.go asserts every key here is
	// really declared by the script's --print-spec.
	Params map[string]string `json:"params,omitempty"`
	// DependsOn lists step ids that must have verified before this one starts.
	DependsOn []string `json:"dependsOn,omitempty"`
	// ReadOnly declares that the step does not change the machine. It is what
	// licenses the scriptOk predicate.
	ReadOnly bool `json:"readOnly,omitempty"`
	// Elevation is the privilege this step needs, declared not discovered.
	Elevation Elevation `json:"elevation"`
	// Receipt names the artifact class this step leaves behind (the `kind` an
	// uninstall step passes to remove-artifact.sh). Empty means the step
	// creates nothing that needs taking back. Install graphs only.
	Receipt string `json:"receipt,omitempty"`
	// PreExistingPath resolves, against the result envelope, to "did this
	// artifact already exist before we ran". Required exactly when Receipt is
	// set. See the package comment for the grammar.
	PreExistingPath string `json:"preExistingPath,omitempty"`
	// ContainedBy names another install step whose receipt already covers this
	// step's change -- a secret written INTO the cluster goes away when the
	// cluster does, so it needs no receipt of its own. It is the honest
	// alternative to a receipt, not a way around one: graph_test.go (#3370)
	// requires every mutating install step to declare one or the other, and
	// requires the named step to be reversible and to be a dependency of this
	// one. Mutually exclusive with Receipt. Install graphs only.
	ContainedBy string `json:"containedBy,omitempty"`
	// Reverses names the install-graph step id this step undoes. Uninstall
	// graphs only; it is what makes the drift assertion in #3370 possible.
	Reverses string `json:"reverses,omitempty"`
	// Retained declares that this step changes the machine and the change is
	// DELIBERATELY KEPT -- the third answer to "say where this gets taken
	// back", alongside a receipt and containedBy (memql#3566).
	//
	// It exists because the honest answer for a distribution package is
	// "nowhere". certutil is a general system tool that other software uses,
	// and an application uninstaller that removes distro packages is how
	// installers earn their reputation. Forcing that step to claim a receipt it
	// will never honour, or a containment that is not true, would put a lie in
	// the document instead of a decision. Install graphs only.
	Retained bool `json:"retained,omitempty"`
	// RetainedReason says WHY, in the document, next to the claim. Required
	// when Retained is set: "this is kept" is a decision an operator reading
	// the graph is entitled to see argued.
	RetainedReason string `json:"retainedReason,omitempty"`
	// Shared marks an uninstall step whose artifact is NOT MemQL-only -- k3d,
	// kubectl, mkcert, the local CA. Those are general tools the operator may
	// now depend on, so removing them is OFFERED rather than assumed, and the
	// wizard shows them unticked (memql#3566). A step without this flag removes
	// something only MemQL put there and only MemQL uses, and needs no
	// permission beyond "uninstall MemQL". Uninstall graphs only.
	Shared bool `json:"shared,omitempty"`
	// SharedReason is what the operator reads beside the checkbox: what else
	// this thing is good for. Required when Shared is set.
	SharedReason string `json:"sharedReason,omitempty"`
	// Verify is mandatory: what this step must prove before the graph moves on.
	Verify Verify `json:"verify"`
}

Step is one capability-script invocation plus everything an executor needs to schedule it, prove it, and (for an install step) undo it later.

func (*Step) PreExisting

func (s *Step) PreExisting() (field string, negate bool, ok bool)

PreExisting decodes the step's pre-existence declaration into the result field to read and whether its meaning is inverted. ok is false when the step declares "none" or nothing at all -- there is no field to consult.

func (*Step) VerifyField

func (s *Step) VerifyField() string

VerifyField returns the bare result-envelope field this step's predicate reads, or "" for scriptOk.

type Verify

type Verify struct {
	Kind  VerifyKind `json:"kind"`
	Field string     `json:"field,omitempty"`
	Value string     `json:"value,omitempty"`
}

Verify is one step's proof obligation. See the package comment for the vocabulary.

func (Verify) Evaluate

func (v Verify) Evaluate(envelope map[string]any) (bool, error)

Evaluate applies the predicate to a parsed capability-script result envelope ({"ok":..., "result":{...}}). Keeping the vocabulary executable HERE is what stops each executor from inventing its own reading of the same graph.

type VerifyKind

type VerifyKind string

VerifyKind is the closed predicate vocabulary documented in the package comment.

const (
	// VerifyScriptOk reads the envelope's ok flag and nothing else. Valid only
	// on a readOnly step.
	VerifyScriptOk VerifyKind = "scriptOk"
	// VerifyResultTrue asserts result.<field> is true.
	VerifyResultTrue VerifyKind = "resultTrue"
	// VerifyResultFalse asserts result.<field> is false.
	VerifyResultFalse VerifyKind = "resultFalse"
	// VerifyResultNonEmpty asserts result.<field> is present and non-empty.
	VerifyResultNonEmpty VerifyKind = "resultNonEmpty"
	// VerifyResultEquals asserts result.<field> stringifies to Value.
	VerifyResultEquals VerifyKind = "resultEquals"
)

Jump to

Keyboard shortcuts

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