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.

https://cuelang.org

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 the values.yaml file 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, but values.yaml states 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 for loops 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":       #ServiceSchema

And 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:3

When 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:10

Let’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 the values.yaml file 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, but values.yaml states 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 for loops 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":       #ServiceSchema

And 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