CUE Lang: From Strict Validation to Dynamic Kubernetes Manifests
Managing Kubernetes configurations through traditional Helm charts and static YAML can quickly escalate into a complex maintenance burden. Code repetition, lack of strict type validation, and the sheer difficulty of managing exceptions make manifests fragile.
This is where CUE (Configure, Unify, Execute) steps in. Operating on lattice theory mathematics, CUE acts simultaneously as a rigorous schema validator and a powerful configuration generation engine.
CUE is much more than a simple validation tool for YAML; it is a powerful data constraint language designed to bring order to the chaos of modern infrastructure. By treating configuration as a logical graph rather than just static text, CUE allows platform engineers and developers to move beyond the limitations of manual tagging and copy-paste workflows.
Whether you are using it to:
- Enforce Strict Policies: Ensuring that every service in your cluster follows security and naming conventions.
- Generate Boilerplate: Reducing thousands of lines of repetitive YAML into a few clean, maintainable CUE templates.
- Unify Multiple Sources: Acting as a “Single Source of Truth” that syncs your Go structs, Protobuf definitions, and Kubernetes manifests.
In short, CUE is to YAML what TypeScript is to JavaScript. It provides the safety, types, and modularity needed to manage complex systems at scale. By adopting CUE, you aren’t just checking for syntax errors—you are building a robust, self-documenting, and automated configuration pipeline that scales with your infrastructure.
The Fundamental CUE Dictionary
To design robust configurations, you must first master the primitive symbols that define CUE’s rules and constraints. Think of this as your schema vocabulary:
- Definitions (
#): Everything that starts with#is not exported to the final YAML file. It serves solely and exclusively for internal validation and structuring. They act as reusable schemas.+1 - Default Values (
*): The asterisk marks the value that CUE automatically chooses if thevalues.yamlfile does not fill in the field. For example, in#AllowedPorts: *80 | 8080 | 443, port 80 is assumed by default. If the*is missing in a disjunction and no value is provided, CUE returns a validation error.+2 - Unification Error (
_|_): This symbol represents a logical failure. In CUE, two configurations can never conflict. If a schema requires port 80, butvalues.yamlstates 8080, CUE evaluates the result as_|_(Bottom), stopping execution with an error.+2 - Optional Fields (
?:): A field followed by?means it is not mandatory for validation to pass. However, if it exists, it must strictly follow the rules defined within its block.+1 - Open Structures (
...): Definitions (schemas with#) are “closed” by default, meaning they do not accept explicitly undeclared keys. The use of...acts as an “escape”, telling CUE to accept other unmapped keys.+1
Advanced Features and the DRY Principle
CUE shines when it comes to eliminating “spaghetti code” and automating complex data generation:
- Hidden Fields (
_): If you need auxiliary variables for logical calculations but do not want them polluting your generated YAML, you can start the field name with an underscore (_). Like Definitions (#), hidden fields are not exported.+1 - String Interpolation: You can dynamically inject values into strings using
\(), which is incredibly practical for generating labels or hostnames based on the environment. - Key Constraints: You can apply rules conditionally based on the dictionary key’s pattern. For example, enforcing strict Semantic Versioning (
SemVer) only on keys that end with_VERSION. - Comprehensions: Unlike Helm templates which can easily break YAML indentation, in CUE you generate pure data. You can use
forloops inside lists or dictionaries.
Instead of writing dozens of static, repetitive lines in your YAML to configure database hosts across different environments, you can generate them dynamically:
// Dynamic configuration generation using comprehensions
"devops-api": #ServiceSchema & {
configMaps: "db-hosts": data: {
for db in _dbList {
for env in _envList {
"\(db)_\(env)": "postgresql.\(_baseDomain)"
}
}
}
}
Composition and Unification
CUE does not use classical object-oriented inheritance. Instead, it relies on Composition. The & operator strictly merges schemas and business rules.
If you want to create a secure but flexible architecture for Docker images, you can compose a strict core schema with an open extension:
// Base strict rules (Closed by default)
#ImageCore: {
repository: =~"^nexus\\.devops-db\\.internal/.*"
tag: =~"^[0-9]+\\.[0-9]+\\.[0-9]+$"
pullPolicy: *"Always" | "IfNotPresent" | "Never"
}
// Composing rules to allow unmapped fields
#ImageOpen: #ImageCore & {
...
}
By enforcing #ImageOpen, you guarantee mandatory restrictions (like pointing to the internal Nexus registry and using SemVer tags), while gracefully allowing the team to inject other native Kubernetes attributes, like imagePullSecrets, without breaking the pipeline.
The CLI Workflow: Vet, Eval, and Export
When managing infrastructure and tweaking your logic using vi, the CUE ecosystem provides purpose-built tools for different stages of the lifecycle.
1. The Silent Validator (cue vet)
The vet command does not convert formats or generate new files. Its sole job is to audit and validate your data against the schema.
- Success: It prints absolutely nothing on the screen. It returns an exit code of 0, making it the perfect tool for a CI/CD pipeline.+1
- Failure: It returns an exit code of 1 and details exactly where the rule failed.
Bash
# Validates the YAML against the CUE schema silently
cue vet config.cue values.yaml
2. The Debugging Inspector (cue eval)
After editing complex rules with vi, this command shows what CUE is “thinking” before final export. It displays the unified state, evaluating all hidden variables and applied interpolations.+1
Bash
# Evaluates and prints the unified configuration tree
cue eval config.cue values.yaml
3. The Final Generator (cue export)
This is the conversion engine. This command takes the unified data tree and converts it to an external format. If there is any validation error, export also refuses to generate the final file. It seamlessly supports YAML, JSON, and text outputs.+2
Bash
# Exports the evaluated data to a standard YAML format
cue export config.cue values.yaml --out yaml
Adopting these concepts transforms purely descriptive YAML manifests into mathematically validated infrastructure, ultimately eliminating deployment surprises in production environments.
Full CUE code used in this example:
config.cue
package config
import (
"strings"
)
// 1. INTERNAL VARIABLES (Hidden fields)
// These start with an underscore and will not be exported to the final YAML.
_baseDomain: "devops-db.internal"
_dbList: ["customers", "inventory", "orders", "payment", "reporting"]
_envList: ["dev", "staging", "prod"]
// 2.1. DEFINITION (Semi-open Struct)
#GlobalConfig: {
// Limits the environment to specific strings, defaulting to "dev"
environment: *"dev" | "staging" | "prod"
// Allow any other unspecified fields to pass validation
...
}
// 2.2. GLOBAL CONFIGURATION
global: #GlobalConfig
// 3. REUSABLE SCHEMAS & LOGIC
#ReplicaRules: int & >=1
// Conditionally adjust rules based on the global environment
if global.environment == "prod" {
#ReplicaRules: >=2
}
#IngressRules: {
enabled: bool | *false
host?: string
// Cross-field validation: if enabled is true, host MUST be present
// and must end with our internal domain.
if enabled {
host: string & strings.HasSuffix(_baseDomain)
}
}
#ImageRules: {
// Validates repository domain dynamically using string interpolation
repository: string & strings.HasPrefix("nexus.\(_baseDomain)/")
// Semantic versioning validation
tag: string & =~"^v?[0-9]+\\.[0-9]+\\.[0-9]+$"
pullPolicy: *"Always" | "IfNotPresent" | "Never"
...
}
// ImageRules + ... to have the exceptions
#ImageOpen: #ImageRules & {
...
}
#SecretRules: {
type: *"Opaque" | string
// Validates that all values inside 'data' follow a basic Base64 regex pattern
data?: [string]: string & =~"^[A-Za-z0-9+/]+={0,2}$"
}
// 4. MAIN COMPONENT SCHEMA
#ServiceSchema: {
replicaCount: *1 | #ReplicaRules
ingress?: #IngressRules
image: #ImageRules
service: {
port: *80 | 8080 | 443
}
secrets?: [string]: #SecretRules
configMaps?: [string]: {
data: [string]: string
}
// Advanced map validation: if a key ends in _VERSION, the value must be a valid semver string
env?: [Key=string]: Value=(string | bool) & {
if strings.HasSuffix(Key, "_VERSION") {
Value & string & =~"^v?[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
...
}
// 5. SERVICE BINDING & DATA GENERATION
"devops-api": #ServiceSchema & {
// Automatically generating the massive configMap data
// This allows you to delete all the repetitive lines from values.yaml
configMaps: "db-hosts": data: {
for db in _dbList {
for env in _envList {
"\(db)_\(env)": "postgresql.\(_baseDomain)"
}
}
}
}
"iplocation-api": #ServiceSchema
"totp-api": #ServiceSchemaAnd this is the YAML file used for validation:
global:
environment: dev
imagePullSecrets:
- name: regcred
devops-api:
replicaCount: 1
ingress:
enabled: true
host: devops-api.devops-db.internal
image:
repository: nexus.devops-db.internal/devops_images/devops-api
tag: "1.1.2"
service:
port: 80
secrets:
devops-api:
type: Opaque
data:
GITLAB_TOKEN: Z2xwYXQtdWN0WEhTcDN6cW5MQUsxQmJSMXRhRzg2TVFwMU9qWUguMDEuMHcxaXJydWly
POSTGRES_PASSWORD: MTIzNHF3ZXI=
POSTGRES_USER: ZGV2b3BzX2FwaQ==
env:
DEVOPSAPI_VERSION: v1.1.2
configMaps:
db-hosts:
data:
customers_dev: postgresql.devops-db.internal
inventory_dev: postgresql.devops-db.internal
orders_dev: postgresql.devops-db.internal
payment_dev: postgresql.devops-db.internal
reporting_dev: postgresql.devops-db.internal
customers_staging: postgresql.devops-db.internal
inventory_staging: postgresql.devops-db.internal
orders_staging: postgresql.devops-db.internal
payment_staging: postgresql.devops-db.internal
reporting_staging: postgresql.devops-db.internal
customers_prod: postgresql.devops-db.internal
inventory_prod: postgresql.devops-db.internal
orders_prod: postgresql.devops-db.internal
payment_prod: postgresql.devops-db.internal
reporting_prod: postgresql.devops-db.internal
iplocation-api:
replicaCount: 1
ingress:
enabled: true
host: iplocation-api.devops-db.internal
image:
repository: nexus.devops-db.internal/devops_images/iplocation
pullPolicy: IfNotPresent
tag: "1.0.0"
service:
port: 80
env:
IPLOCATOR_VERSION: v1.0.0
totp-api:
replicaCount: 1
ingress:
enabled: true
host: totp-api.devops-db.internal
image:
repository: nexus.devops-db.internal/devops_images/totp-validator
pullPolicy: IfNotPresent
tag: "1.0.0"
service:
port: 80
env:
TOTPVALIDATOR_VERSION: v1.0.0 Validating Structures in CUE: Open vs. Closed Structs
When writing CUE, standard structs are “open” by default. This means that if you unify your CUE code with a YAML file containing extra fields (such as imagePullSecrets), CUE will simply merge them without throwing an error, as long as the explicitly defined fields (like environment) are valid.
However, if you want strict validation where undeclared fields trigger an error, you must use Definitions. Definitions are declared with a hash (#) and behave as “closed” structs. When you apply a definition to a configuration block, CUE rejects any key that is not explicitly expected.
Here are three ways to implement this validation, depending on your desired behavior:
1. Strict Validation (Throws an Error)
In this scenario, we create a strict definition. If your YAML contains an unspecified field like imagePullSecrets, the validation will immediately fail.
// schema.cue
// 2.1. DEFINITION (Strict/Closed Struct)
#GlobalConfig: {
// Limits the environment to specific strings, defaulting to "dev"
environment: *"dev" | "staging" | "prod"
}
// 2.2. GLOBAL CONFIGURATION
// Apply the strict definition to the global key
global: #GlobalConfig
Running cue vet config.yaml schema.cue against a YAML with imagePullSecrets will return a validation error similar to: global.imagePullSecrets: field not allowed.
2. Optional Typing (Recommended)
If you know that imagePullSecrets might exist in the YAML, the best practice is to include it in your definition as an optional field (using the ? token). This keeps the struct closed and secure, but teaches CUE how to validate the secrets if they are present.
// schema.cue
// 2.1. DEFINITION (Strict/Closed Struct)
#GlobalConfig: {
// Limits the environment to specific strings, defaulting to "dev"
environment: *"dev" | "staging" | "prod"
// Explicitly allow imagePullSecrets as an optional field
imagePullSecrets?: [...{
name: string
}]
}
// 2.2. GLOBAL CONFIGURATION
global: #GlobalConfig
This approach passes validation and guarantees that if a secret is added, it must strictly be a list containing a name key of type string.
3. Strict Validation with an “Open Door”
If you do not want to define imagePullSecrets or any other future fields that might appear in the YAML, you can use an ellipsis (...) inside your definition to explicitly make it “open”.
// schema.cue
// 2.1. DEFINITION (Semi-open Struct)
#GlobalConfig: {
// Limits the environment to specific strings, defaulting to "dev"
environment: *"dev" | "staging" | "prod"
// Allow any other unspecified fields to pass validation
...
}
// 2.2. GLOBAL CONFIGURATION
global: #GlobalConfig
With this version, CUE focuses solely on ensuring the environment is correct ("dev", "staging", or "prod"). Everything else injected into the YAML under the global key is ignored by the validator and accepted without errors.
// values.yaml
global:
imagePullSecrets:
- name: regcred
cue export config.cue values.yaml
{
"global": {
"environment": "dev",
"imagePullSecrets": [
{
"name": "regcred"
}
]
},
[...] Missing YAML Keys and Default Values
When validating YAML files against a CUE schema, a common question arises: What happens if the root key (like global) is entirely missing from the YAML?
If your CUE file applies a definition to the global key, the validation will not fail as long as all fields inside that definition have default values.
For example, given this setup:
// schema.cue
// 2.1. DEFINITION
#GlobalConfig: {
// Limits the environment to specific strings, defaulting to "dev"
environment: *"dev" | "staging" | "prod"
}
// 2.2. GLOBAL CONFIGURATION
global: #GlobalConfig
If the values.yaml file is completely empty or missing the global key, running cue vet config.cue values.yaml will succeed. This happens because CUE unifies the data from all sources. Since the only field inside #GlobalConfig has a default value (*"dev"), the compiler has all the information it needs to independently build a valid and complete object.
// values.yaml
global:
imagePullSecrets:
- name: regcred
cue export config.cue values.yaml
global.imagePullSecrets: field not allowed:
./values.yaml:2:3When will it fail?
The validation will only fail if your definition contains at least one required field that does not have a default value.
// schema.cue
// 2.1. DEFINITION
#GlobalConfig: {
// Has a default, so it passes if omitted
environment: *"dev" | "staging" | "prod"
// Required field without a default value
appName: string
}
// 2.2. GLOBAL CONFIGURATION
global: #GlobalConfig
In this scenario, if the YAML lacks the global block (or includes it but misses the appName key), the validation will fail with an “incomplete value” error. This occurs because CUE strictly requires a value for appName to complete the configuration, but neither the schema nor the YAML provided one.
// values.yaml
global:
environment: dev
cue export config.cue values.yaml
global.appName: incomplete value string:
./config.cue:19:14
Examples
Now, let’s insert some errors to illustrate:
tag: "1.1"
host: totp-api.devops-db.local
port: 80000 And running the vet to validate, unfortunately, cue doesn’t export in other formats besides this one; a JSON would be very helpful in pipelines, for example, but nothing that some intermediate code can’t solve.
cue vet config.cue values.yaml
"devops-api".service.port: 3 errors in empty disjunction:
"devops-api".service.port: conflicting values 443 and 80000:
./config.cue:64:28
./config.cue:81:15
./values.yaml:13:11
"devops-api".service.port: conflicting values 80 and 80000:
./config.cue:64:16
./config.cue:81:15
./values.yaml:13:11
"devops-api".service.port: conflicting values 8080 and 80000:
./config.cue:64:21
./config.cue:81:15
./values.yaml:13:11
"totp-api".ingress.host: invalid value "totp-api.devops-db.local" (does not satisfy strings.HasSuffix("devops-db.internal")):
./config.cue:33:24
./config.cue:9:14
./config.cue:29:14
./config.cue:32:5
./values.yaml:60:11
"devops-api".image.tag: invalid value "1.1" (out of bound =~"^v?[0-9]+\\.[0-9]+\\.[0-9]+$"):
./config.cue:41:26
./config.cue:41:17
./values.yaml:11:10Let’s take advantage of the first section: global and remove it completely from the YAML, to exemplify the structures in CUE: Open vs. Closed Structs:
If we run vet, the error is shown because it’s a required section.
If we export it, the default environment value is shown (*dev).
The Fundamental CUE Dictionary
To design robust configurations, you must first master the primitive symbols that define CUE’s rules and constraints. Think of this as your schema vocabulary:
- Definitions (
#): Everything that starts with#is not exported to the final YAML file. It serves solely and exclusively for internal validation and structuring. They act as reusable schemas.+1 - Default Values (
*): The asterisk marks the value that CUE automatically chooses if thevalues.yamlfile does not fill in the field. For example, in#AllowedPorts: *80 | 8080 | 443, port 80 is assumed by default. If the*is missing in a disjunction and no value is provided, CUE returns a validation error.+2 - Unification Error (
_|_): This symbol represents a logical failure. In CUE, two configurations can never conflict. If a schema requires port 80, butvalues.yamlstates 8080, CUE evaluates the result as_|_(Bottom), stopping execution with an error.+2 - Optional Fields (
?:): A field followed by?means it is not mandatory for validation to pass. However, if it exists, it must strictly follow the rules defined within its block.+1 - Open Structures (
...): Definitions (schemas with#) are “closed” by default, meaning they do not accept explicitly undeclared keys. The use of...acts as an “escape”, telling CUE to accept other unmapped keys.+1
Advanced Features and the DRY Principle
CUE shines when it comes to eliminating “spaghetti code” and automating complex data generation:
- Hidden Fields (
_): If you need auxiliary variables for logical calculations but do not want them polluting your generated YAML, you can start the field name with an underscore (_). Like Definitions (#), hidden fields are not exported.+1 - String Interpolation: You can dynamically inject values into strings using
\(), which is incredibly practical for generating labels or hostnames based on the environment. - Key Constraints: You can apply rules conditionally based on the dictionary key’s pattern. For example, enforcing strict Semantic Versioning (
SemVer) only on keys that end with_VERSION. - Comprehensions: Unlike Helm templates which can easily break YAML indentation, in CUE you generate pure data. You can use
forloops inside lists or dictionaries.
Instead of writing dozens of static, repetitive lines in your YAML to configure database hosts across different environments, you can generate them dynamically:
// Dynamic configuration generation using comprehensions
"devops-api": #ServiceSchema & {
configMaps: "db-hosts": data: {
for db in _dbList {
for env in _envList {
"\(db)_\(env)": "postgresql.\(_baseDomain)"
}
}
}
}
Composition and Unification
CUE does not use classical object-oriented inheritance. Instead, it relies on Composition. The & operator strictly merges schemas and business rules.
If you want to create a secure but flexible architecture for Docker images, you can compose a strict core schema with an open extension:
// Base strict rules (Closed by default)
#ImageCore: {
repository: =~"^nexus\\.devops-db\\.internal/.*"
tag: =~"^[0-9]+\\.[0-9]+\\.[0-9]+$"
pullPolicy: *"Always" | "IfNotPresent" | "Never"
}
// Composing rules to allow unmapped fields
#ImageOpen: #ImageCore & {
...
}
By enforcing #ImageOpen, you guarantee mandatory restrictions (like pointing to the internal Nexus registry and using SemVer tags), while gracefully allowing the team to inject other native Kubernetes attributes, like imagePullSecrets, without breaking the pipeline.
The CLI Workflow: Vet, Eval, and Export
When managing infrastructure and tweaking your logic using vi, the CUE ecosystem provides purpose-built tools for different stages of the lifecycle.
1. The Silent Validator (cue vet)
The vet command does not convert formats or generate new files. Its sole job is to audit and validate your data against the schema.
- Success: It prints absolutely nothing on the screen. It returns an exit code of 0, making it the perfect tool for a CI/CD pipeline.+1
- Failure: It returns an exit code of 1 and details exactly where the rule failed.
Bash
# Validates the YAML against the CUE schema silently
cue vet config.cue values.yaml
2. The Debugging Inspector (cue eval)
After editing complex rules with vi, this command shows what CUE is “thinking” before final export. It displays the unified state, evaluating all hidden variables and applied interpolations.+1
Bash
# Evaluates and prints the unified configuration tree
cue eval config.cue values.yaml
3. The Final Generator (cue export)
This is the conversion engine. This command takes the unified data tree and converts it to an external format. If there is any validation error, export also refuses to generate the final file. It seamlessly supports YAML, JSON, and text outputs.+2
Bash
# Exports the evaluated data to a standard YAML format
cue export config.cue values.yaml --out yaml
Adopting these concepts transforms purely descriptive YAML manifests into mathematically validated infrastructure, ultimately eliminating deployment surprises in production environments.
Full CUE code used in this example:
config.cue
package config
import (
"strings"
)
// 1. INTERNAL VARIABLES (Hidden fields)
// These start with an underscore and will not be exported to the final YAML.
_baseDomain: "devops-db.internal"
_dbList: ["customers", "inventory", "orders", "payment", "reporting"]
_envList: ["dev", "staging", "prod"]
// 2.1. DEFINITION (Semi-open Struct)
#GlobalConfig: {
// Limits the environment to specific strings, defaulting to "dev"
environment: *"dev" | "staging" | "prod"
// Allow any other unspecified fields to pass validation
...
}
// 2.2. GLOBAL CONFIGURATION
global: #GlobalConfig
// 3. REUSABLE SCHEMAS & LOGIC
#ReplicaRules: int & >=1
// Conditionally adjust rules based on the global environment
if global.environment == "prod" {
#ReplicaRules: >=2
}
#IngressRules: {
enabled: bool | *false
host?: string
// Cross-field validation: if enabled is true, host MUST be present
// and must end with our internal domain.
if enabled {
host: string & strings.HasSuffix(_baseDomain)
}
}
#ImageRules: {
// Validates repository domain dynamically using string interpolation
repository: string & strings.HasPrefix("nexus.\(_baseDomain)/")
// Semantic versioning validation
tag: string & =~"^v?[0-9]+\\.[0-9]+\\.[0-9]+$"
pullPolicy: *"Always" | "IfNotPresent" | "Never"
...
}
// ImageRules + ... to have the exceptions
#ImageOpen: #ImageRules & {
...
}
#SecretRules: {
type: *"Opaque" | string
// Validates that all values inside 'data' follow a basic Base64 regex pattern
data?: [string]: string & =~"^[A-Za-z0-9+/]+={0,2}$"
}
// 4. MAIN COMPONENT SCHEMA
#ServiceSchema: {
replicaCount: *1 | #ReplicaRules
ingress?: #IngressRules
image: #ImageRules
service: {
port: *80 | 8080 | 443
}
secrets?: [string]: #SecretRules
configMaps?: [string]: {
data: [string]: string
}
// Advanced map validation: if a key ends in _VERSION, the value must be a valid semver string
env?: [Key=string]: Value=(string | bool) & {
if strings.HasSuffix(Key, "_VERSION") {
Value & string & =~"^v?[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
...
}
// 5. SERVICE BINDING & DATA GENERATION
"devops-api": #ServiceSchema & {
// Automatically generating the massive configMap data
// This allows you to delete all the repetitive lines from values.yaml
configMaps: "db-hosts": data: {
for db in _dbList {
for env in _envList {
"\(db)_\(env)": "postgresql.\(_baseDomain)"
}
}
}
}
"iplocation-api": #ServiceSchema
"totp-api": #ServiceSchemaAnd this is the YAML file used for validation:
global:
environment: dev
imagePullSecrets:
- name: regcred
devops-api:
replicaCount: 1
ingress:
enabled: true
host: devops-api.devops-db.internal
image:
repository: nexus.devops-db.internal/devops_images/devops-api
tag: "1.1.2"
service:
port: 80
secrets:
devops-api:
type: Opaque
data:
GITLAB_TOKEN: Z2xwYXQtdWN0WEhTcDN6cW5MQUsxQmJSMXRhRzg2TVFwMU9qWUguMDEuMHcxaXJydWly
POSTGRES_PASSWORD: MTIzNHF3ZXI=
POSTGRES_USER: ZGV2b3BzX2FwaQ==
env:
DEVOPSAPI_VERSION: v1.1.2
configMaps:
db-hosts:
data:
customers_dev: postgresql.devops-db.internal
inventory_dev: postgresql.devops-db.internal
orders_dev: postgresql.devops-db.internal
payment_dev: postgresql.devops-db.internal
reporting_dev: postgresql.devops-db.internal
customers_staging: postgresql.devops-db.internal
inventory_staging: postgresql.devops-db.internal
orders_staging: postgresql.devops-db.internal
payment_staging: postgresql.devops-db.internal
reporting_staging: postgresql.devops-db.internal
customers_prod: postgresql.devops-db.internal
inventory_prod: postgresql.devops-db.internal
orders_prod: postgresql.devops-db.internal
payment_prod: postgresql.devops-db.internal
reporting_prod: postgresql.devops-db.internal
iplocation-api:
replicaCount: 1
ingress:
enabled: true
host: iplocation-api.devops-db.internal
image:
repository: nexus.devops-db.internal/devops_images/iplocation
pullPolicy: IfNotPresent
tag: "1.0.0"
service:
port: 80
env:
IPLOCATOR_VERSION: v1.0.0
totp-api:
replicaCount: 1
ingress:
enabled: true
host: totp-api.devops-db.internal
image:
repository: nexus.devops-db.internal/devops_images/totp-validator
pullPolicy: IfNotPresent
tag: "1.0.0"
service:
port: 80
env:
TOTPVALIDATOR_VERSION: v1.0.0 Validating Structures in CUE: Open vs. Closed Structs
When writing CUE, standard structs are “open” by default. This means that if you unify your CUE code with a YAML file containing extra fields (such as imagePullSecrets), CUE will simply merge them without throwing an error, as long as the explicitly defined fields (like environment) are valid.
However, if you want strict validation where undeclared fields trigger an error, you must use Definitions. Definitions are declared with a hash (#) and behave as “closed” structs. When you apply a definition to a configuration block, CUE rejects any key that is not explicitly expected.
Here are three ways to implement this validation, depending on your desired behavior:
1. Strict Validation (Throws an Error)
In this scenario, we create a strict definition. If your YAML contains an unspecified field like imagePullSecrets, the validation will immediately fail.
// schema.cue
// 2.1. DEFINITION (Strict/Closed Struct)
#GlobalConfig: {
// Limits the environment to specific strings, defaulting to "dev"
environment: *"dev" | "staging" | "prod"
}
// 2.2. GLOBAL CONFIGURATION
// Apply the strict definition to the global key
global: #GlobalConfig
Running cue vet config.yaml schema.cue against a YAML with imagePullSecrets will return a validation error similar to: global.imagePullSecrets: field not allowed.
2. Optional Typing (Recommended)
If you know that imagePullSecrets might exist in the YAML, the best practice is to include it in your definition as an optional field (using the ? token). This keeps the struct closed and secure, but teaches CUE how to validate the secrets if they are present.
// schema.cue
// 2.1. DEFINITION (Strict/Closed Struct)
#GlobalConfig: {
// Limits the environment to specific strings, defaulting to "dev"
environment: *"dev" | "staging" | "prod"
// Explicitly allow imagePullSecrets as an optional field
imagePullSecrets?: [...{
name: string
}]
}
// 2.2. GLOBAL CONFIGURATION
global: #GlobalConfig
This approach passes validation and guarantees that if a secret is added, it must strictly be a list containing a name key of type string.
3. Strict Validation with an “Open Door”
If you do not want to define imagePullSecrets or any other future fields that might appear in the YAML, you can use an ellipsis (...) inside your definition to explicitly make it “open”.
// schema.cue
// 2.1. DEFINITION (Semi-open Struct)
#GlobalConfig: {
// Limits the environment to specific strings, defaulting to "dev"
environment: *"dev" | "staging" | "prod"
// Allow any other unspecified fields to pass validation
...
}
// 2.2. GLOBAL CONFIGURATION
global: #GlobalConfig
With this version, CUE focuses solely on ensuring the environment is correct ("dev", "staging", or "prod"). Everything else injected into the YAML under the global key is ignored by the validator and accepted without errors.
Examples
Now, let’s insert some errors to illustrate:
tag: "1.1"
host: totp-api.devops-db.local
port: 80000 And running the vet to validate, unfortunately, cue doesn’t export in other formats besides this one; a JSON would be very helpful in pipelines, for example, but nothing that some intermediate code can’t solve.
cue vet config.cue values.yaml
"devops-api".service.port: 3 errors in empty disjunction:
"devops-api".service.port: conflicting values 443 and 80000:
./config.cue:64:28
./config.cue:81:15
./values.yaml:13:11
"devops-api".service.port: conflicting values 80 and 80000:
./config.cue:64:16
./config.cue:81:15
./values.yaml:13:11
"devops-api".service.port: conflicting values 8080 and 80000:
./config.cue:64:21
./config.cue:81:15
./values.yaml:13:11
"totp-api".ingress.host: invalid value "totp-api.devops-db.local" (does not satisfy strings.HasSuffix("devops-db.internal")):
./config.cue:33:24
./config.cue:9:14
./config.cue:29:14
./config.cue:32:5
./values.yaml:60:11
"devops-api".image.tag: invalid value "1.1" (out of bound =~"^v?[0-9]+\\.[0-9]+\\.[0-9]+$"):
./config.cue:41:26
./config.cue:41:17
./values.yaml:11:10