Skip to content

Commit 5105843

Browse files
authored
feat(values)!: support state built-ins (#4957)
Signed-off-by: Brandt Keller <brandt.keller@defenseunicorns.com>
1 parent 9c79597 commit 5105843

21 files changed

Lines changed: 537 additions & 51 deletions

File tree

examples/values-templating/nginx-configmap.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,15 @@ data:
7272
<pre>Repeat and Indent Example:</pre>
7373
<pre>{{ .Values.site.message | repeat 3 | indent 4 }}</pre>
7474
75+
<!-- State Template Examples -->
76+
<!-- .State fields expose Zarf runtime state set during zarf init.
77+
Non-sensitive fields are always available without any stateAccess declaration. -->
78+
<h3>Cluster State (via .State)</h3>
79+
<pre>Registry: {{ .State.Registry.Address }}</pre>
80+
<pre>Storage class: {{ .State.StorageClass }}</pre>
81+
<pre>IP family: {{ .State.IPFamily }}</pre>
82+
<pre>IPv6 only: {{ eq .State.IPFamily "ipv6" }}</pre>
83+
7584
{{ .Values.site.footer }}
7685
</body>
7786
</html>

examples/values-templating/readme.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
# [ALPHA] Values & Templates Example
22

3-
This example demonstrates the pre-release alpha version of Zarf's values templating system, including support for **Sprig functions** for advanced template processing and **Helm chart value overrides**.
3+
This example demonstrates the pre-release alpha version of Zarf's values templating system, including support for **Sprig functions** for advanced template processing, **Helm chart value overrides**, and **cluster state access** via `.State`.
44

55
## Features Demonstrated
66

77
- **Basic templating** with `{{ .Values.* }}`, `{{ .Build.* }}`, `{{ .Metadata.* }}`, `{{ .Constants.* }}`, and `{{ .Variables.* }}`
8+
- **Cluster state** with `{{ .State.Registry.Address }}`, `{{ .State.StorageClass }}`, `{{ .State.IPFamily }}`, and other non-sensitive runtime fields via `.State`
89
- **Sprig functions** for string manipulation, lists, math, encoding, and more
910
- **File templating** with both simple substitution and complex transformations
1011
- **Dynamic configuration** using template functions for practical Kubernetes deployments

site/src/content/docs/ref/values.mdx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,3 +155,42 @@ In addition to user supplied Variables, and Constants, Zarf maintains a list of
155155
Because files can be deployed without a Kubernetes cluster, some built-in values such as `###ZARF_REGISTRY###` may not be available if no previous component has required access to the cluster. If you need one of these built-in values, a prior component will need to have been called that requires access to the cluster, such as `images`, `repos`, `charts`, `manifests`, or `dataInjections`.
156156

157157
:::
158+
159+
### Cluster State (`.State`)
160+
161+
The `.State` object exposes Zarf cluster state in **manifests**, **files**, and **action `cmd` fields** that have `template: true` set.
162+
163+
**Non-sensitive fields** are always available with no additional declaration:
164+
165+
| Field | Description |
166+
|---|---|
167+
| `.State.StorageClass` | Default storage class (cf. `###ZARF_STORAGE_CLASS###`) |
168+
| `.State.IPFamily` | `"ipv4"`, `"ipv6"`, or `"dual"` (cf. `###ZARF_IPV6_ONLY###`) |
169+
| `.State.Registry.Address` | Registry URL (cf. `###ZARF_REGISTRY###`) |
170+
| `.State.Registry.Port` | Registry port (cf. `###ZARF_REGISTRY_PORT###`) |
171+
| `.State.Registry.PushUsername` | Registry push username |
172+
| `.State.Registry.PullUsername` | Registry pull username |
173+
| `.State.Registry.Mode` | `"nodeport"`, `"proxy"`, or `"external"` (cf. `###ZARF_REGISTRY_PROXY###`) |
174+
| `.State.Registry.MTLSEnabled` | `bool` (cf. `###ZARF_REGISTRY_MTLS_ENABLED###`) |
175+
| `.State.Registry.SeedAddress` | Localhost seed registry address (cf. `###ZARF_SEED_REGISTRY###`) |
176+
| `.State.Git.Address` | Git server URL |
177+
| `.State.Git.PushUsername` | Git push username (cf. `###ZARF_GIT_PUSH###`) |
178+
| `.State.Git.PullUsername` | Git pull username (cf. `###ZARF_GIT_PULL###`) |
179+
| `.State.Git.IsInternal` | `bool` |
180+
| `.State.Injector.Image` | Injector container image (cf. `###ZARF_INJECTOR_IMAGE###`) |
181+
| `.State.Injector.Port` | Injector host port (cf. `###ZARF_INJECTOR_HOSTPORT###`) |
182+
| `.State.Injector.PayloadConfigMaps` | Number of payload ConfigMaps (cf. `###ZARF_INJECTOR_PAYLOAD_CONFIGMAPS###`) |
183+
| `.State.Injector.PayloadShaSum` | SHA sum of injector payload (cf. `###ZARF_INJECTOR_SHASUM###`) |
184+
185+
**Sensitive fields** require a `stateAccess` declaration on the component. Accessing a sensitive field without the corresponding group causes a template error at deploy time:
186+
187+
```yaml
188+
components:
189+
- name: my-component
190+
stateAccess:
191+
- registryCredentials # unlocks .State.Registry.{PushPassword,PullPassword,Secret,Htpasswd}
192+
- gitCredentials # unlocks .State.Git.{PushPassword,PullPassword}
193+
- agentCerts # unlocks .State.Agent.{CA,Cert,Key} (base64-encoded)
194+
```
195+
196+
See the [values-templating example](/ref/examples/values-templating/) for a working demonstration of `.State` fields in manifests and actions.

src/api/v1alpha1/component.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,24 @@ type ZarfComponent struct {
5656

5757
// List of resources to health check after deployment
5858
HealthChecks []NamespacedObjectKindReference `json:"healthChecks,omitempty"`
59+
60+
// Groups of sensitive .State fields this component may access in Go templates (manifests, files, actions with template: true).
61+
// Valid values: "registryCredentials", "gitCredentials", "agentCerts".
62+
StateAccess []StateAccessKey `json:"stateAccess,omitempty"`
5963
}
6064

65+
// StateAccessKey identifies a named group of sensitive state fields available in {{ .State }} Go templates.
66+
type StateAccessKey string
67+
68+
const (
69+
// StateAccessRegistryCredentials unlocks .State.Registry.{PushPassword,PullPassword,Secret,Htpasswd}.
70+
StateAccessRegistryCredentials StateAccessKey = "registryCredentials"
71+
// StateAccessGitCredentials unlocks .State.Git.{PushPassword,PullPassword}.
72+
StateAccessGitCredentials StateAccessKey = "gitCredentials"
73+
// StateAccessAgentCerts unlocks .State.Agent.{CA,Cert,Key} (base64-encoded) and adds the .State.Agent sub-object.
74+
StateAccessAgentCerts StateAccessKey = "agentCerts"
75+
)
76+
6177
// ImageArchive points to an archived file containing an OCI layout
6278
type ImageArchive struct {
6379
// Path to file containing an OCI-layout

src/internal/packager/template/template.go

Lines changed: 1 addition & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import (
1717
"github.com/zarf-dev/zarf/src/config"
1818
"github.com/zarf-dev/zarf/src/pkg/interactive"
1919
"github.com/zarf-dev/zarf/src/pkg/logger"
20-
"github.com/zarf-dev/zarf/src/pkg/utils"
2120
"github.com/zarf-dev/zarf/src/pkg/variables"
2221
)
2322

@@ -81,7 +80,7 @@ func GetZarfTemplates(ctx context.Context, componentName string, s *state.State)
8180

8281
case "zarf-seed-registry", "zarf-registry":
8382
builtinMap["SEED_REGISTRY"] = state.LocalhostRegistryAddress(s.IPFamily, s.InjectorInfo.Port)
84-
htpasswd, err := generateHtpasswd(&regInfo)
83+
htpasswd, err := regInfo.Htpasswd()
8584
if err != nil {
8685
return templateMap, err
8786
}
@@ -110,26 +109,6 @@ func GetZarfTemplates(ctx context.Context, componentName string, s *state.State)
110109
return templateMap, nil
111110
}
112111

113-
// generateHtpasswd returns an htpasswd string for the current state's RegistryInfo.
114-
func generateHtpasswd(regInfo *state.RegistryInfo) (string, error) {
115-
// Only calculate this for internal registries to allow longer external passwords
116-
if regInfo.IsInternal() {
117-
pushUser, err := utils.GetHtpasswdString(regInfo.PushUsername, regInfo.PushPassword)
118-
if err != nil {
119-
return "", fmt.Errorf("error generating htpasswd string: %w", err)
120-
}
121-
122-
pullUser, err := utils.GetHtpasswdString(regInfo.PullUsername, regInfo.PullPassword)
123-
if err != nil {
124-
return "", fmt.Errorf("error generating htpasswd string: %w", err)
125-
}
126-
127-
return fmt.Sprintf("%s\\n%s", pushUser, pullUser), nil
128-
}
129-
130-
return "", nil
131-
}
132-
133112
func debugPrintTemplateMap(ctx context.Context, templateMap map[string]*variables.TextTemplate) {
134113
sanitizedMap := getSanitizedTemplateMap(templateMap)
135114
logger.From(ctx).Debug("cluster.debugPrintTemplateMap", "templateMap", sanitizedMap)

src/internal/template/template.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@ package template
77
import (
88
"bytes"
99
"context"
10+
"encoding/base64"
1011
"encoding/json"
1112
"fmt"
1213
"maps"
1314
"os"
1415
"path/filepath"
16+
"slices"
1517
"strings"
1618
ttmpl "text/template"
1719
"time"
@@ -21,6 +23,7 @@ import (
2123
"github.com/goccy/go-yaml"
2224
"github.com/zarf-dev/zarf/src/api/v1alpha1"
2325
"github.com/zarf-dev/zarf/src/pkg/logger"
26+
"github.com/zarf-dev/zarf/src/pkg/state"
2427
"github.com/zarf-dev/zarf/src/pkg/value"
2528
"github.com/zarf-dev/zarf/src/pkg/variables"
2629
)
@@ -40,6 +43,7 @@ const (
4043
objectKeyBuild = "Build"
4144
objectKeyConstants = "Constants"
4245
objectKeyVariables = "Variables"
46+
objectKeyState = "State"
4347
)
4448

4549
// NewObjects instantiates an Objects map, which provides templating context. The "with" options below allow for
@@ -105,6 +109,85 @@ func (o Objects) WithPackage(pkg v1alpha1.ZarfPackage) Objects {
105109
return o
106110
}
107111

112+
// StateAccess bundles the runtime state with the named access groups a component has declared.
113+
// A zero value (nil State) is safe and causes WithState to be a no-op.
114+
type StateAccess struct {
115+
// State is the Zarf runtime state loaded from the cluster.
116+
State *state.State
117+
// AccessKeys lists which groups of sensitive state fields the component may access.
118+
// Accessing a field whose group is not listed causes a template error (missingkey=error).
119+
AccessKeys []v1alpha1.StateAccessKey
120+
}
121+
122+
// WithState adds Zarf runtime state to the template Objects under the "State" key.
123+
// Non-sensitive fields (addresses, usernames, counts) are always included.
124+
// Sensitive field groups are only included when the corresponding StateAccessKey is present in
125+
// access.AccessKeys — accessing an absent group causes a template error (missingkey=error).
126+
// If access.State is nil, the Objects map is returned unchanged.
127+
func (o Objects) WithState(access StateAccess) (Objects, error) {
128+
s := access.State
129+
if s == nil {
130+
return o, nil
131+
}
132+
133+
registry := map[string]any{
134+
"Address": s.RegistryInfo.Address,
135+
"Port": s.RegistryInfo.Port,
136+
"PushUsername": s.RegistryInfo.PushUsername,
137+
"PullUsername": s.RegistryInfo.PullUsername,
138+
"Mode": string(s.RegistryInfo.RegistryMode),
139+
"MTLSEnabled": s.RegistryInfo.ShouldUseMTLS(),
140+
"SeedAddress": state.LocalhostRegistryAddress(s.IPFamily, s.InjectorInfo.Port),
141+
}
142+
git := map[string]any{
143+
"Address": s.GitServer.Address,
144+
"PushUsername": s.GitServer.PushUsername,
145+
"PullUsername": s.GitServer.PullUsername,
146+
"IsInternal": s.GitServer.IsInternal(),
147+
}
148+
injector := map[string]any{
149+
"Image": s.InjectorInfo.Image,
150+
"Port": s.InjectorInfo.Port,
151+
"PayloadConfigMaps": s.InjectorInfo.PayLoadConfigMapAmount,
152+
"PayloadShaSum": s.InjectorInfo.PayLoadShaSum,
153+
}
154+
155+
if slices.Contains(access.AccessKeys, v1alpha1.StateAccessRegistryCredentials) {
156+
registry["PushPassword"] = s.RegistryInfo.PushPassword
157+
registry["PullPassword"] = s.RegistryInfo.PullPassword
158+
registry["Secret"] = s.RegistryInfo.Secret
159+
htpasswd, err := s.RegistryInfo.Htpasswd()
160+
if err != nil {
161+
return o, fmt.Errorf("generating htpasswd for .State.Registry.Htpasswd: %w", err)
162+
}
163+
registry["Htpasswd"] = htpasswd
164+
}
165+
if slices.Contains(access.AccessKeys, v1alpha1.StateAccessGitCredentials) {
166+
git["PushPassword"] = s.GitServer.PushPassword
167+
git["PullPassword"] = s.GitServer.PullPassword
168+
}
169+
170+
stateMap := map[string]any{
171+
"Distro": s.Distro,
172+
"StorageClass": s.StorageClass,
173+
"IPFamily": string(s.IPFamily),
174+
"Registry": registry,
175+
"Git": git,
176+
"Injector": injector,
177+
}
178+
179+
if slices.Contains(access.AccessKeys, v1alpha1.StateAccessAgentCerts) {
180+
stateMap["Agent"] = map[string]any{
181+
"CA": base64.StdEncoding.EncodeToString(s.AgentTLS.CA),
182+
"Cert": base64.StdEncoding.EncodeToString(s.AgentTLS.Cert),
183+
"Key": base64.StdEncoding.EncodeToString(s.AgentTLS.Key),
184+
}
185+
}
186+
187+
o[objectKeyState] = stateMap
188+
return o, nil
189+
}
190+
108191
// Apply takes a string, fills in the templates with the given Objects, and returns a new string.
109192
func Apply(ctx context.Context, s string, objs Objects) (string, error) {
110193
l := logger.From(ctx)

0 commit comments

Comments
 (0)