Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ SWAGGER_FILES := pkg/apiclient/_.primary.swagger.json \
pkg/apiclient/workflowtemplate/workflow-template.swagger.json \
pkg/apiclient/sync/sync.swagger.json
PROTO_BINARIES := $(TOOL_PROTOC_GEN_GOGO) $(TOOL_PROTOC_GEN_GOGOFAST) $(TOOL_GOIMPORTS) $(TOOL_PROTOC_GEN_GRPC_GATEWAY) $(TOOL_PROTOC_GEN_SWAGGER) $(TOOL_CLANG_FORMAT)
QUICK_GENERATED_DOCS := docs/metrics.md docs/tracing.md docs/database-migrations.md
QUICK_GENERATED_DOCS := docs/metrics.md docs/tracing.md docs/database-migrations.md docs/variable-flow/variables.md
GENERATED_DOCS := $(QUICK_GENERATED_DOCS) docs/fields.md docs/cli/argo.md docs/workflow-controller-configmap.md docs/go-sdk-guide.md

# protoc,my.proto
Expand Down Expand Up @@ -768,6 +768,10 @@ docs/tracing.md: $(TELEMETRY_BUILDER) util/telemetry/builder/values.yaml
docs/database-migrations.md: persist/sqldb/migrate.go util/sync/db/migrate.go hack/docs/migrations/main.go
GOFLAGS="$(GOFLAGS) -mod=mod" go run ./hack/docs/migrations

docs/variable-flow/variables.md: $(wildcard util/variables/*.go) $(wildcard util/variables/keys/*.go)
@echo Rebuilding $@
go test -run TestGenerateMarkdown -count=1 ./util/variables/ -args -write

# swagger
pkg/apis/workflow/v1alpha1/openapi_generated.go: $(TOOL_OPENAPI_GEN) $(TYPES)
# These files are generated on a v4/ folder by the tool. Link them to the root folder
Expand Down Expand Up @@ -860,7 +864,7 @@ endif
.PHONY: docs-spellcheck
docs-spellcheck: $(TOOL_MDSPELL) $(QUICK_GENERATED_DOCS) ## Spell check docs
# check docs for spelling mistakes
$(TOOL_MDSPELL) --ignore-numbers --ignore-acronyms --en-us --no-suggestions --report $(shell find docs -name '*.md' -not -name upgrading.md -not -name README.md -not -name fields.md -not -name workflow-controller-configmap.md -not -name upgrading.md -not -name executor_swagger.md -not -path '*/cli/*' -not -name tested-kubernetes-versions.md -not -name new-features.md)
$(TOOL_MDSPELL) --ignore-numbers --ignore-acronyms --en-us --no-suggestions --report $(shell find docs -name '*.md' -not -name upgrading.md -not -name README.md -not -name fields.md -not -name workflow-controller-configmap.md -not -name upgrading.md -not -name executor_swagger.md -not -path '*/cli/*' -not -name tested-kubernetes-versions.md -not -name new-features.md -not -path '*/variable-flow/*')
# alphabetize spelling file -- ignore first line (comment), then sort the rest case-sensitive and remove duplicates
$(shell cat .spelling | awk 'NR<2{ print $0; next } { print $0 | "LC_COLLATE=C sort" }' | uniq > .spelling.tmp && mv .spelling.tmp .spelling)

Expand Down
495 changes: 495 additions & 0 deletions docs/variable-flow/variables-showcase.yaml

Large diffs are not rendered by default.

582 changes: 582 additions & 0 deletions docs/variable-flow/variables.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,7 @@ require (
golang.org/x/net v0.53.0
golang.org/x/sys v0.43.0
golang.org/x/term v0.42.0
golang.org/x/text v0.36.0 // indirect
golang.org/x/text v0.36.0
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ nav:
- Features:
# this is a bit of a dumping ground, I've tried to order with key features first
- variables.md
- Variable catalog: variable-flow/variables.md
- retries.md
- lifecyclehook.md
- synchronization.md
Expand Down
17 changes: 9 additions & 8 deletions util/template/expression_template.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (

"github.com/argoproj/argo-workflows/v4/util/logging"
"github.com/argoproj/argo-workflows/v4/util/maps"
varkeys "github.com/argoproj/argo-workflows/v4/util/variables/keys"
)

func init() {
Expand All @@ -29,14 +30,14 @@ func init() {

var (
variablesToCheck = []string{
"item",
"retries",
"lastRetry.exitCode",
"lastRetry.status",
"lastRetry.duration",
"lastRetry.message",
"workflow.status",
"workflow.failures",
varkeys.Item.Template(),
varkeys.Retries.Template(),
varkeys.RetriesLastExitCode.Template(),
varkeys.RetriesLastStatus.Template(),
varkeys.RetriesLastDuration.Template(),
varkeys.RetriesLastMessage.Template(),
varkeys.WorkflowStatus.Template(),
varkeys.WorkflowFailures.Template(),
}
)

Expand Down
6 changes: 6 additions & 0 deletions util/variables/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Package variables is the catalog of every Argo Workflows expression
// variable. Each variable is declared once via Define (in
// util/variables/keys/) and obtains a *Key handle. Key.Set is the only
// public write path on a *Scope, so the catalog and the write sites cannot
// drift — they are literally the same objects.
package variables
196 changes: 196 additions & 0 deletions util/variables/docgen.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
package variables

import (
"bytes"
"fmt"
"io"
"slices"
"strings"

md "github.com/nao1215/markdown"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)

// GenerateMarkdown renders the catalog as Markdown. Sections:
// alphabetical index, by Kind, matrix by TemplateKind, by LifecyclePhase.
func GenerateMarkdown() string {
var buf bytes.Buffer
mdoc := md.NewMarkdown(io.Writer(&buf))
all := All()

mdoc.H1("Workflow variables catalog")
mdoc.PlainText("")
mdoc.PlainTextf("Auto-generated from `util/variables` via `GenerateMarkdown()`. %d variables registered.", len(all))
mdoc.PlainText("")

mdoc.H2("1. Alphabetical index")
mdoc.PlainText("")
writeFullTable(mdoc, all)

mdoc.H2("2. Grouped by Kind")
mdoc.PlainText("")
writeByKind(mdoc, all)

mdoc.H2("3. Matrix by TemplateKind")
mdoc.PlainText("")
mdoc.PlainText("Which variables are in scope for each template type. `•` = in scope, blank = not in scope.")
mdoc.PlainText("")
writeMatrix(mdoc, all)

mdoc.H2("4. Grouped by LifecyclePhase")
mdoc.PlainText("")
writePhaseLegend(mdoc)
writeByPhase(mdoc, all)

_ = mdoc.Build()
return buf.String()
}

func writeFullTable(mdoc *md.Markdown, all []*Key) {
rows := make([][]string, len(all))
for i, k := range all {
rows[i] = []string{md.Code(k.template), k.kind.String(), k.valueType, joinPhases(k.phases), k.description}
}
table(mdoc, []string{"Key", "Kind", "Type", "Availability", "Description"}, rows)
}

func writeByKind(mdoc *md.Markdown, all []*Key) {
groups := map[Kind][]*Key{}
var kinds []Kind
for _, k := range all {
if _, ok := groups[k.kind]; !ok {
kinds = append(kinds, k.kind)
}
groups[k.kind] = append(groups[k.kind], k)
}
slices.Sort(kinds)
titler := cases.Title(language.English)
for _, kd := range kinds {
mdoc.H3(titler.String(kd.String()))
mdoc.PlainText("")
rows := make([][]string, len(groups[kd]))
for i, k := range groups[kd] {
rows[i] = []string{md.Code(k.template), k.valueType, joinPhases(k.phases), k.description}
}
table(mdoc, []string{"Key", "Type", "Availability", "Description"}, rows)
}
}

func writeMatrix(mdoc *md.Markdown, all []*Key) {
cols := append([]TemplateKind{TmplAll}, AllTemplateKinds...)
header := append([]string{"Key"}, kindStrings(cols)...)
rows := make([][]string, len(all))
for i, k := range all {
row := make([]string, len(cols)+1)
row[0] = md.Code(k.template)
hasAll := slices.Contains(k.appliesTo, TmplAll)
for j, c := range cols {
explicit := slices.Contains(k.appliesTo, c)
// The "any" column reports raw eligibility, not reachability.
if c == TmplAll {
if explicit {
row[j+1] = "•"
}
continue
}
// appliesTo gate: either the kind is listed explicitly, or the
// variable opts in to every kind via TmplAll.
if !explicit && !hasAll {
continue
}
// Phase-reachability gate: at least one of the variable's
// declared phases must be reachable from inside this kind.
// Without this check, narrow phases like PhInsideLoop or
// PhExitHandler bleed into columns that never enter them.
if !phasesIntersect(k.phases, ReachablePhases(c)) {
continue
}
row[j+1] = "•"
}
rows[i] = row
}
table(mdoc, header, rows)
}

func phasesIntersect(a, b []LifecyclePhase) bool {
for _, x := range a {
if slices.Contains(b, x) {
return true
}
}
return false
}

func writePhaseLegend(mdoc *md.Markdown) {
table(mdoc, []string{"Phase", "Meaning"}, [][]string{
{string(PhWorkflowStart), "Globals populated once, up front, before any template runs."},
{string(PhPreDispatch), "Immediately before a template's pod is created; pod.name / node.name / steps.name / tasks.name are set."},
{string(PhDuringExecute), "Inside a template body; inputs.* are bound."},
{string(PhInsideLoop), "Inside a withItems/withParam expansion; `item`, `item.<key>` are bound."},
{string(PhInsideRetry), "Inside a retryStrategy template; retries.* are bound."},
{string(PhAfterNodeInit), "A referenced node has been initialised by the controller (id, status, startedAt are populated — startedAt is set at node-init time, before any pod is created, for all node types including non-pod ones like Suspend / HTTP / Plugin / Steps / DAG)."},
{string(PhAfterPodStart), "The referenced node's pod has started; ip, hostNodeName are populated (k8s-supplied; meaningless for non-pod node types)."},
{string(PhAfterNodeComplete), "The referenced node has finished (any terminal phase); finishedAt, exitCode are populated."},
{string(PhAfterNodeSucceeded), "The referenced node has finished with Succeeded; outputs.result, outputs.parameters.*, outputs.artifacts.* are populated."},
{string(PhAfterLoop), "Every child of a withItems/withParam group has completed; aggregated outputs appear."},
{string(PhExitHandler), "The onExit template runs. workflow.{status,failures,duration} are final. Any earlier-phase variable is also visible here (scope accumulates)."},
{string(PhMetricEmission), "Inside a Prometheus metric expression. Adds duration, status, exitCode, `resourcesDuration.<resource>`, and the current node's bare outputs.result / `outputs.parameters.<name>`."},
{string(PhCronEval), "Evaluating a CronWorkflow `spec.when` or `spec.stopStrategy.expression`. Adds cronworkflow.* variables describing the cron object's identity, labels/annotations, and run counts."},
})
}

func writeByPhase(mdoc *md.Markdown, all []*Key) {
phases := []LifecyclePhase{
PhWorkflowStart, PhPreDispatch, PhDuringExecute,
PhInsideLoop, PhInsideRetry,
PhAfterNodeInit, PhAfterPodStart, PhAfterNodeComplete, PhAfterNodeSucceeded,
PhAfterLoop, PhExitHandler, PhMetricEmission, PhCronEval,
}
for _, p := range phases {
var keys []*Key
for _, k := range all {
if slices.Contains(k.phases, p) {
keys = append(keys, k)
continue
}
// Exit-handler scope accumulates: every node-ref variable whose
// appliesTo includes TmplExitHandler is reachable from onExit,
// even though its primary phase is the one when the underlying
// value first becomes available (after-node-init, after-loop, …).
if p == PhExitHandler && slices.Contains(k.appliesTo, TmplExitHandler) {
keys = append(keys, k)
}
}
if len(keys) == 0 {
continue
}
mdoc.H3(fmt.Sprintf("%s (%d variables)", p, len(keys)))
mdoc.PlainText("")
rows := make([][]string, len(keys))
for i, k := range keys {
rows[i] = []string{md.Code(k.template), k.kind.String(), k.valueType}
}
table(mdoc, []string{"Key", "Kind", "Type"}, rows)
}
}

func table(mdoc *md.Markdown, header []string, rows [][]string) {
mdoc.CustomTable(md.TableSet{Header: header, Rows: rows}, md.TableOptions{AutoWrapText: false})
}

func joinPhases(ps []LifecyclePhase) string {
s := make([]string, len(ps))
for i, p := range ps {
s[i] = string(p)
}
return strings.Join(s, ", ")
}

func kindStrings(ks []TemplateKind) []string {
out := make([]string, len(ks))
for i, k := range ks {
out[i] = string(k)
}
return out
}
35 changes: 35 additions & 0 deletions util/variables/docgen_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package variables_test

import (
"flag"
"os"
"strings"
"testing"

v "github.com/argoproj/argo-workflows/v4/util/variables"
_ "github.com/argoproj/argo-workflows/v4/util/variables/keys"
)

// -write regenerates docs/variable-flow/variables.md in-place:
//
// go test -run TestGenerateMarkdown ./util/variables/ -args -write
var writeDocs = flag.Bool("write", false, "write docs/variable-flow/variables.md")

func TestGenerateMarkdown(t *testing.T) {
md := v.GenerateMarkdown()
for _, must := range []string{
"# Workflow variables catalog",
"workflow.name", "workflow.parameters.<name>",
"steps.<name>.outputs.result", "item.<key>", "pod.name",
} {
if !strings.Contains(md, must) {
t.Errorf("generated doc missing %q", must)
}
}
if !*writeDocs {
return
}
if err := os.WriteFile("../../docs/variable-flow/variables.md", []byte(md), 0o644); err != nil {
t.Fatalf("write: %v", err)
}
}
Loading
Loading