Features¶
Features are conditional blueprint fragments that enable modular composition of blueprints based on user configuration values. They allow you to conditionally include Terraform components and Kustomizations in your blueprint based on expressions evaluated against your windsor.yaml and values.yaml configurations.
Feature Definition¶
A Feature is defined in a YAML file, typically located in _template/features/ directory of a blueprint:
kind: Feature
apiVersion: blueprints.windsorcli.dev/v1alpha1
metadata:
name: aws-feature
description: AWS-specific infrastructure components
when: provider == 'aws'
terraform:
- path: network/vpc
source: core
inputs:
cidr: ${network.cidr_block ?? "10.0.0.0/16"}
enable_nat: ${network.enable_nat ?? true}
strategy: merge
kustomize:
- name: ingress
path: ingress/base
source: core
components:
- nginx
- nginx/nodeport
substitutions:
domain: ${dns.domain}
strategy: merge
| Field | Type | Description |
|---|---|---|
kind |
string |
Must be Feature. |
apiVersion |
string |
Must be blueprints.windsorcli.dev/v1alpha1. |
metadata |
Metadata |
Feature metadata including name and description. |
when |
string |
Expression that determines if the feature should be applied. Evaluated against configuration values. If empty or evaluates to true, the feature is applied. |
terraform |
[]ConditionalTerraformComponent |
Terraform components to include when the feature matches. |
kustomize |
[]ConditionalKustomization |
Kustomizations to include when the feature matches. |
Features inherit Repository and Sources from the base blueprint they are merged into.
Conditional Logic¶
Features use the Go expr library for expression evaluation. Expressions are evaluated against your configuration values from windsor.yaml and values.yaml.
Expression Syntax¶
Expressions support:
- Equality/inequality:
==,!= - Logical operators:
&&,|| - Parentheses for grouping:
(expression) - Nested object access:
provider,cluster.enabled,vm.driver,cluster.workers.count - String literals: Use single quotes:
'aws','talos','local' - Boolean values:
true,false - Numeric values:
1,2.5 - Null coalescing:
value ?? default - Functions:
values(),jsonnet(),file()
Expression Examples¶
# Simple equality check
when: provider == 'aws'
# Multiple conditions
when: provider == 'aws' && cluster.enabled == true
# Nested property access
when: cluster.enabled == true && cluster.driver == 'talos'
# Complex logical expressions
when: (provider == 'aws' || provider == 'azure') && cluster.enabled == true
# Null coalescing
when: observability.enabled ?? false
ConditionalTerraformComponent¶
Extends TerraformComponent with conditional logic and merge strategy support.
| Field | Type | Description |
|---|---|---|
path |
string |
Path of the Terraform module. Required. |
source |
string |
Source of the Terraform module. Optional. |
inputs |
map[string]any |
Configuration values for the module. Supports ${} expressions. |
dependsOn |
[]string |
Dependencies of this terraform component. |
destroy |
*bool |
Determines if the component should be destroyed during down operations. |
parallelism |
int |
Limits the number of concurrent operations as Terraform walks the graph. |
when |
string |
Expression that determines if this component should be applied. If empty, the component is always applied when the parent feature matches. |
strategy |
string |
Merge strategy: merge (default) or replace. Only available in features. |
Merge Strategies¶
Merge (default):
- Matches on Path and Source
- Deep merges inputs maps (overlay values override base values)
- Appends unique dependencies to dependsOn
- Updates destroy and parallelism if provided
- If no matching component exists, the component is appended
Replace:
- Matches on Path and Source
- Completely replaces the matching component with the new one
- All existing inputs, dependencies, and settings are discarded
- If no matching component exists, the component is appended
Input Evaluation¶
Input values support:
- Expressions: Use
${}syntax (e.g.,${cluster.workers.count},${cluster.workers.count + 2}) - String literals: Plain strings are treated as literals
- Other types: Numbers, booleans, etc. are passed through as-is
Expressions support:
- Direct property access: ${provider}
- Nested access: ${cluster.workers.count}
- Arithmetic: ${cluster.workers.count * 2}
- Null coalescing: ${cluster.endpoint ?? "https://localhost:6443"}
- Functions: ${values(cluster.controlplanes.nodes)}
- File loading: ${jsonnet("config.jsonnet")}, ${file("key.pem")}
ConditionalKustomization¶
Extends Kustomization with conditional logic and merge strategy support.
| Field | Type | Description |
|---|---|---|
name |
string |
Name of the kustomization. Required. |
path |
string |
Path of the kustomization. Required. |
source |
string |
Source of the kustomization. Optional. |
dependsOn |
[]string |
Dependencies of this kustomization. |
components |
[]string |
Components to include in the kustomization. |
patches |
[]BlueprintPatch |
Patches to apply to the kustomization. |
substitutions |
map[string]string |
Values for post-build variable replacement. Supports ${} expressions. |
when |
string |
Expression that determines if this kustomization should be applied. If empty, the kustomization is always applied when the parent feature matches. |
strategy |
string |
Merge strategy: merge (default) or replace. Only available in features. |
Merge Strategies¶
Merge (default):
- Matches on Name
- Appends unique components to the components array
- Appends unique dependencies to dependsOn
- Updates path, source, and destroy if provided
- Appends patches to the existing patches array
- If no matching kustomization exists, the kustomization is appended
Replace:
- Matches on Name
- Completely replaces the matching kustomization with the new one
- All existing components, dependencies, patches, and settings are discarded
- If no matching kustomization exists, the kustomization is appended
Substitutions¶
Substitutions are:
- Converted to strings (as required by Flux post-build substitution)
- Stored in ConfigMaps
- Available in Kubernetes manifests via Flux's postBuild substitution
- Support ${} expression interpolation
Kustomization Patches¶
Patches support expression interpolation in the patch field content using ${} syntax.
Blueprint Format (path-based):
kustomize:
- name: my-app
path: apps/my-app
patches:
- path: patches/custom-patch.yaml
Flux Format (inline with target selector):
kustomize:
- name: my-app
path: apps/my-app
patches:
- patch: |-
- op: replace
path: /spec/replicas
value: ${replicas ?? 3}
target:
kind: Deployment
name: my-app
namespace: default
Patch content in the patch field supports expression interpolation:
- Direct property access: ${dns.domain}
- Nested access: ${cluster.workers.count}
- Null coalescing: ${replicas ?? 3}
- String interpolation: "${context}-config"
When using the merge strategy (default), patches are appended to existing patches. When using the replace strategy, all existing patches are discarded and replaced with the new patches.
File Loading Functions¶
Features support two file loading functions for dynamic configuration:
jsonnet()¶
Loads and evaluates a Jsonnet file:
terraform:
- path: cluster/talos
source: core
inputs:
config: ${jsonnet("talos-config.jsonnet")}
worker_patches: ${jsonnet("talos-config.jsonnet").worker_config_patches}
- Takes a relative path to a
.jsonnetfile - Evaluates the Jsonnet file with access to configuration values
- Returns the evaluated result (can be any JSON-compatible type)
- Paths are relative to the feature file location
file()¶
Loads a file as a string:
kustomize:
- name: ingress
path: ingress/base
substitutions:
tls_cert: ${file("certs/tls.crt")}
tls_key: ${file("certs/tls.key")}
- Takes a relative path to any file
- Returns the file contents as a string
- Paths are relative to the feature file location
Path Resolution¶
Both functions resolve paths relative to the feature file location:
- Feature at _template/features/aws.yaml can reference config.jsonnet in the same directory
- Use ../configs/config.jsonnet for files in parent directories
- Paths work with both local filesystem and in-memory template data (from archives)
Feature Loading¶
Features are automatically loaded from:
- _template/features/*.yaml - Individual feature files
- _template/features/**/*.yaml - Nested feature directories
Features are processed in alphabetical order by name, then merged into the base blueprint.