Compare commits

..

59 Commits

Author SHA1 Message Date
Jeff McCune
0061e7311a fixup 2025-01-24 13:50:29 -08:00
Jeff McCune
d5fb9037e2 docs: flatten a complex hierarchy
Migrating the helm hierarchy from the original example wasn't a great
example because the hierarchy was simple and there were few overrides.

This change goes further with a second example that generates a complex
hierarchy with 11 layers of overrides and data placed randomly through
the levels.   The document shows how holos helps flatten the hierarchy
into one layer to simplify the configuration.
2025-01-24 12:57:37 -08:00
Jeff McCune
70c76daddb docs: Unify Values - Part 2
The purpose of this post is to provide a simpler solution.  Accidental
complexity is eliminated by refactoring the 5 level Helm value hierarchy
into one single unified CUE struct.
2025-01-21 15:32:14 -08:00
Jeff McCune
ad2bf5dde1 docs: upgrade docusaurus to 3.7.0
npm i @docusaurus/core@latest @docusaurus/plugin-client-redirects@latest \
    @docusaurus/preset-classic@latest @docusaurus/theme-mermaid@latest \
    @docusaurus/module-type-aliases@latest @docusaurus/tsconfig@latest \
    @docusaurus/types@latest
2025-01-21 13:31:19 -08:00
Jeff McCune
44c2fe220a test: fix helm capabilities test
Helm was upgraded in GitHub Actions resulting in an accidental failure
of the test case.
2025-01-17 12:33:28 -08:00
Jeff McCune
fe1ae2fa80 docs: migrate from an ApplicationSet blog post 2025-01-17 12:22:56 -08:00
Jeff McCune
e9d1240d63 docs: make update-docs for version 0.103.0 2025-01-12 14:26:27 -08:00
Gary Larizza
03fa4eaaa2 docs: Helm Values test updates
* Convert all files with.period.separators to hyphen-separators.
* Rename and markdown_test.go to be specific to Helm Values.
* Move helm-values_test.go to be in the same directory as the Helm Values doc.
* Move Blackbox common configuration CUE file to `config/prometheus` so it can be imported as necessary.
* Use explicit import statements for Blackbox common config in `blackbox` and `prometheus` components.

Closes: #399
2025-01-12 14:25:44 -08:00
Jeff McCune
e363f3a597 docs: add make update-docs task
We need to run this prior to tagging a release otherwise the tests fail
for the new version string.
2025-01-12 14:22:58 -08:00
Jeff McCune
8b49ed93be docs: release version 0.103.0 2025-01-12 14:09:45 -08:00
Jeff McCune
d2be9fe278 helm: add valueFiles for migration from an ApplicationSet
Without this patch migrating from [helm hierarchies] to Holos requires
the user to unify the value hierarchy.  This is a problem because helm
hierarchies are difficult to unify because it's not clear if or why a
value is used in the final results.  This makes it difficult to identify
how to resolve conflicts.

This patch adds `valueFiles` field to the Helm component kind.  This
field is intended to provide a direct migration path from the
ApplicationSet.spec.template.spec.sources.helm.valueFiles field.  With
this patch, users can directly migrate the values files to CUE using
`@embed`, then directly migrate the valueFiles field to reference the
values from within CUE.

Note we actively discourage the use of Helm value hierarchies.  The
feature is intended as a temporary migration tool.  We encourage the use
of CUE unification instead.  After migration, the valueFiles field
should be refactored to the values field as one unified structure in
CUE.  The valueFiles field makes this second order migration easier
becuase we can inspect and verify the complete rendered output, allowing
us to determine if a value is actually used in the final configuration
or is overridden.

[helm hierarchies]: https://medium.com/containers-101/using-helm-hierarchies-in-multi-source-argo-cd-applications-for-promoting-to-different-gitops-133c3bc93678
2025-01-12 13:30:29 -08:00
Jeff McCune
6ec341bbb1 docs: redirect /docs/api/core 2025-01-10 15:02:12 -08:00
Jeff McCune
13a4305b78 docs: add redirect for /blog/rendered-manifest-pattern (manifest instead of manifests) 2025-01-10 14:50:26 -08:00
Jeff McCune
0cfce3a823 docs: redirect rendered manifests pattern for now
Need a URL we can redirect when we publish our own variation on the
pattern with a link back to Akuity.
2025-01-10 10:55:06 -08:00
Jeff McCune
61d7539e1c docs: fix /docs/guides/ redirect 2025-01-09 16:03:50 -08:00
Jeff McCune
bf84724137 docs: add redirects for github.com/holos-run readme 2025-01-09 15:11:04 -08:00
Jeff McCune
9f0de7555c init: change to holos.example default cue module
Match the cue mod init behavior of a module named `cue.example`.
2025-01-09 13:57:26 -08:00
Gary Larizza
650636f944 Merge pull request #393 from holos-run/gl/update-helm-docs
Update Helm Values Tutorial to use testscript
2025-01-09 12:01:09 -08:00
Gary Larizza
b28c110694 Update Helm Values tutorial to use testscript
PROBLEM:

The Helm Values tutorial contains a fair bit of code/scripts, and we
need a way to test the steps we recommend to make sure nothing breaks
or slips out of date.

SOLUTION:

* Use `testscript` as a way to automate the execution of the steps in the doc and verify that none of the steps produce errors.
* Update the MDX file to directly reference the files embedded into the testscript.

OUTCOME:

* We have an automated way to perform the steps in the Helm Values document.
* We have unit tests that will fail should any of the commands being executed in the doc fail.
* The doc's MDX file directly references the files within the testscript, so we only need to modify the MDX file to update wording.
2025-01-09 11:53:53 -08:00
Gary Larizza
5bb3e90b38 Install raw-loader module
We use this module within our markdown tutorials (like the Helm Values
tutorial) to load in files generated by testscript.
2025-01-09 11:53:13 -08:00
Jeff McCune
6a60b613ff render: fix selectors (#394)
Without this patch selectors don't work as expected.  This patch
fixes selectors such that each --selector flag value configures one
selector containing multiple positive or negative label matchers.

Result:

Render build plans for cluster dev or cluster test.  Note the use of two
flags indicating logical OR.

    holos render platform --selector cluster=test --selector cluster=dev
    rendered external-secrets for cluster test in 299.897542ms
    rendered external-secrets for cluster dev in 299.9225ms
    rendered external-secrets-crds for cluster test in 667.6075ms
    rendered external-secrets-crds for cluster dev in 708.126541ms
    rendered platform in 708.795625ms

Render build plans for prod clusters that are not customer facing.  Note
the use of one selector with comma separated labels.

    holos render platform --selector "tier=prod,scope!=customer"
2025-01-08 21:09:00 -08:00
Jeff McCune
5862725bab builder: deprecate ExtractYAML, use cue embed instead
Easier to place the data, better supported in the ecosystem.
2025-01-02 18:53:10 -08:00
Jeff McCune
8660826b05 builder: protect LoadInstance with a mutex
CUE is not safe for concurrent access so we protect the main
LoadInstance function with a mutex lock.
2025-01-02 17:32:53 -08:00
Jeff McCune
449df91e33 docs: app.holos.run/description not cli
The core component documentation on the annotation used to configure the
display line for each rendered component was incorrect.
2025-01-02 08:36:37 -08:00
Jeff McCune
ac59173b30 ci: update holos-run/holos-action version (try 3)
Fix the use of digests when pulling and pushing images.  Pull the image
from ghcr.io before pushing it to quay.io
2024-12-23 10:33:45 -08:00
Jeff McCune
fb75e560fc ci: update holos-run/holos-action version (try 2)
When new container image versions are built, automatically update the
holos-run/holos-action to use the new version.

Users of the action automatically update by default as a result.
2024-12-23 09:52:09 -08:00
Jeff McCune
69a064e3ea ci: update holos-run/holos-action version
When new container image versions are built, automatically update the
holos-run/holos-action to use the new version.

Users of the action automatically update by default as a result.
2024-12-23 07:23:36 -08:00
Jeff McCune
71b72807bb ci: tag v0.102.1 for container images
We need a released tag to reference in workflows that use the container
image to render the platform configuration.

This is the first image, subsequent git tags will also build and publish
container images.
2024-12-21 08:08:51 -08:00
Jeff McCune
0e4ecf9d13 ci: fix error in containers.yaml 2024-12-21 07:33:31 -08:00
Jeff McCune
ec2fdadd44 ci: build container from any ref
Too hard to try and build back in time, so let's just get it working
then build containers going forward for tags.
2024-12-21 07:31:09 -08:00
Jeff McCune
38b082095f ci: drop linux/arm/v7 support
There aren't kubectl images to build against.
2024-12-21 07:14:21 -08:00
Jeff McCune
f9346ea7c0 ci: use Dockerfile from main when building tags
Problem: We can't build old tags because the wrong Dockerfile is used
from the old tag.

Solution: Save the Dockerfile from main and use it to build the tag.
This create a dirty working directory but that's OK.
2024-12-21 07:11:29 -08:00
Jeff McCune
0f7010288a ci: build distroless container image for holos
Push it to ghcr and quay.

 * sign images with cosign and oidc id token
 * add kustomize v5.5.0 to tools for distroless image

Usage:

    docker run -v $(pwd):/app -w /app --rm -it ghcr.io/holos-run/holos:v0.101.8 holos render platform
2024-12-21 06:58:57 -08:00
Jeff McCune
386fb89cc6 ci: replace lint workflow with cspell
The lint workflow was slow and we don't often change buf or angular
these days so they're not necessary.

The remaining valuable task is cspell, which we can speed up with a
dedicated step.
2024-12-20 13:52:54 -08:00
Jeff McCune
c5401d6b02 ci: speed up tests by killing steps 2024-12-20 11:57:05 -08:00
Jeff McCune
f215405643 docs: fix links in readme 2024-12-20 07:28:04 -08:00
Jeff McCune
2c79982bd3 cue: enable @embed for loading yaml (#385)
mpvl suggests @embed is a more ideal solution than our implementation of
core.Component.Instances for the use case of unifying YAML data updated
by Kargo Stage resources.

See the issue for a link to the discussion.
2024-12-20 07:14:01 -08:00
Jeff McCune
e5e4de3073 cue: update to 0.11.1
go get cuelang.org/go/cmd/cue@latest

    go: downloading cuelang.org/go v0.11.1
    go: upgraded cuelang.org/go v0.11.0 => v0.11.1
2024-12-20 07:09:39 -08:00
Jeff McCune
ec462f5f0b docs: redirect /docs/support 2024-12-19 22:13:04 -08:00
Jeff McCune
0e95a2812e cmd: expose MakeMain() for testing
I'd like to add the kargo-demo repository to Unity to test evalv3, but
can't get a handle on the main function to wire up to testscript.

This patch fixes the problem by moving the MakeMain function to a public
package so the kargo-demo go module can import and call it using the go
mod tools technique.
2024-12-19 15:19:46 -08:00
Jeff McCune
54efe3e24a core: pass --extract-yaml flag from platform to component (#376)
Previously holos render platform was not setting the --extract-yaml file
when calling holos render component, causing data file instances defined
in the Platform spec to be discarded.

This patch passes the value along using the flag.
2024-12-19 08:39:55 -08:00
Jeff McCune
f693f049f4 core: refactor --instance to --extract-yaml (#376)
Extract YAML is more clear and aligns with the schema docs for the
Component Instance field which has an extractYAML kind.  This also
leaves the door open for additional kinds of data extractors which are
almost certainly going to be needed.
2024-12-19 08:34:05 -08:00
Jeff McCune
85238710ac core: unify data files into config (#376)
Previously there isn't a good way to unify json and yaml files with the
cue configuration.  This is a problem for use cases where data can be
generated idempotentialy prior to rendering the platform configuration.

The first use case is to explore unifying configuration with decrypted
sops values, which isn't typical since Holos is designed to handle
secrets with ExternalSecret resources, but does fit into the use case of
executing a command to produce data idempotently, then make the data
available to the platform configuration.

Other use cases this feature is intended to support are the prior
experiment where we fetch top level platform configuration from an rpc
service, and the future goal of integrating with data provided by
Terraform.
2024-12-19 08:34:05 -08:00
Jeff McCune
3ec62d272e v1alpha5: update kargo crds to 1.1.1 2024-12-19 08:34:04 -08:00
Jeff McCune
49afb44fd4 docs: redirect /docs/comparison/ 2024-12-18 14:37:36 -08:00
Gary Larizza
a023f135ab Add a Comparisons page
PROBLEM:

We've noticed that Holos almost immediately gets compared to Timoni, and
we frequently get asked for specifics in how they're similar/different.

SOLUTION:

* Add a `Comparison` page.
* Include a section that compares Holos to Timoni

OUTCOME:

Fewer questions about how Holos compares to Timoni because people are
able to find that answer themselves on our docs page.
2024-12-18 14:33:52 -08:00
Jeff McCune
c6a3a5d689 docs: redirect /docs/kargo/ 2024-12-17 06:30:20 -08:00
Jeff McCune
3f1eed3f06 platform: add kargo.akuity.io custom resource definitions
Needed for Kargo integration.  Imported with timoni from v1.0.3 Kargo
CRD's.
2024-12-16 13:19:39 -08:00
Jeff McCune
7fb7df1441 docs: make the linter happy 2024-12-16 11:04:35 -05:00
Jeff McCune
a798111d4d docs: add oci helm charts example
Question came up in chat, there isn't a good example and it's a pain to
piece together from the reference docs.
2024-12-16 10:56:50 -05:00
Jeff McCune
3ddb823341 docs: add note about compinit
Andy ran into issues enabling completion without first figuring out how
to initialize the completion system.
2024-12-16 08:15:45 -05:00
Jeff McCune
70d48592c4 docs: fix environments topic
It didn't work, failed with:

  ❯ holos show buildplans --selector app.holos.run/city=ams
  could not run: Component.Name: 2 errors in empty disjunction: (and 2 more errors) at internal/builder/instance.go:66
  Component.Name: 2 errors in empty disjunction:
  Component.Name: conflicting values "no-name" and "podinfo-ams":
      /Users/jeff/Holos/foo/holos-environments-tutorial/components/podinfo/podinfo.cue:6:12
      /Users/jeff/Holos/foo/holos-environments-tutorial/schema.cue:6:13
      /Users/jeff/Holos/foo/holos-environments-tutorial/schema.cue:35:2
      /Users/jeff/Holos/foo/holos-environments-tutorial/tags.cue:13:19
  Component.Name: conflicting values "podinfo" and "podinfo-ams":
      /Users/jeff/Holos/foo/holos-environments-tutorial/components/podinfo/podinfo.cue:6:12
      /Users/jeff/Holos/foo/holos-environments-tutorial/components/podinfo/podinfo.cue:7:8
      /Users/jeff/Holos/foo/holos-environments-tutorial/schema.cue:6:13
      /Users/jeff/Holos/foo/holos-environments-tutorial/schema.cue:35:2

This was likely because the podinfo component was used in different ways
in different topics.  Don't use the shared component to fix the problem.
2024-12-13 09:20:52 -05:00
Jeff McCune
006f08df93 docs: add kargo place holder (#378) 2024-12-11 09:58:54 -08:00
Jeff McCune
39e2db5d37 docs: remove related content from youtube embed
Except stuff in our own channel.
2024-12-08 19:43:12 -08:00
Jeff McCune
ceb293fd8a docs: fix typescript className not class check error 2024-12-08 19:36:36 -08:00
Jeff McCune
188ff95015 docs: enable youtube fullscreen
Without this patch the fullscreen button is disabled.
2024-12-08 19:33:06 -08:00
Jeff McCune
5f658e0ba0 docs: add flux kustomization example (#374)
Almost identical to the ArgoCD Application example.
2024-12-08 19:20:12 -08:00
Jeff McCune
18b2850d3c platform: import flux custom resources
kustomize build https://github.com/fluxcd/flux2/manifests/crds\?ref=v2.4.0 \
      timoni mod vendor crds -f-
2024-12-08 19:03:18 -08:00
Jeff McCune
366a7fe93d docs: private helm repos need updated schemas (#370)
Document the need to run holos init platform v1alpha5 --force to use the
private helm repository feature.
2024-12-08 17:13:56 -08:00
260 changed files with 17316 additions and 1435 deletions

View File

@@ -29,6 +29,7 @@
"authpolicy",
"authproxy",
"authroutes",
"autoload",
"automount",
"automounting",
"autoscaler",
@@ -36,6 +37,7 @@
"blackbox",
"buildplan",
"buildplans",
"Buildx",
"builtinpluginloadingoptions",
"cachedir",
"cadvisor",
@@ -59,6 +61,7 @@
"Cmds",
"CNCF",
"CODEOWNERS",
"compinit",
"componentconfig",
"configdir",
"configmap",
@@ -72,6 +75,7 @@
"creds",
"crossplane",
"crunchydata",
"ctxt",
"cuecontext",
"cuelang",
"customresourcedefinition",
@@ -81,6 +85,7 @@
"destinationrules",
"devel",
"devicecode",
"distroless",
"dnsmasq",
"dscacheutil",
"ecrauthorizationtoken",
@@ -99,6 +104,7 @@
"fieldmaskpb",
"fieldspec",
"flushcache",
"fluxcd",
"fullname",
"gatewayclass",
"gatewayclasses",
@@ -152,6 +158,7 @@
"jetstack",
"jiralert",
"Jsonnet",
"Kargo",
"kfbh",
"killall",
"kubeadm",
@@ -276,6 +283,7 @@
"serviceentries",
"serviceentry",
"servicemonitor",
"sigstore",
"somevalue",
"SOMEVAR",
"sortoptions",
@@ -316,6 +324,7 @@
"udev",
"uibutton",
"Unmarshal",
"unshallow",
"unstage",
"untar",
"upbound",

143
.github/workflows/container.yaml vendored Normal file
View File

@@ -0,0 +1,143 @@
name: Container
# Only allow actors with write permission to the repository to trigger this
# workflow.
permissions:
contents: write
on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
git_ref:
description: 'Git ref to build (e.g., refs/tags/v1.2.3, refs/heads/main)'
required: true
type: string
jobs:
buildx:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
attestations: write
id-token: write
steps:
- name: Set tag from trigger event
id: opts
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "ref=${{ inputs.git_ref }}" >> $GITHUB_OUTPUT
else
echo "ref=${GITHUB_REF}" >> $GITHUB_OUTPUT
fi
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ steps.opts.outputs.ref }}
- name: SHA
id: sha
run: echo "sha=$(/usr/bin/git log -1 --format='%H')" >> $GITHUB_OUTPUT
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Fetch tags
run: git fetch --prune --unshallow --tags
- name: Set Tags
id: tags
run: |
echo "detail=$(/usr/bin/git describe --tags HEAD)" >> $GITHUB_OUTPUT
echo "suffix=$(test -n "$(git status --porcelain)" && echo '-dirty' || echo '')" >> $GITHUB_OUTPUT
echo "tag=$(/usr/bin/git describe --tags HEAD)$(test -n "$(git status --porcelain)" && echo '-dirty' || echo '')" >> $GITHUB_OUTPUT
- name: Login to ghcr.io
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push container images
id: build-and-push
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: |
ghcr.io/holos-run/holos:${{ steps.tags.outputs.tag }}
ghcr.io/holos-run/holos:${{ steps.sha.outputs.sha }}${{ steps.tags.outputs.suffix }}
- name: Setup Cosign to sign container images
uses: sigstore/cosign-installer@v3.7.0
- name: Sign with GitHub OIDC Token
env:
DIGEST: ${{ steps.build-and-push.outputs.digest }}
run: |
cosign sign --yes ghcr.io/holos-run/holos:${{ steps.tags.outputs.tag }}@${DIGEST}
cosign sign --yes ghcr.io/holos-run/holos:${{ steps.sha.outputs.sha }}${{ steps.tags.outputs.suffix }}@${DIGEST}
- uses: actions/create-github-app-token@v1
id: app-token
with:
owner: ${{ github.repository_owner }}
app-id: ${{ vars.GORELEASER_APP_ID }}
private-key: ${{ secrets.GORELEASER_APP_PRIVATE_KEY }}
- name: Get GitHub App User ID
id: get-user-id
run: echo "user-id=$(gh api "/users/${{ steps.app-token.outputs.app-slug }}[bot]" --jq .id)" >> "$GITHUB_OUTPUT"
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
- run: |
git config --global user.name '${{ steps.app-token.outputs.app-slug }}[bot]'
git config --global user.email '${{ steps.get-user-id.outputs.user-id }}+${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com'
- name: Update holos-run/holos-action
env:
IMAGE: ghcr.io/holos-run/holos:v0.102.1
VERSION: ${{ steps.tags.outputs.tag }}
USER_ID: ${{ steps.get-user-id.outputs.user-id }}
TOKEN: ${{ steps.app-token.outputs.token }}
run: |
set -euo pipefail
git clone "https://github.com/holos-run/holos-action"
cd holos-action
git remote set-url origin https://${USER_ID}:${TOKEN}@github.com/holos-run/holos-action
docker pull --quiet "${IMAGE}"
docker run -v $(pwd):/app --workdir /app --rm "${IMAGE}" \
holos cue export --out yaml action.cue -t "version=${VERSION}" > action.yml
git add action.yml
git commit -m "ci: update holos to ${VERSION} - https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" || (echo "No changes to commit"; exit 0)
git push origin HEAD:main HEAD:v0 HEAD:v1
- name: Login to quay.io
uses: docker/login-action@v3
with:
registry: quay.io
username: ${{ secrets.QUAY_USER }}
password: ${{ secrets.QUAY_TOKEN }}
- name: Push to quay.io
env:
DIGEST: ${{ steps.build-and-push.outputs.digest }}
run: |
# docker push quay.io/holos-run/holos:${{ steps.tags.outputs.tag }}
docker pull --quiet ghcr.io/holos-run/holos:${{ steps.tags.outputs.tag }}@${DIGEST}
docker tag ghcr.io/holos-run/holos:${{ steps.tags.outputs.tag }}@${DIGEST} \
quay.io/holos-run/holos:${{ steps.tags.outputs.tag }}
docker push quay.io/holos-run/holos:${{ steps.tags.outputs.tag }}
docker pull --quiet ghcr.io/holos-run/holos:${{ steps.sha.outputs.sha }}${{ steps.tags.outputs.suffix }}@${DIGEST}
docker tag ghcr.io/holos-run/holos:${{ steps.sha.outputs.sha }}${{ steps.tags.outputs.suffix }}@${DIGEST} \
quay.io/holos-run/holos:${{ steps.sha.outputs.sha }}${{ steps.tags.outputs.suffix }}
docker push quay.io/holos-run/holos:${{ steps.sha.outputs.sha }}${{ steps.tags.outputs.suffix }}
- name: Sign quay.io image
env:
DIGEST: ${{ steps.build-and-push.outputs.digest }}
run: |
cosign sign --yes quay.io/holos-run/holos:${{ steps.tags.outputs.tag }}@${DIGEST}
cosign sign --yes quay.io/holos-run/holos:${{ steps.sha.outputs.sha }}${{ steps.tags.outputs.suffix }}@${DIGEST}
outputs:
tag: ${{ steps.tags.outputs.tag }}
detail: ${{ steps.tags.outputs.detail }}

View File

@@ -1,6 +1,5 @@
---
# https://github.com/golangci/golangci-lint-action?tab=readme-ov-file#how-to-use
name: Lint
name: Spelling
"on":
push:
branches:
@@ -8,35 +7,11 @@ name: Lint
- test
pull_request:
types: [opened, synchronize]
permissions:
contents: read
jobs:
lint:
name: lint
cspell:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: 20
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: stable
## Not needed on ubuntu-latest
# - name: Install Packages
# run: sudo apt update && sudo apt -qq -y install git curl zip unzip tar bzip2 make
- name: Install Tools
run: make tools
- name: Lint
# golangci-lint runs in a separate workflow.
run: make lint -o golangci-lint
- uses: actions/checkout@v4
- run: ./hack/cspell

View File

@@ -28,19 +28,11 @@ jobs:
with:
go-version: stable
- name: Install Packages
run: sudo apt update && sudo apt -qq -y install git curl zip unzip tar bzip2 make
- name: Set up Helm
uses: azure/setup-helm@v4
- name: Set up Kubectl
uses: azure/setup-kubectl@v4
- name: Install Tools
run: |
set -x
make tools
- name: Test
run: ./scripts/test

1
.gitignore vendored
View File

@@ -12,3 +12,4 @@ tmp/
/holos-k3d/
/holos-infra/
node_modules/
.tmp/

View File

@@ -1,8 +1,31 @@
FROM quay.io/holos-run/debian:bullseye AS final
USER root
WORKDIR /app
ADD bin bin
RUN chown -R app: /app
# Kubernetes requires the user to be numeric
USER 8192
ENTRYPOINT bin/holos server
FROM registry.k8s.io/kubectl:v1.31.0 AS kubectl
# https://github.com/GoogleContainerTools/distroless
FROM golang:1.23 AS build
WORKDIR /go/src/app
COPY . .
RUN CGO_ENABLED=0 make install
RUN CGO_ENABLED=0 go install sigs.k8s.io/kustomize/kustomize/v5
# Install helm to /usr/local/bin/helm
# https://helm.sh/docs/intro/install/#from-script
# https://holos.run/docs/v1alpha5/tutorial/setup/#dependencies
RUN curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 \
&& chmod 700 get_helm.sh \
&& DESIRED_VERSION=v3.16.2 ./get_helm.sh \
&& rm -f get_helm.sh
COPY --from=kubectl /bin/kubectl /usr/local/bin/
# distroless
FROM gcr.io/distroless/static-debian12 AS final
COPY --from=build \
/go/bin/holos \
/go/bin/kustomize \
/usr/local/bin/kubectl \
/usr/local/bin/helm \
/bin/
# Usage: docker run -v $(pwd):/app --workdir /app --rm -it quay.io/holos-run/holos holos render platform
CMD ["/bin/holos"]

View File

@@ -154,6 +154,10 @@ website: ## Build website
unity: ## https://cuelabs.dev/unity/
./scripts/unity
.PHONY: update-docs
update-docs: ## Update doc examples
HOLOS_UPDATE_SCRIPTS=1 go test -v ./doc/md/...
.PHONY: help
help: ## Display this help menu.
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-20s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)

View File

@@ -119,12 +119,12 @@ here to help.
Holos is licensed under Apache 2.0 as found in the [LICENSE file](LICENSE).
[Holos]: https://holos.run
[Holos]: https://holos.run/docs/overview/
[rendered manifests pattern]: https://akuity.io/blog/the-rendered-manifests-pattern
[CUE]: https://cuelang.org/
[Discord]: https://discord.gg/JgDVbNpye7
[GitHub discussions]: https://github.com/holos-run/holos/discussions
[Why CUE for Configuration]: https://holos.run/blog/why-cue-for-configuration/
[topics]: https://holos.run/docs/topics/
[tutorial]: https://holos.run/docs/overview/
[setup]: https://holos.run/docs/setup/
[tutorial]: https://holos.run/docs/tutorial/
[topics]: https://holos.run/docs/topics/

View File

@@ -84,6 +84,9 @@ type Helm struct {
Chart core.Chart
// Values represents data to marshal into a values.yaml for helm.
Values core.Values
// ValueFiles represents value files for migration from helm value
// hierarchies. Use Values instead.
ValueFiles []core.ValueFile `json:",omitempty"`
// EnableHooks enables helm hooks when executing the `helm template` command.
EnableHooks bool `cue:"true | *false"`
// Namespace sets the helm chart namespace flag if provided.

View File

@@ -118,8 +118,12 @@ type Helm struct {
// Chart represents a helm chart to manage.
Chart Chart `json:"chart" yaml:"chart"`
// Values represents values for holos to marshal into values.yaml when
// rendering the chart.
// rendering the chart. Values follow ValueFiles when both are provided.
Values Values `json:"values" yaml:"values"`
// ValueFiles represents hierarchial value files passed in order to the helm
// template -f flag. Useful for migration from an ApplicationSet. Use Values
// instead. ValueFiles precede Values when both are provided.
ValueFiles []ValueFile `json:"valueFiles,omitempty" yaml:"valueFiles,omitempty"`
// EnableHooks enables helm hooks when executing the `helm template` command.
EnableHooks bool `json:"enableHooks,omitempty" yaml:"enableHooks,omitempty"`
// Namespace represents the helm namespace flag
@@ -130,6 +134,17 @@ type Helm struct {
KubeVersion string `json:"kubeVersion,omitempty" yaml:"kubeVersion,omitempty"`
}
// ValueFile represents one Helm value file produced from CUE.
type ValueFile struct {
// Name represents the file name, e.g. "region-values.yaml"
Name string `json:"name" yaml:"name"`
// Kind is a discriminator.
Kind string `json:"kind" yaml:"kind" cue:"\"Values\""`
// Values represents values for holos to marshal into the file name specified
// by Name when rendering the chart.
Values Values `json:"values,omitempty" yaml:"values,omitempty"`
}
// Values represents [Helm] Chart values generated from CUE.
type Values map[string]any
@@ -263,7 +278,7 @@ type Metadata struct {
// Labels represents a resource selector.
Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"`
// Annotations represents arbitrary non-identifying metadata. For example
// holos uses the `cli.holos.run/description` annotation to log resources in a
// holos uses the `app.holos.run/description` annotation to log resources in a
// user customized way.
Annotations map[string]string `json:"annotations,omitempty" yaml:"annotations,omitempty"`
}
@@ -303,6 +318,10 @@ type Component struct {
// Path represents the path of the component relative to the platform root.
// Injected as the tag variable "holos_component_path".
Path string `json:"path" yaml:"path"`
// Instances represents additional cue instance paths to unify with Path.
// Useful to unify data files into a component BuildPlan. Added in holos
// 0.101.7.
Instances []Instance `json:"instances,omitempty" yaml:"instances,omitempty"`
// WriteTo represents the holos render component --write-to flag. If empty,
// the default value for the --write-to flag is used.
WriteTo string `json:"writeTo,omitempty" yaml:"writeTo,omitempty"`
@@ -316,6 +335,30 @@ type Component struct {
// resulting BuildPlan.
Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"`
// Annotations represents arbitrary non-identifying metadata. Use the
// `cli.holos.run/description` to customize the log message of each BuildPlan.
// `app.holos.run/description` to customize the log message of each BuildPlan.
Annotations map[string]string `json:"annotations,omitempty" yaml:"annotations,omitempty"`
}
// Instance represents a data instance to unify with the configuration.
//
// Useful to unify json and yaml files with cue configuration files for
// integration with other tools. For example, executing holos render platform
// from a pull request workflow after [Kargo] executes the [yaml update] and
// [git wait for pr] promotion steps.
//
// [Kargo]: https://docs.kargo.io/
// [yaml update]: https://docs.kargo.io/references/promotion-steps#yaml-update
// [git wait for pr]: https://docs.kargo.io/references/promotion-steps#git-wait-for-pr
type Instance struct {
// Kind is a discriminator.
Kind string `json:"kind" yaml:"kind" cue:"\"ExtractYAML\""`
// Ignored unless kind is ExtractYAML.
ExtractYAML ExtractYAML `json:"extractYAML,omitempty" yaml:"extractYAML,omitempty"`
}
// ExtractYAML represents a cue data instance encoded as yaml or json. If Path
// refers to a directory all files in the directory are extracted
// non-recursively. Otherwise, path must refer to a file.
type ExtractYAML struct {
Path string `json:"path" yaml:"path"`
}

63
cmd/cmd.go Normal file
View File

@@ -0,0 +1,63 @@
package cmd
import (
"context"
"fmt"
"log/slog"
"os"
"runtime/pprof"
"runtime/trace"
"github.com/holos-run/holos/internal/cli"
"github.com/holos-run/holos/internal/holos"
)
// MakeMain makes a main function for the cli or tests.
func MakeMain(options ...holos.Option) func() int {
return func() (exitCode int) {
cfg := holos.New(options...)
slog.SetDefault(cfg.Logger())
ctx := context.Background()
if format := os.Getenv("HOLOS_CPU_PROFILE"); format != "" {
f, _ := os.Create(fmt.Sprintf(format, os.Getppid(), os.Getpid()))
err := pprof.StartCPUProfile(f)
defer func() {
pprof.StopCPUProfile()
f.Close()
}()
if err != nil {
return cli.HandleError(ctx, err, cfg)
}
}
defer memProfile(ctx, cfg)
if format := os.Getenv("HOLOS_TRACE"); format != "" {
f, _ := os.Create(fmt.Sprintf(format, os.Getppid(), os.Getpid()))
err := trace.Start(f)
defer func() {
trace.Stop()
f.Close()
}()
if err != nil {
return cli.HandleError(ctx, err, cfg)
}
}
feature := &holos.EnvFlagger{}
if err := cli.New(cfg, feature).ExecuteContext(ctx); err != nil {
return cli.HandleError(ctx, err, cfg)
}
return 0
}
}
func memProfile(ctx context.Context, cfg *holos.Config) {
if format := os.Getenv("HOLOS_MEM_PROFILE"); format != "" {
f, _ := os.Create(fmt.Sprintf(format, os.Getppid(), os.Getpid()))
defer f.Close()
if err := pprof.WriteHeapProfile(f); err != nil {
_ = cli.HandleError(ctx, err, cfg)
}
}
}

View File

@@ -3,9 +3,9 @@ package main
import (
"os"
"github.com/holos-run/holos/internal/cli"
"github.com/holos-run/holos/cmd"
)
func main() {
os.Exit(cli.MakeMain()())
os.Exit(cmd.MakeMain()())
}

View File

@@ -6,13 +6,13 @@ import (
"testing"
cue "cuelang.org/go/cmd/cue/cmd"
"github.com/holos-run/holos/internal/cli"
"github.com/holos-run/holos/cmd"
"github.com/rogpeppe/go-internal/testscript"
)
func TestMain(m *testing.M) {
os.Exit(testscript.RunMain(m, map[string]func() int{
"holos": cli.MakeMain(),
"holos": cmd.MakeMain(),
"cue": cue.Main,
}))
}

View File

@@ -1,7 +1,9 @@
# https://github.com/holos-run/holos/issues/330
exec holos init platform v1alpha5 --force
# Make sure the helm chart works with plain helm
exec helm template ./components/capabilities/vendor/0.1.0/capabilities
cmp stdout want/helm-template.yaml
stdout 'name: has-foo-v1beta1'
stdout 'kubeVersion: v'
exec holos render platform ./platform
# When no capabilities are specified
cmp deploy/components/capabilities/capabilities.gen.yaml want/when-no-capabilities-specified.yaml

View File

@@ -86,6 +86,9 @@ type Helm struct {
Chart core.Chart
// Values represents data to marshal into a values.yaml for helm.
Values core.Values
// ValueFiles represents value files for migration from helm value
// hierarchies. Use Values instead.
ValueFiles []core.ValueFile `json:",omitempty"`
// EnableHooks enables helm hooks when executing the `helm template` command.
EnableHooks bool `cue:"true | *false"`
// Namespace sets the helm chart namespace flag if provided.

View File

@@ -22,12 +22,14 @@ Package core contains schemas for a [Platform](<#Platform>) and [BuildPlan](<#Bu
- [type Chart](<#Chart>)
- [type Command](<#Command>)
- [type Component](<#Component>)
- [type ExtractYAML](<#ExtractYAML>)
- [type File](<#File>)
- [type FileContent](<#FileContent>)
- [type FileContentMap](<#FileContentMap>)
- [type FilePath](<#FilePath>)
- [type Generator](<#Generator>)
- [type Helm](<#Helm>)
- [type Instance](<#Instance>)
- [type InternalLabel](<#InternalLabel>)
- [type Join](<#Join>)
- [type Kind](<#Kind>)
@@ -41,6 +43,7 @@ Package core contains schemas for a [Platform](<#Platform>) and [BuildPlan](<#Bu
- [type Resources](<#Resources>)
- [type Transformer](<#Transformer>)
- [type Validator](<#Validator>)
- [type ValueFile](<#ValueFile>)
- [type Values](<#Values>)
@@ -169,6 +172,10 @@ type Component struct {
// Path represents the path of the component relative to the platform root.
// Injected as the tag variable "holos_component_path".
Path string `json:"path" yaml:"path"`
// Instances represents additional cue instance paths to unify with Path.
// Useful to unify data files into a component BuildPlan. Added in holos
// 0.101.7.
Instances []Instance `json:"instances,omitempty" yaml:"instances,omitempty"`
// WriteTo represents the holos render component --write-to flag. If empty,
// the default value for the --write-to flag is used.
WriteTo string `json:"writeTo,omitempty" yaml:"writeTo,omitempty"`
@@ -182,11 +189,22 @@ type Component struct {
// resulting BuildPlan.
Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"`
// Annotations represents arbitrary non-identifying metadata. Use the
// `cli.holos.run/description` to customize the log message of each BuildPlan.
// `app.holos.run/description` to customize the log message of each BuildPlan.
Annotations map[string]string `json:"annotations,omitempty" yaml:"annotations,omitempty"`
}
```
<a name="ExtractYAML"></a>
## type ExtractYAML {#ExtractYAML}
ExtractYAML represents a cue data instance encoded as yaml or json. If Path refers to a directory all files in the directory are extracted non\-recursively. Otherwise, path must refer to a file.
```go
type ExtractYAML struct {
Path string `json:"path" yaml:"path"`
}
```
<a name="File"></a>
## type File {#File}
@@ -266,8 +284,12 @@ type Helm struct {
// Chart represents a helm chart to manage.
Chart Chart `json:"chart" yaml:"chart"`
// Values represents values for holos to marshal into values.yaml when
// rendering the chart.
// rendering the chart. Values follow ValueFiles when both are provided.
Values Values `json:"values" yaml:"values"`
// ValueFiles represents hierarchial value files passed in order to the helm
// template -f flag. Useful for migration from an ApplicationSet. Use Values
// instead. ValueFiles precede Values when both are provided.
ValueFiles []ValueFile `json:"valueFiles,omitempty" yaml:"valueFiles,omitempty"`
// EnableHooks enables helm hooks when executing the `helm template` command.
EnableHooks bool `json:"enableHooks,omitempty" yaml:"enableHooks,omitempty"`
// Namespace represents the helm namespace flag
@@ -279,6 +301,22 @@ type Helm struct {
}
```
<a name="Instance"></a>
## type Instance {#Instance}
Instance represents a data instance to unify with the configuration.
Useful to unify json and yaml files with cue configuration files for integration with other tools. For example, executing holos render platform from a pull request workflow after [Kargo](<https://docs.kargo.io/>) executes the [yaml update](<https://docs.kargo.io/references/promotion-steps#yaml-update>) and [git wait for pr](<https://docs.kargo.io/references/promotion-steps#git-wait-for-pr>) promotion steps.
```go
type Instance struct {
// Kind is a discriminator.
Kind string `json:"kind" yaml:"kind" cue:"\"ExtractYAML\""`
// Ignored unless kind is ExtractYAML.
ExtractYAML ExtractYAML `json:"extractYAML,omitempty" yaml:"extractYAML,omitempty"`
}
```
<a name="InternalLabel"></a>
## type InternalLabel {#InternalLabel}
@@ -343,7 +381,7 @@ type Metadata struct {
// Labels represents a resource selector.
Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"`
// Annotations represents arbitrary non-identifying metadata. For example
// holos uses the `cli.holos.run/description` annotation to log resources in a
// holos uses the `app.holos.run/description` annotation to log resources in a
// user customized way.
Annotations map[string]string `json:"annotations,omitempty" yaml:"annotations,omitempty"`
}
@@ -460,6 +498,23 @@ type Validator struct {
}
```
<a name="ValueFile"></a>
## type ValueFile {#ValueFile}
ValueFile represents one Helm value file produced from CUE.
```go
type ValueFile struct {
// Name represents the file name, e.g. "region-values.yaml"
Name string `json:"name" yaml:"name"`
// Kind is a discriminator.
Kind string `json:"kind" yaml:"kind" cue:"\"Values\""`
// Values represents values for holos to marshal into the file name specified
// by Name when rendering the chart.
Values Values `json:"values,omitempty" yaml:"values,omitempty"`
}
```
<a name="Values"></a>
## type Values {#Values}

View File

@@ -0,0 +1,57 @@
---
description: Holos compared to other tools
sidebar_label: Comparison
slug: comparison
sidebar_position: 40
---
{/* cspell:ignore Prodan, rollouts */}
# Holos compared to other tools
## Timoni
Holos and Timoni both aim to solve similar problems but approach them at
different levels of the stack.
Timoni focuses on managing applications by evaluating [CUE] stored in OCI
containers. Its creator, Stephan Prodan, envisions a controller that applies the
resulting manifests. In this process, Timoni defers to [Flux] for managing Helm
charts within the cluster.
In contrast, Holos implements the [Rendered Manifests Pattern] and takes a
different approach, particularly in how it handles [Helm] charts. Like
[ArgoCD], Holos renders Helm charts into manifests using the `helm template`
command in its rendering pipeline. Holos differs from Timoni in several important
ways:
1. **Separation of Responsibilities:** Holos stops short of applying
rendered manifests to a cluster, leaving that task to existing tools like
[ArgoCD], [Flux], or even basic `kubectl apply` commands.
2. **Ecosystem Integration:** By focusing solely on rendering Kubernetes
manifests, Holos creates space for other tools to handle deployment and
management. For instance, Holos integrates seamlessly with [Kargo] for
progressive rollouts, as [Kargo] operates between Holos and the Kubernetes API.
This approach ensures that you're not locked into any specific tool and can
choose the best solution for each task.
3. **Platform Integration:** Holos focuses on integrating multiple Components
into a larger Platform. In Holos terminology, a Component refers to a wrapper
for [Helm] charts, [Kustomize] bases, or raw YAML files, integrated into the
rendering pipeline through [CUE]. A Platform represents the full combination of
these components.
4. **Explicit Rendering Pipeline:** Holos emphasizes flexibility in its
rendering pipeline. The system allows any tool that generates Kubernetes
manifests to be wrapped in a Generator, which can then feed into existing
transformers like [Kustomize]. This explicit separation makes Holos highly
adaptable for different workflows.
[Kargo]: https://kargo.io/
[Flux]: https://fluxcd.io
[Helm]: https://helm.sh
[ArgoCD]: https://argoproj.github.io/cd/
[Kustomize]: https://kustomize.io/
[CUE]: https://cuelang.org/
[Rendered Manifests Pattern]: https://akuity.io/blog/the-rendered-manifests-pattern

View File

@@ -0,0 +1,218 @@
---
slug: flux-kustomization
title: Flux Kustomization
description: Configuring a Kustomization for each Component.
sidebar_position: 120
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import CommonComponent from '../../common/example-component.mdx';
import CommonComponentIntegrate from '../../common/example-component-integrate.mdx';
# Flux Kustomization
## Overview
This topic covers how to mix in a Flux Kustomization to all components. We'll
use the `Artifacts` field of [ComponentConfig] defined by the author schema.
## The Code
### Generating the structure
Use `holos` to generate a minimal platform directory structure. Start by
creating a blank directory to hold the platform configuration.
```shell
mkdir holos-flux-kustomization && cd holos-flux-kustomization
```
```shell
holos init platform v1alpha5
```
### Creating an example Component
<CommonComponent />
<CommonComponentIntegrate />
## Adding Flux Kustomizations
Configure Holos to render a [Kustomization] by defining an [Artifact] for it in
every BuildPlan holos produces. We're unifying our custom configuration with
the existing `#ComponentConfig` defined in `schema.cue`.
```bash
cat <<EOF >flux-kustomization.cue
```
```cue showLineNumbers
package holos
import (
"path"
flux "kustomize.toolkit.fluxcd.io/kustomization/v1"
)
#ComponentConfig: {
Name: _
OutputBaseDir: _
let ArtifactPath = path.Join([OutputBaseDir, "gitops", "\(Name).kustomization.gen.yaml"], path.Unix)
let ResourcesPath = path.Join(["deploy", OutputBaseDir, "components", Name], path.Unix)
Artifacts: "\(Name)-kustomization": {
artifact: ArtifactPath
generators: [{
kind: "Resources"
output: artifact
resources: Kustomization: (Name): flux.#Kustomization & {
metadata: name: Name
metadata: namespace: "default"
spec: {
interval: "5m"
timeout: "1m"
prune: true
path: ResourcesPath
sourceRef: {
kind: "GitRepository"
name: "webapp"
}
}
}
}]
}
}
```
```bash
EOF
```
## Inspecting the BuildPlan
Our customized `#ComponentConfig` results in the following `BuildPlan`.
:::note
The second artifact around line 40 contains the configured `Kustomization`
resource.
:::
<Tabs groupId="55075C71-02E8-4222-88C0-2D52C82D18FC">
<TabItem value="command" label="Command">
```bash
holos cue export --expression holos --out=yaml ./components/podinfo
```
</TabItem>
<TabItem value="output" label="Output">
```yaml showLineNumbers
kind: BuildPlan
apiVersion: v1alpha5
metadata:
name: podinfo
spec:
artifacts:
- artifact: components/podinfo/podinfo.gen.yaml
generators:
- kind: Helm
output: helm.gen.yaml
helm:
chart:
name: podinfo
version: 6.6.2
release: podinfo
repository:
name: podinfo
url: https://stefanprodan.github.io/podinfo
values:
ui:
message: Hello World
enableHooks: false
- kind: Resources
output: resources.gen.yaml
resources: {}
validators: []
transformers:
- kind: Kustomize
inputs:
- helm.gen.yaml
- resources.gen.yaml
output: components/podinfo/podinfo.gen.yaml
kustomize:
kustomization:
resources:
- helm.gen.yaml
- resources.gen.yaml
kind: Kustomization
apiVersion: kustomize.config.k8s.io/v1beta1
- artifact: gitops/podinfo.kustomization.gen.yaml
generators:
- kind: Resources
output: gitops/podinfo.kustomization.gen.yaml
resources:
Kustomization:
podinfo:
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: podinfo
namespace: default
spec:
interval: 5m
path: deploy/components/podinfo
prune: true
sourceRef:
kind: GitRepository
name: webapp
timeout: 1m
```
</TabItem>
</Tabs>
## Rendering manifests
<Tabs groupId="E150C802-7162-4FBF-82A7-77D9ADAEE847">
<TabItem value="command" label="Command">
```bash
holos render platform
```
</TabItem>
<TabItem value="output" label="Output">
```
rendered podinfo in 140.341417ms
rendered platform in 140.441333ms
```
</TabItem>
</Tabs>
## Reviewing the Kustomization
The Artifact we added to `#ComponentConfig` will produce a Flux Kustomization
resource for every component in the platform. The output in this example is
located at:
```txt
deploy/gitops/podinfo.kustomization.gen.yaml
```
```yaml showLineNumbers
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: podinfo
namespace: default
spec:
interval: 5m
path: deploy/components/podinfo
prune: true
sourceRef:
kind: GitRepository
name: webapp
timeout: 1m
```
[podinfo]: https://github.com/stefanprodan/podinfo
[CUE Module]: https://cuelang.org/docs/reference/modules/
[CUE Tags]: https://cuelang.org/docs/howto/inject-value-into-evaluation-using-tag-attribute/
[Kustomization]: https://fluxcd.io/flux/components/kustomize/kustomizations/
[Platform]: ../../api/author.md#Platform
[ComponentConfig]: ../../api/author.md#ComponentConfig
[Artifact]: ../../api/core.md#Artifact

20
doc/md/topics/kargo.mdx Normal file
View File

@@ -0,0 +1,20 @@
---
description: Kargo
slug: kargo
sidebar_position: 110
---
# Kargo
Holos pairs nicely with [Kargo], offering a holistic solution for code
promotion across stages.
Watch this space for a more detailed write up of the integration being
developed.
If you're interested in this topic, please thumbs up the [Kargo
Topic](https://github.com/holos-run/holos/issues/378) issue, or drop into
[Discord] and let us know about your use case.
[Kargo]: https://kargo.io/
[Discord]: https://discord.gg/JgDVbNpye7

View File

@@ -0,0 +1,65 @@
---
description: OCI Helm Charts
slug: oci-helm-charts
sidebar_position: 710
---
# OCI Helm Charts
Holos supports OCI Helm charts. Use the following example to get started.
```bash
mkdir -p oci-helm && cd oci-helm
holos init platform v1alpha5
```
```bash
mkdir -p components/podinfo-oci
cat <<EOF > components/podinfo-oci/podinfo-oci.cue
```
```cue showLineNumbers
package holos
holos: Component.BuildPlan
Component: #Helm & {
Chart: {
name: "oci://ghcr.io/stefanprodan/charts/podinfo"
release: "podinfo"
version: "6.6.2"
}
}
```
```bash
EOF
```
Register the component with the platform.
```bash
cat <<EOF >platform/podinfo-oci.cue
```
```cue showLineNumbers
package holos
Platform: Components: podinfo: {
name: "podinfo-oci"
path: "components/podinfo-oci"
}
```
```bash
EOF
```
The OCI chart is cached in the vendor directory and rendered.
```bash
holos render platform
```
```txt
Pulled: ghcr.io/stefanprodan/charts/podinfo:6.6.2
Digest: sha256:83295d47de6d6ca634ed4b952a7572fc176bcc38854d0c11ca0fa197bc5f1154
rendered podinfo-oci in 7.21581325s
rendered platform in 7.216199167s
```

View File

@@ -1,11 +1,19 @@
---
description: Private Helm Repositories
slug: private-helm
sidebar_position: 999
sidebar_position: 700
---
# Private Helm
Holos supports private Helm repositories accessed with http basic authentication
since `v0.101.4`. Use the following command to update your author and core
schemas to support this configuration.
```bash
holos init platform v1alpha5 --force
```
## Configuration
Holos uses the Helm SDK and defers to it for authentication to private

View File

@@ -45,7 +45,40 @@ holos init platform v1alpha5
### Using an example Component
<CommonComponent />
Create a directory for the example `podinfo` component we'll use to render
platform manifests.
```bash
mkdir -p components/podinfo
```
Create the CUE configuration for the example `podinfo` component.
```bash
cat <<EOF >components/podinfo/podinfo.cue
```
```cue showLineNumbers
package holos
holos: Component.BuildPlan
Component: #Helm & {
Chart: {
name: "podinfo"
version: "6.6.2"
repository: {
name: "podinfo"
url: "https://stefanprodan.github.io/podinfo"
}
}
Values: ui: {
message: string | *"Hello World" @tag(message, type=string)
}
}
```
```bash
EOF
```
We'll integrate the component with the platform after we define the
configuration structures.
@@ -277,6 +310,9 @@ let ProdPodinfo = {
parameters: EnvironmentName: "prod-\(_city)"
}
```
```
EOF
```
### Using the environment

View File

@@ -0,0 +1,7 @@
exec bash -c 'bash -euo pipefail $WORK/command.sh 2>&1'
cmp stdout $WORK/output.txt
-- command.sh --
holos --version
-- output.txt --
0.103.0

View File

@@ -0,0 +1,374 @@
# Set $HOME because:
# - Helm uses it for temporary files
# - Git requires it for setting author name/email globally
env HOME=$WORK/.tmp
chmod 0755 $WORK/update.sh
# Configure git author for testscript execution
exec git config --global user.name 'Holos Docs'
exec git config --global user.email 'hello@holos.run'
exec git config --global init.defaultBranch main
# Remove the tutorial directory if it already exists
exec rm -rf holos-helm-values-tutorial
# Create and change to the tutorial directory, and then initialize the Holos platform
exec bash -c 'bash -euo pipefail mkdir-and-init.sh'
cd holos-helm-values-tutorial
# Git init and create the component directories
exec bash -c 'bash -euo pipefail $WORK/git-init.sh'
exec bash -c 'bash -euo pipefail $WORK/mkdir-components.sh'
# Combine and execute the multiline prometheus/blackbox component header/body/trailer files
exec cat $WORK/prometheus-component-header.sh ../prometheus-component-body.cue ../eof-trailer.sh
stdin stdout
exec bash -xeuo pipefail
exec cat $WORK/blackbox-component-header.sh ../blackbox-component-body.cue ../eof-trailer.sh
stdin stdout
exec bash -xeuo pipefail
# Combine and execute the multiline platform registration header/body/trailer files.
exec cat $WORK/register-components-header.sh ../register-components-body.cue ../eof-trailer.sh
stdin stdout
exec bash -xeuo pipefail
# Render the platform, capture stdout, and use update.sh to gate whether the
# output file should be updated.
#
# NOTE: The [net] condition will test whether external network access is available
[net] exec bash -c 'bash -euo pipefail $WORK/render.sh 2>&1'
[net] stdin stdout
exec $WORK/update.sh $WORK/register-components-output.txt
# Commit and conditionally update the output file
exec bash -c 'bash -euo pipefail $WORK/register-components-git-commit.sh'
stdin stdout
exec $WORK/update.sh $WORK/register-components-git-commit-output.txt
# Import values
exec bash -c 'bash -euo pipefail $WORK/import-prometheus-values.sh'
exec bash -c 'bash -euo pipefail $WORK/import-blackbox-values.sh'
# Render, update the output file, commit, and update the commit output file.
[net] exec bash -c 'bash -euo pipefail $WORK/render.sh 2>&1'
[net] stdin stdout
exec $WORK/update.sh $WORK/import-values-render-output.txt
exec bash -c 'bash -euo pipefail $WORK/import-values-git-commit.sh'
stdin stdout
exec $WORK/update.sh $WORK/import-values-git-output.txt
# Create the common configuration path
exec bash -c 'bash -euo pipefail $WORK/mkdir-common-config.sh'
# Combine and execute the common configuration header/body/trailer to write the cue file.
exec cat $WORK/blackbox-common-config-header.sh ../blackbox-common-config-body.cue ../eof-trailer.sh
stdin stdout
exec bash -xeuo pipefail
# Git commit blackbox common config
exec bash -c 'bash -euo pipefail $WORK/blackbox-common-config-git-commit.sh'
stdin stdout
exec $WORK/update.sh $WORK/blackbox-common-config-git-output.txt
# Patch the common config values file and write to output file.
#
# NOTE: Using a symlink here because the patch script references values.patch
# within the same directory, but it actually lives one directory up in the
# testscript $WORK dir.
exec ln -s $WORK/values.patch values.patch
exec bash -c 'bash -euo pipefail $WORK/common-config-patch.sh'
stdin stdout
exec $WORK/update.sh $WORK/common-config-patch.txt
# Remove patch and commit changes
exec bash -c 'bash -euo pipefail $WORK/common-config-rm.sh'
exec bash -c 'bash -euo pipefail $WORK/common-config-git.sh'
stdin stdout
exec $WORK/update.sh $WORK/common-config-git-output.txt
# Final render and update of output file.
[net] exec bash -c 'bash -euo pipefail $WORK/render.sh 2>&1'
[net] stdin stdout
exec $WORK/update.sh $WORK/reviewing-changes-git-output.txt
# Git diff and write to output file.
exec bash -c 'bash -euo pipefail $WORK/git-diff.sh'
stdin stdout
exec $WORK/update.sh $WORK/git.diff
# Final commit and write to output file
exec bash -c 'bash -euo pipefail $WORK/reviewing-changes-git-commit.sh'
stdin stdout
exec $WORK/update.sh $WORK/reviewing-changes-git-output.txt
# Clean up the tutorial directory and tmp $HOME directory
cd $WORK
exec rm -rf holos-helm-values-tutorial
exec rm -rf $HOME
-- update.sh --
#! /bin/bash
set -euo pipefail
[[ -s "$1" ]] && [[ -z "${HOLOS_UPDATE_SCRIPTS:-}" ]] && exit 0
cat > "$1"
-- mkdir-and-init.sh --
mkdir holos-helm-values-tutorial
cd holos-helm-values-tutorial
holos init platform v1alpha5
-- git-init.sh --
git init . && git add . && git commit -m "initial commit"
-- mkdir-components.sh --
mkdir -p components/prometheus components/blackbox
-- prometheus-component-header.sh --
cat <<EOF > components/prometheus/prometheus.cue
-- prometheus-component-body.cue --
package holos
// Produce a helm chart build plan.
holos: Helm.BuildPlan
Helm: #Helm & {
Chart: {
name: "prometheus"
version: "25.27.0"
repository: {
name: "prometheus-community"
url: "https://prometheus-community.github.io/helm-charts"
}
}
}
-- eof-trailer.sh --
EOF
-- blackbox-component-header.sh --
cat <<EOF > components/blackbox/blackbox.cue
-- blackbox-component-body.cue --
package holos
// Produce a helm chart build plan.
holos: Helm.BuildPlan
Helm: #Helm & {
Chart: {
name: "prometheus-blackbox-exporter"
version: "9.0.1"
repository: {
name: "prometheus-community"
url: "https://prometheus-community.github.io/helm-charts"
}
}
}
-- register-components-header.sh --
cat <<EOF > platform/prometheus.cue
-- register-components-body.cue --
package holos
Platform: Components: {
prometheus: {
name: "prometheus"
path: "components/prometheus"
}
blackbox: {
name: "blackbox"
path: "components/blackbox"
}
}
-- render.sh --
holos render platform
-- register-components-output.txt --
cached prometheus-blackbox-exporter 9.0.1
rendered blackbox in 3.825430417s
cached prometheus 25.27.0
rendered prometheus in 4.840089667s
rendered platform in 4.840137792s
-- register-components-git-commit.sh --
git add . && git commit -m 'add blackbox and prometheus'
-- register-components-git-commit-output.txt --
[main b5df111] add blackbox and prometheus
5 files changed, 1550 insertions(+)
create mode 100644 components/blackbox/blackbox.cue
create mode 100644 components/prometheus/prometheus.cue
create mode 100644 deploy/components/blackbox/blackbox.gen.yaml
create mode 100644 deploy/components/prometheus/prometheus.gen.yaml
create mode 100644 platform/prometheus.cue
-- import-prometheus-values.sh --
holos cue import \
--package holos \
--path 'Helm: Values:' \
--outfile components/prometheus/values.cue \
components/prometheus/vendor/25.27.0/prometheus/values.yaml
-- import-blackbox-values.sh --
holos cue import \
--package holos \
--path 'Helm: Values:' \
--outfile components/blackbox/values.cue \
components/blackbox/vendor/9.0.1/prometheus-blackbox-exporter/values.yaml
-- import-values-render-output.txt --
rendered blackbox in 365.936792ms
rendered prometheus in 371.855875ms
rendered platform in 372.109916ms
-- import-values-git-commit.sh --
git add . && git commit -m 'import values'
-- import-values-git-output.txt --
[main 52e90ea] import values
2 files changed, 1815 insertions(+)
create mode 100644 components/blackbox/values.cue
create mode 100644 components/prometheus/values.cue
-- mkdir-common-config.sh --
mkdir -p config/prometheus
-- blackbox-common-config-header.sh --
cat <<EOF > config/prometheus/blackbox.cue
-- blackbox-common-config-body.cue --
package prometheus
// Schema Definition
#Blackbox: {
// host constrained to a lower case dns label
host: string & =~"^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$"
// port constrained to a valid range
port: int & >0 & <=65535
}
// Concrete values must validate against the schema.
blackbox: #Blackbox & {
host: "blackbox"
port: 9115
}
-- blackbox-common-config-git-commit.sh --
git add . && git commit -m 'add blackbox configuration'
-- blackbox-common-config-git-output.txt --
[main 1adcd08] add blackbox configuration
1 file changed, 15 insertions(+)
create mode 100644 components/blackbox.cue
-- common-config-patch.sh --
patch -p1 < values.patch
-- values.patch --
--- a/components/blackbox/values.cue
+++ b/components/blackbox/values.cue
@@ -1,6 +1,11 @@
package holos
+// Import common blackbox configuration
+import "holos.example/config/prometheus"
+
Helm: Values: {
+ fullnameOverride: prometheus.blackbox.host
+
global: {
//# Global image registry to use if it needs to be overriden for some specific use cases (e.g local registries, custom images, ...)
//#
@@ -192,7 +197,7 @@ Helm: Values: {
annotations: {}
labels: {}
type: "ClusterIP"
- port: 9115
+ port: prometheus.blackbox.port
ipDualStack: {
enabled: false
ipFamilies: ["IPv6", "IPv4"]
--- a/components/prometheus/values.cue
+++ b/components/prometheus/values.cue
@@ -1,5 +1,8 @@
package holos
+// Import common blackbox configuration
+import "holos.example/config/prometheus"
+
Helm: Values: {
// yaml-language-server: $schema=values.schema.json
// Default values for prometheus.
@@ -1083,7 +1086,7 @@ Helm: Values: {
target_label: "__param_target"
}, {
target_label: "__address__"
- replacement: "blackbox"
+ replacement: "\(prometheus.blackbox.host):\(prometheus.blackbox.port)"
}, {
source_labels: ["__param_target"]
target_label: "instance"
-- common-config-patch.txt --
patching file 'components/blackbox/values.cue'
patching file 'components/prometheus/values.cue'
-- common-config-rm.sh --
rm values.patch
-- common-config-git.sh --
git add . && git commit -m 'integrate blackbox and prometheus together'
-- common-config-git-output.txt --
[main 4221803] integrate blackbox and prometheus together
2 files changed, 4 insertions(+), 2 deletions(-)
-- reviewing-changes-render-output.txt --
rendered blackbox in 374.810666ms
rendered prometheus in 382.899334ms
rendered platform in 383.270625ms
-- git-diff.sh --
git diff
-- git.diff --
diff --git a/deploy/components/blackbox/blackbox.gen.yaml b/deploy/components/blackbox/blackbox.gen.yaml
index 3db20cd..5336f44 100644
--- a/deploy/components/blackbox/blackbox.gen.yaml
+++ b/deploy/components/blackbox/blackbox.gen.yaml
@@ -7,7 +7,7 @@ metadata:
app.kubernetes.io/name: prometheus-blackbox-exporter
app.kubernetes.io/version: v0.25.0
helm.sh/chart: prometheus-blackbox-exporter-9.0.1
- name: prometheus-blackbox-exporter
+ name: blackbox
namespace: default
---
apiVersion: v1
@@ -31,7 +31,7 @@ metadata:
app.kubernetes.io/name: prometheus-blackbox-exporter
app.kubernetes.io/version: v0.25.0
helm.sh/chart: prometheus-blackbox-exporter-9.0.1
- name: prometheus-blackbox-exporter
+ name: blackbox
namespace: default
---
apiVersion: v1
@@ -43,7 +43,7 @@ metadata:
app.kubernetes.io/name: prometheus-blackbox-exporter
app.kubernetes.io/version: v0.25.0
helm.sh/chart: prometheus-blackbox-exporter-9.0.1
- name: prometheus-blackbox-exporter
+ name: blackbox
namespace: default
spec:
ports:
@@ -65,7 +65,7 @@ metadata:
app.kubernetes.io/name: prometheus-blackbox-exporter
app.kubernetes.io/version: v0.25.0
helm.sh/chart: prometheus-blackbox-exporter-9.0.1
- name: prometheus-blackbox-exporter
+ name: blackbox
namespace: default
spec:
replicas: 1
@@ -119,8 +119,8 @@ spec:
name: config
hostNetwork: false
restartPolicy: Always
- serviceAccountName: prometheus-blackbox-exporter
+ serviceAccountName: blackbox
volumes:
- configMap:
- name: prometheus-blackbox-exporter
+ name: blackbox
name: config
diff --git a/deploy/components/prometheus/prometheus.gen.yaml b/deploy/components/prometheus/prometheus.gen.yaml
index 9e02bce..ab638f0 100644
--- a/deploy/components/prometheus/prometheus.gen.yaml
+++ b/deploy/components/prometheus/prometheus.gen.yaml
@@ -589,7 +589,7 @@ data:
- source_labels:
- __address__
target_label: __param_target
- - replacement: blackbox
+ - replacement: blackbox:9115
target_label: __address__
- source_labels:
- __param_target
-- reviewing-changes-git-commit.sh --
git add . && git commit -m 'render integrated blackbox and prometheus manifests'
-- reviewing-changes-git-output.txt --
[main 67efe0d] render integrated blackbox and prometheus manifests
2 files changed, 7 insertions(+), 7 deletions(-)

View File

@@ -0,0 +1 @@
holos --version

View File

@@ -0,0 +1 @@
0.103.0

View File

@@ -0,0 +1,15 @@
package prometheus
// Schema Definition
#Blackbox: {
// host constrained to a lower case dns label
host: string & =~"^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$"
// port constrained to a valid range
port: int & >0 & <=65535
}
// Concrete values must validate against the schema.
blackbox: #Blackbox & {
host: "blackbox"
port: 9115
}

View File

@@ -0,0 +1 @@
git add . && git commit -m 'add blackbox configuration'

View File

@@ -0,0 +1,3 @@
[main 1adcd08] add blackbox configuration
1 file changed, 15 insertions(+)
create mode 100644 components/blackbox.cue

View File

@@ -0,0 +1 @@
cat <<EOF > config/prometheus/blackbox.cue

View File

@@ -0,0 +1,15 @@
package holos
// Produce a helm chart build plan.
holos: Helm.BuildPlan
Helm: #Helm & {
Chart: {
name: "prometheus-blackbox-exporter"
version: "9.0.1"
repository: {
name: "prometheus-community"
url: "https://prometheus-community.github.io/helm-charts"
}
}
}

View File

@@ -0,0 +1 @@
cat <<EOF > components/blackbox/blackbox.cue

View File

@@ -0,0 +1,2 @@
[main 4221803] integrate blackbox and prometheus together
2 files changed, 4 insertions(+), 2 deletions(-)

View File

@@ -0,0 +1 @@
git add . && git commit -m 'integrate blackbox and prometheus together'

View File

@@ -0,0 +1 @@
patch -p1 < values.patch

View File

@@ -0,0 +1,2 @@
patching file 'components/blackbox/values.cue'
patching file 'components/prometheus/values.cue'

View File

@@ -0,0 +1 @@
rm values.patch

View File

@@ -0,0 +1 @@
EOF

View File

@@ -0,0 +1 @@
git diff

View File

@@ -0,0 +1 @@
git init . && git add . && git commit -m "initial commit"

View File

@@ -0,0 +1,64 @@
diff --git a/deploy/components/blackbox/blackbox.gen.yaml b/deploy/components/blackbox/blackbox.gen.yaml
index 3db20cd..5336f44 100644
--- a/deploy/components/blackbox/blackbox.gen.yaml
+++ b/deploy/components/blackbox/blackbox.gen.yaml
@@ -7,7 +7,7 @@ metadata:
app.kubernetes.io/name: prometheus-blackbox-exporter
app.kubernetes.io/version: v0.25.0
helm.sh/chart: prometheus-blackbox-exporter-9.0.1
- name: prometheus-blackbox-exporter
+ name: blackbox
namespace: default
---
apiVersion: v1
@@ -31,7 +31,7 @@ metadata:
app.kubernetes.io/name: prometheus-blackbox-exporter
app.kubernetes.io/version: v0.25.0
helm.sh/chart: prometheus-blackbox-exporter-9.0.1
- name: prometheus-blackbox-exporter
+ name: blackbox
namespace: default
---
apiVersion: v1
@@ -43,7 +43,7 @@ metadata:
app.kubernetes.io/name: prometheus-blackbox-exporter
app.kubernetes.io/version: v0.25.0
helm.sh/chart: prometheus-blackbox-exporter-9.0.1
- name: prometheus-blackbox-exporter
+ name: blackbox
namespace: default
spec:
ports:
@@ -65,7 +65,7 @@ metadata:
app.kubernetes.io/name: prometheus-blackbox-exporter
app.kubernetes.io/version: v0.25.0
helm.sh/chart: prometheus-blackbox-exporter-9.0.1
- name: prometheus-blackbox-exporter
+ name: blackbox
namespace: default
spec:
replicas: 1
@@ -119,8 +119,8 @@ spec:
name: config
hostNetwork: false
restartPolicy: Always
- serviceAccountName: prometheus-blackbox-exporter
+ serviceAccountName: blackbox
volumes:
- configMap:
- name: prometheus-blackbox-exporter
+ name: blackbox
name: config
diff --git a/deploy/components/prometheus/prometheus.gen.yaml b/deploy/components/prometheus/prometheus.gen.yaml
index 9e02bce..ab638f0 100644
--- a/deploy/components/prometheus/prometheus.gen.yaml
+++ b/deploy/components/prometheus/prometheus.gen.yaml
@@ -589,7 +589,7 @@ data:
- source_labels:
- __address__
target_label: __param_target
- - replacement: blackbox
+ - replacement: blackbox:9115
target_label: __address__
- source_labels:
- __param_target

View File

@@ -0,0 +1,5 @@
holos cue import \
--package holos \
--path 'Helm: Values:' \
--outfile components/blackbox/values.cue \
components/blackbox/vendor/9.0.1/prometheus-blackbox-exporter/values.yaml

View File

@@ -0,0 +1,5 @@
holos cue import \
--package holos \
--path 'Helm: Values:' \
--outfile components/prometheus/values.cue \
components/prometheus/vendor/25.27.0/prometheus/values.yaml

View File

@@ -0,0 +1 @@
git add . && git commit -m 'import values'

View File

@@ -0,0 +1,4 @@
[main 52e90ea] import values
2 files changed, 1815 insertions(+)
create mode 100644 components/blackbox/values.cue
create mode 100644 components/prometheus/values.cue

View File

@@ -0,0 +1,3 @@
rendered blackbox in 365.936792ms
rendered prometheus in 371.855875ms
rendered platform in 372.109916ms

View File

@@ -0,0 +1,3 @@
mkdir holos-helm-values-tutorial
cd holos-helm-values-tutorial
holos init platform v1alpha5

View File

@@ -0,0 +1 @@
mkdir -p config/prometheus

View File

@@ -0,0 +1 @@
mkdir -p components/prometheus components/blackbox

View File

@@ -0,0 +1,15 @@
package holos
// Produce a helm chart build plan.
holos: Helm.BuildPlan
Helm: #Helm & {
Chart: {
name: "prometheus"
version: "25.27.0"
repository: {
name: "prometheus-community"
url: "https://prometheus-community.github.io/helm-charts"
}
}
}

View File

@@ -0,0 +1 @@
cat <<EOF > components/prometheus/prometheus.cue

View File

@@ -0,0 +1,12 @@
package holos
Platform: Components: {
prometheus: {
name: "prometheus"
path: "components/prometheus"
}
blackbox: {
name: "blackbox"
path: "components/blackbox"
}
}

View File

@@ -0,0 +1,7 @@
[main b5df111] add blackbox and prometheus
5 files changed, 1550 insertions(+)
create mode 100644 components/blackbox/blackbox.cue
create mode 100644 components/prometheus/prometheus.cue
create mode 100644 deploy/components/blackbox/blackbox.gen.yaml
create mode 100644 deploy/components/prometheus/prometheus.gen.yaml
create mode 100644 platform/prometheus.cue

View File

@@ -0,0 +1 @@
git add . && git commit -m 'add blackbox and prometheus'

View File

@@ -0,0 +1 @@
cat <<EOF > platform/prometheus.cue

View File

@@ -0,0 +1,5 @@
cached prometheus-blackbox-exporter 9.0.1
rendered blackbox in 3.825430417s
cached prometheus 25.27.0
rendered prometheus in 4.840089667s
rendered platform in 4.840137792s

View File

@@ -0,0 +1 @@
holos render platform

View File

@@ -0,0 +1 @@
git add . && git commit -m 'render integrated blackbox and prometheus manifests'

View File

@@ -0,0 +1,2 @@
[main 67efe0d] render integrated blackbox and prometheus manifests
2 files changed, 7 insertions(+), 7 deletions(-)

View File

@@ -0,0 +1,3 @@
rendered blackbox in 374.810666ms
rendered prometheus in 382.899334ms
rendered platform in 383.270625ms

View File

@@ -0,0 +1,4 @@
#! /bin/bash
set -euo pipefail
[[ -s "$1" ]] && [[ -z "${HOLOS_UPDATE_SCRIPTS:-}" ]] && exit 0
cat > "$1"

View File

@@ -0,0 +1,43 @@
--- a/components/blackbox/values.cue
+++ b/components/blackbox/values.cue
@@ -1,6 +1,11 @@
package holos
+// Import common blackbox configuration
+import "holos.example/config/prometheus"
+
Helm: Values: {
+ fullnameOverride: prometheus.blackbox.host
+
global: {
//# Global image registry to use if it needs to be overriden for some specific use cases (e.g local registries, custom images, ...)
//#
@@ -192,7 +197,7 @@ Helm: Values: {
annotations: {}
labels: {}
type: "ClusterIP"
- port: 9115
+ port: prometheus.blackbox.port
ipDualStack: {
enabled: false
ipFamilies: ["IPv6", "IPv4"]
--- a/components/prometheus/values.cue
+++ b/components/prometheus/values.cue
@@ -1,5 +1,8 @@
package holos
+// Import common blackbox configuration
+import "holos.example/config/prometheus"
+
Helm: Values: {
// yaml-language-server: $schema=values.schema.json
// Default values for prometheus.
@@ -1083,7 +1086,7 @@ Helm: Values: {
target_label: "__param_target"
}, {
target_label: "__address__"
- replacement: "blackbox"
+ replacement: "\(prometheus.blackbox.host):\(prometheus.blackbox.port)"
}, {
source_labels: ["__param_target"]
target_label: "instance"

View File

@@ -8,6 +8,7 @@ sidebar_position: 40
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import YouTube from '@site/src/components/YouTube';
import CodeBlock from '@theme/CodeBlock';
<head>
<meta property="og:title" content="Helm Values | Holos" />
@@ -41,82 +42,57 @@ resource.
## The Code
### Holos Version
Ensure you have a current version of `holos` installed. This document was
tested with the following version.
import HolosVersionCommand from '!!raw-loader!./_helm-values/script-01-holos-version/command.sh';
import HolosVersionOutput from '!!raw-loader!./_helm-values/script-01-holos-version/output.txt';
<CodeBlock language="bash">{HolosVersionCommand}</CodeBlock>
<CodeBlock language="txt">{HolosVersionOutput}</CodeBlock>
### Generating the structure
Use `holos` to generate a minimal platform directory structure. First, create
and navigate into a blank directory, then use the `holos init platform` command:
```shell
mkdir holos-helm-values-tutorial
cd holos-helm-values-tutorial
holos init platform v1alpha5
```
import MkdirAndInit from '!!raw-loader!./_helm-values/script-02-helm-values/mkdir-and-init.sh';
<CodeBlock language="bash">{MkdirAndInit}</CodeBlock>
Make an initial commit to track changes:
```bash
git init . && git add . && git commit -m "initial commit"
```
import GitInit from '!!raw-loader!./_helm-values/script-02-helm-values/git-init.sh';
<CodeBlock language="bash">{GitInit}</CodeBlock>
### Managing the Components
Create the `prometheus` and `blackbox` component directories, then add each of
the following file contents.
```bash
mkdir -p components/prometheus components/blackbox
```
import MkdirComponents from '!!raw-loader!./_helm-values/script-02-helm-values/mkdir-components.sh';
import PrometheusComponentHeader from '!!raw-loader!./_helm-values/script-02-helm-values/prometheus-component-header.sh';
import PrometheusComponentBody from '!!raw-loader!./_helm-values/script-02-helm-values/prometheus-component-body.cue';
import BlackboxComponentHeader from '!!raw-loader!./_helm-values/script-02-helm-values/blackbox-component-header.sh';
import BlackboxComponentBody from '!!raw-loader!./_helm-values/script-02-helm-values/blackbox-component-body.cue';
import EofTrailer from '!!raw-loader!./_helm-values/script-02-helm-values/eof-trailer.sh';
<CodeBlock language="bash">{MkdirComponents}</CodeBlock>
<Tabs groupId="D15A3008-1EFC-4D34-BED1-15BC0C736CC3">
<TabItem value="prometheus.cue" label="prometheus.cue">
```bash
cat <<EOF > components/prometheus/prometheus.cue
```
```cue showLineNumbers
package holos
// Produce a helm chart build plan.
holos: Helm.BuildPlan
Helm: #Helm & {
Chart: {
name: "prometheus"
version: "25.27.0"
repository: {
name: "prometheus-community"
url: "https://prometheus-community.github.io/helm-charts"
}
}
}
```
```bash
EOF
```
<CodeBlock language="bash">{PrometheusComponentHeader}</CodeBlock>
<CodeBlock language="cue" showLineNumbers>{PrometheusComponentBody}</CodeBlock>
<CodeBlock language="bash">{EofTrailer}</CodeBlock>
</TabItem>
<TabItem value="blackbox.cue" label="blackbox.cue">
```bash
cat <<EOF > components/blackbox/blackbox.cue
```
```cue showLineNumbers
package holos
// Produce a helm chart build plan.
holos: Helm.BuildPlan
Helm: #Helm & {
Chart: {
name: "prometheus-blackbox-exporter"
version: "9.0.1"
repository: {
name: "prometheus-community"
url: "https://prometheus-community.github.io/helm-charts"
}
}
}
```
```bash
EOF
```
<CodeBlock language="bash">{BlackboxComponentHeader}</CodeBlock>
<CodeBlock language="cue" showLineNumbers>{BlackboxComponentBody}</CodeBlock>
<CodeBlock language="bash">{EofTrailer}</CodeBlock>
</TabItem>
</Tabs>
@@ -124,64 +100,38 @@ EOF
Register the components with the platform by adding the following file to the platform directory.
```bash
cat <<EOF > platform/prometheus.cue
```
```cue showLineNumbers
package holos
import RegisterComponentsHeader from '!!raw-loader!./_helm-values/script-02-helm-values/register-components-header.sh';
import RegisterComponentsBody from '!!raw-loader!./_helm-values/script-02-helm-values/register-components-body.cue';
Platform: Components: {
prometheus: {
name: "prometheus"
path: "components/prometheus"
}
blackbox: {
name: "blackbox"
path: "components/blackbox"
}
}
```
```bash
EOF
```
<CodeBlock language="bash">{RegisterComponentsHeader}</CodeBlock>
<CodeBlock language="cue" showLineNumbers>{RegisterComponentsBody}</CodeBlock>
<CodeBlock language="bash">{EofTrailer}</CodeBlock>
Render the platform.
import RenderCommand from '!!raw-loader!./_helm-values/script-02-helm-values/render.sh';
import RegisterComponentsRenderOutput from '!!raw-loader!./_helm-values/script-02-helm-values/register-components-output.txt';
<Tabs groupId="33D6BFED-62D8-4A42-A26A-F3121D57C4E5">
<TabItem value="command" label="Command">
```bash
holos render platform
```
<CodeBlock language="bash">{RenderCommand}</CodeBlock>
</TabItem>
<TabItem value="output" label="Output">
```txt
cached prometheus-blackbox-exporter 9.0.1
rendered blackbox in 3.825430417s
cached prometheus 25.27.0
rendered prometheus in 4.840089667s
rendered platform in 4.840137792s
```
<CodeBlock language="txt">{RegisterComponentsRenderOutput}</CodeBlock>
</TabItem>
</Tabs>
Commit the results.
import GitCommitRegisterComponents from '!!raw-loader!./_helm-values/script-02-helm-values/register-components-git-commit.sh';
import RegisterComponentsGitOutput from '!!raw-loader!./_helm-values/script-02-helm-values/register-components-git-commit-output.txt';
<Tabs groupId="446CC550-A634-45C0-BEC7-992E5C56D4FA">
<TabItem value="command" label="Command">
```bash
git add . && git commit -m 'add blackbox and prometheus'
```
<CodeBlock language="bash">{GitCommitRegisterComponents}</CodeBlock>
</TabItem>
<TabItem value="output" label="Output">
```txt
[main b5df111] add blackbox and prometheus
5 files changed, 1550 insertions(+)
create mode 100644 components/blackbox/blackbox.cue
create mode 100644 components/prometheus/prometheus.cue
create mode 100644 deploy/components/blackbox/blackbox.gen.yaml
create mode 100644 deploy/components/prometheus/prometheus.gen.yaml
create mode 100644 platform/prometheus.cue
```
<CodeBlock language="txt">{RegisterComponentsGitOutput}</CodeBlock>
</TabItem>
</Tabs>
@@ -190,21 +140,11 @@ git add . && git commit -m 'add blackbox and prometheus'
Holos renders Helm charts with their default values. We can import these default
values into CUE to work with them as structured data instead of text markup.
```bash
holos cue import \
--package holos \
--path 'Helm: Values:' \
--outfile components/prometheus/values.cue \
components/prometheus/vendor/25.27.0/prometheus/values.yaml
```
import ImportPrometheusValues from '!!raw-loader!./_helm-values/script-02-helm-values/import-prometheus-values.sh';
import ImportBlackboxValues from '!!raw-loader!./_helm-values/script-02-helm-values/import-blackbox-values.sh';
```bash
holos cue import \
--package holos \
--path 'Helm: Values:' \
--outfile components/blackbox/values.cue \
components/blackbox/vendor/9.0.1/prometheus-blackbox-exporter/values.yaml
```
<CodeBlock language="bash">{ImportPrometheusValues}</CodeBlock>
<CodeBlock language="bash">{ImportBlackboxValues}</CodeBlock>
These commands convert the YAML data into CUE code and nest the values under the
`Values` field of the `Helm` struct.
@@ -215,67 +155,43 @@ CUE unifies `values.cue` with the other `\*.cue` files in the same directory.
Render the platform using `holos render platform` and commit the results.
import ImportValuesRenderOutput from '!!raw-loader!./_helm-values/script-02-helm-values/import-values-render-output.txt';
import ImportValuesGitCommit from '!!raw-loader!./_helm-values/script-02-helm-values/import-values-git-commit.sh';
import ImportValuesGitOutput from '!!raw-loader!./_helm-values/script-02-helm-values/import-values-git-output.txt';
<Tabs groupId="BDDCD65A-2E9D-4BA6-AAE2-8099494D5E4B">
<TabItem value="command" label="Command">
```bash
holos render platform
```
<CodeBlock language="bash">{RenderCommand}</CodeBlock>
</TabItem>
<TabItem value="output" label="Output">
```txt
rendered blackbox in 365.936792ms
rendered prometheus in 371.855875ms
rendered platform in 372.109916ms
```
<CodeBlock language="txt">{ImportValuesRenderOutput}</CodeBlock>
</TabItem>
</Tabs>
<Tabs groupId="1636C619-258E-4D49-8052-F64B588C9177">
<TabItem value="command" label="Command">
```bash
git add . && git commit -m 'import values'
```
<CodeBlock language="bash">{ImportValuesGitCommit}</CodeBlock>
</TabItem>
<TabItem value="output" label="Output">
```txt
[main 52e90ea] import values
2 files changed, 1815 insertions(+)
create mode 100644 components/blackbox/values.cue
create mode 100644 components/prometheus/values.cue
```
<CodeBlock language="txt">{ImportValuesGitOutput}</CodeBlock>
</TabItem>
</Tabs>
### Managing Common Configuration
To manage shared configuration for both Helm charts, define a structure that
holds the common configuration values. Place this configuration in the
`components` directory to ensure it is accessible to all components.
holds the common configuration values. Create a `config` directory at the root
of the repository, and place the configuration file there to ensure it is
accessible to all components.
import BlackboxCommonConfigMkdir from '!!raw-loader!./_helm-values/script-02-helm-values/mkdir-common-config.sh';
import BlackboxCommonConfigHeader from '!!raw-loader!./_helm-values/script-02-helm-values/blackbox-common-config-header.sh';
import BlackboxCommonConfigBody from '!!raw-loader!./_helm-values/script-02-helm-values/blackbox-common-config-body.cue';
```bash
cat <<EOF > components/blackbox.cue
```
```cue showLineNumbers
package holos
// Schema Definition
#Blackbox: {
// host constrained to a lower case dns label
host: string & =~"^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$"
// port constrained to a valid range
port: int & >0 & <=65535
}
// Concrete values must validate against the schema.
Blackbox: #Blackbox & {
host: "blackbox"
port: 9115
}
```
```bash
EOF
```
<CodeBlock language="bash">{BlackboxCommonConfigMkdir}</CodeBlock>
<CodeBlock language="bash">{BlackboxCommonConfigHeader}</CodeBlock>
<CodeBlock language="cue" showLineNumbers>{BlackboxCommonConfigBody}</CodeBlock>
<CodeBlock language="bash" showLineNumbers>{EofTrailer}</CodeBlock>
:::important
1. CUE loads and unifies all `*.cue` files from the root directory containing
@@ -286,75 +202,41 @@ languages with only type checking.
Add and commit the configuration.
import BlackboxCommonConfigGit from '!!raw-loader!./_helm-values/script-02-helm-values/blackbox-common-config-git-commit.sh';
import BlackboxCommonConfigGitOutput from '!!raw-loader!./_helm-values/script-02-helm-values/blackbox-common-config-git-output.txt';
<Tabs groupId="A738CCE4-F0C6-4CC7-BE1F-2B92F0E86FDC">
<TabItem value="command" label="Command">
```bash
git add . && git commit -m 'add blackbox configuration'
```
<CodeBlock language="bash">{BlackboxCommonConfigGit}</CodeBlock>
</TabItem>
<TabItem value="output" label="Output">
```txt
[main 1adcd08] add blackbox configuration
1 file changed, 15 insertions(+)
create mode 100644 components/blackbox.cue
```
<CodeBlock language="bash">{BlackboxCommonConfigGitOutput}</CodeBlock>
</TabItem>
</Tabs>
### Using Common Configuration Across Components
Referencing common configuration across multiple components is straightforward
and reliable using Holos and CUE.
and reliable using Holos and CUE. Configuration can be imported where necessary
following [CUE module standards], which are similar to Golang.
To apply the common configuration, patch the two `values.cue` files, or manually
edit them to reference `Blackbox.host` and `Blackbox.port`.
edit them to import the configuration and reference `prometheus.blackbox.host`
and `prometheus.blackbox.port`.
import CommonConfigPatchCommand from '!!raw-loader!./_helm-values/script-02-helm-values/common-config-patch.sh';
import CommonConfigPatchDiff from '!!raw-loader!./_helm-values/script-02-helm-values/values.patch';
import CommonConfigPatchOutput from '!!raw-loader!./_helm-values/script-02-helm-values/common-config-patch.txt';
<Tabs groupId="5FFCE892-B8D4-4F5B-B2E2-39EC9E9F87A4">
<TabItem value="command" label="Command">
```bash
patch -p1 < values.patch
```
<CodeBlock language="bash">{CommonConfigPatchCommand}</CodeBlock>
</TabItem>
<TabItem value="patch" label="values.patch">
```diff
--- a/components/blackbox/values.cue
+++ b/components/blackbox/values.cue
@@ -1,6 +1,8 @@
package holos
Helm: Values: {
+ fullnameOverride: Blackbox.host
+
global: {
//# Global image registry to use if it needs to be overriden for some specific use cases (e.g local registries, custom images, ...)
//#
@@ -192,7 +194,7 @@ Helm: Values: {
annotations: {}
labels: {}
type: "ClusterIP"
- port: 9115
+ port: Blackbox.port
ipDualStack: {
enabled: false
ipFamilies: ["IPv6", "IPv4"]
--- a/components/prometheus/values.cue
+++ b/components/prometheus/values.cue
@@ -1083,7 +1083,7 @@ Helm: Values: {
target_label: "__param_target"
}, {
target_label: "__address__"
- replacement: "blackbox"
+ replacement: "\(Blackbox.host):\(Blackbox.port)"
}, {
source_labels: ["__param_target"]
target_label: "instance"
```
<CodeBlock language="diff">{CommonConfigPatchDiff}</CodeBlock>
</TabItem>
<TabItem value="output" label="Output">
```txt
patching file 'components/blackbox/values.cue'
patching file 'components/prometheus/values.cue'
```
<CodeBlock language="txt">{CommonConfigPatchOutput}</CodeBlock>
</TabItem>
</Tabs>
@@ -365,20 +247,17 @@ safely and easily.
Remove the patch file, then commit the changes.
import CommonConfigPatchRm from '!!raw-loader!./_helm-values/script-02-helm-values/common-config-rm.sh';
import CommonConfigPatchGitCommit from '!!raw-loader!./_helm-values/script-02-helm-values/common-config-git.sh';
import CommonConfigPatchGitCommitOutput from '!!raw-loader!./_helm-values/script-02-helm-values/common-config-git-output.txt';
<Tabs groupId="6498B00E-FADA-4EB2-885C-808F1D22E04D">
<TabItem value="command" label="Command">
```bash
rm values.patch
```
```bash
git add . && git commit -m 'integrate blackbox and prometheus together'
```
<CodeBlock language="bash">{CommonConfigPatchRm}</CodeBlock>
<CodeBlock language="bash">{CommonConfigPatchGitCommit}</CodeBlock>
</TabItem>
<TabItem value="output" label="Output">
```txt
[main 4221803] integrate blackbox and prometheus together
2 files changed, 4 insertions(+), 2 deletions(-)
```
<CodeBlock language="txt">{CommonConfigPatchGitCommitOutput}</CodeBlock>
</TabItem>
</Tabs>
@@ -387,97 +266,28 @@ git add . && git commit -m 'integrate blackbox and prometheus together'
Holos makes it easy to view and review platform-wide changes. Render the
platform to observe how both Prometheus and Blackbox update in sync.
import ReviewingChangesRenderOutput from '!!raw-loader!./_helm-values/script-02-helm-values/reviewing-changes-render-output.txt';
<Tabs groupId="E7F6D8B1-22FA-4075-9B44-D9F2815FE0D3">
<TabItem value="command" label="Command">
```bash
holos render platform
```
<CodeBlock language="bash">{RenderCommand}</CodeBlock>
</TabItem>
<TabItem value="output" label="Output">
```txt
rendered blackbox in 374.810666ms
rendered prometheus in 382.899334ms
rendered platform in 383.270625ms
```
<CodeBlock language="txt">{ReviewingChangesRenderOutput}</CodeBlock>
</TabItem>
</Tabs>
Changes are easily visible in version control.
import GitDiffCommand from '!!raw-loader!./_helm-values/script-02-helm-values/git-diff.sh';
import GitDiff from '!!raw-loader!./_helm-values/script-02-helm-values/git.diff';
<Tabs groupId="9789A0EF-24D4-4FB9-978A-3895C2778789">
<TabItem value="command" label="Command">
```bash
git diff
```
<CodeBlock language="bash">{GitDiffCommand}</CodeBlock>
</TabItem>
<TabItem value="output" label="Output">
```diff
diff --git a/deploy/components/blackbox/blackbox.gen.yaml b/deploy/components/blackbox/blackbox.gen.yaml
index 3db20cd..5336f44 100644
--- a/deploy/components/blackbox/blackbox.gen.yaml
+++ b/deploy/components/blackbox/blackbox.gen.yaml
@@ -7,7 +7,7 @@ metadata:
app.kubernetes.io/name: prometheus-blackbox-exporter
app.kubernetes.io/version: v0.25.0
helm.sh/chart: prometheus-blackbox-exporter-9.0.1
- name: prometheus-blackbox-exporter
+ name: blackbox
namespace: default
---
apiVersion: v1
@@ -31,7 +31,7 @@ metadata:
app.kubernetes.io/name: prometheus-blackbox-exporter
app.kubernetes.io/version: v0.25.0
helm.sh/chart: prometheus-blackbox-exporter-9.0.1
- name: prometheus-blackbox-exporter
+ name: blackbox
namespace: default
---
apiVersion: v1
@@ -43,7 +43,7 @@ metadata:
app.kubernetes.io/name: prometheus-blackbox-exporter
app.kubernetes.io/version: v0.25.0
helm.sh/chart: prometheus-blackbox-exporter-9.0.1
- name: prometheus-blackbox-exporter
+ name: blackbox
namespace: default
spec:
ports:
@@ -65,7 +65,7 @@ metadata:
app.kubernetes.io/name: prometheus-blackbox-exporter
app.kubernetes.io/version: v0.25.0
helm.sh/chart: prometheus-blackbox-exporter-9.0.1
- name: prometheus-blackbox-exporter
+ name: blackbox
namespace: default
spec:
replicas: 1
@@ -119,8 +119,8 @@ spec:
name: config
hostNetwork: false
restartPolicy: Always
- serviceAccountName: prometheus-blackbox-exporter
+ serviceAccountName: blackbox
volumes:
- configMap:
- name: prometheus-blackbox-exporter
+ name: blackbox
name: config
diff --git a/deploy/components/prometheus/prometheus.gen.yaml b/deploy/components/prometheus/prometheus.gen.yaml
index 9e02bce..ab638f0 100644
--- a/deploy/components/prometheus/prometheus.gen.yaml
+++ b/deploy/components/prometheus/prometheus.gen.yaml
@@ -589,7 +589,7 @@ data:
- source_labels:
- __address__
target_label: __param_target
- - replacement: blackbox
+ - replacement: blackbox:9115
target_label: __address__
- source_labels:
- __param_target
```
<CodeBlock language="diff">{GitDiff}</CodeBlock>
</TabItem>
</Tabs>
@@ -494,17 +304,15 @@ Blackbox host or port will reconfigure both charts correctly.
Commit the changes and proceed to deploy them.
import ReviewingChangesGitCommit from '!!raw-loader!./_helm-values/script-02-helm-values/reviewing-changes-git-commit.sh';
import ReviewingChangesGitOutput from '!!raw-loader!./_helm-values/script-02-helm-values/reviewing-changes-git-output.txt';
<Tabs groupId="F8C9A98D-DE1E-4EF6-92C1-017A9166F6C7">
<TabItem value="command" label="Command">
```bash
git add . && git commit -m 'render integrated blackbox and prometheus manifests'
```
<CodeBlock language="bash">{ReviewingChangesGitCommit}</CodeBlock>
</TabItem>
<TabItem value="output" label="Output">
```txt
[main 67efe0d] render integrated blackbox and prometheus manifests
2 files changed, 7 insertions(+), 7 deletions(-)
```
<CodeBlock language="txt">{ReviewingChangesGitOutput}</CodeBlock>
</TabItem>
</Tabs>
@@ -524,7 +332,7 @@ service endpoint.
[prometheus]: https://github.com/prometheus-community/helm-charts/tree/prometheus-25.27.0/charts/prometheus
[blackbox]: https://github.com/prometheus-community/helm-charts/tree/prometheus-blackbox-exporter-9.0.1/charts/prometheus-blackbox-exporter
[httpbin]: https://github.com/mccutchen/go-httpbin/tree/v2.15.0
[CUE module standards]: https://cuelang.org/docs/concept/modules-packages-instances/
[Config Schema]: #config-schema
[Technical Overview]: ./overview.mdx

View File

@@ -0,0 +1,87 @@
package main
import (
"os"
"path/filepath"
"runtime"
"slices"
"strings"
"testing"
"github.com/holos-run/holos/cmd"
"github.com/rogpeppe/go-internal/testscript"
cue "cuelang.org/go/cmd/cue/cmd"
)
func TestMain(m *testing.M) {
os.Exit(testscript.RunMain(m, map[string]func() int{
"holos": cmd.MakeMain(),
"cue": cue.Main,
}))
}
// Run these with go test -v to see the verbose names
func TestHelmValues(t *testing.T) {
t.Run("TestHelmValues", func(t *testing.T) {
// Get an ordered list of test script files.
dir := "_helm-values"
for _, file := range sortedTestScripts(t, filepath.Join(dir, "examples")) {
t.Run("examples", func(t *testing.T) {
runOneScript(t, dir, file)
})
}
})
}
func runOneScript(t *testing.T, dir string, file string) {
params := testscript.Params{
Dir: "",
Files: []string{file},
RequireExplicitExec: true,
RequireUniqueNames: false,
WorkdirRoot: filepath.Join(testDir(t), dir),
UpdateScripts: os.Getenv("HOLOS_UPDATE_SCRIPTS") != "",
Setup: func(env *testscript.Env) error {
// Needed for update.sh to determine if we need to update output files.
env.Setenv("HOLOS_UPDATE_SCRIPTS", os.Getenv("HOLOS_UPDATE_SCRIPTS"))
// Just like cmd/cue/cmd.TestScript, set up separate cache and config dirs per test.
env.Setenv("CUE_CACHE_DIR", filepath.Join(env.WorkDir, "tmp/cachedir"))
configDir := filepath.Join(env.WorkDir, "tmp/configdir")
env.Setenv("CUE_CONFIG_DIR", configDir)
return nil
},
}
testscript.Run(t, params)
}
// testDir returns the path of the directory containing the go source file of
// the caller.
func testDir(t *testing.T) string {
_, file, _, ok := runtime.Caller(0)
if !ok {
t.Fatal("could not get runtime caller")
}
return filepath.Dir(file)
}
func sortedTestScripts(t *testing.T, dir string) (files []string) {
entries, err := os.ReadDir(dir)
if os.IsNotExist(err) {
// Continue to helpful error on len(files) == 0 below.
} else if err != nil {
t.Fatal(err)
}
for _, entry := range entries {
name := entry.Name()
if strings.HasSuffix(name, ".txtar") || strings.HasSuffix(name, ".txt") {
files = append(files, filepath.Join(dir, name))
}
}
if len(files) == 0 {
t.Fatalf("no txtar nor txt scripts found in dir %s", dir)
}
slices.Sort(files)
return files
}

View File

@@ -38,8 +38,23 @@ go install github.com/holos-run/holos/cmd/holos@latest
### Completion
:::tip
Completion is automatically enabled if [brew shell
completion](https://docs.brew.sh/Shell-Completion) is also enabled.
:::
<Tabs groupId="65F79D28-2E57-4A90-8EBA-3D8758C80233">
<TabItem value="zsh" label="zsh">
Add the following to `~/.zshrc` if not already present to initialize zsh completion.
```bash
autoload -Uz compinit
compinit
```
Then load holos completion after zsh completion has been initialized.
```bash
source <(holos completion zsh)
```

View File

@@ -18,3 +18,7 @@
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Auto-generated doc examples
multi-sources-example/
kargo-demo/

View File

@@ -0,0 +1,627 @@
---
unlisted: true
slug: trade-in-argocd-appsets-for-rendered-manifests
title: Trade in the complexity of ArgoCD AppSets for fully rendered manifests
authors: [jeff]
tags: [holos, helm, gitops]
# image: /img/cards/validators.png
description: Migrate an ApplicationSet to the rendered manifest pattern with Holos, removing unnecessary complexity.
keywords:
- Holos
- CUE
- Configuration
- Structure
- Kubernetes
- Hydrated
- Rendered
- Manifest
- Pattern
- Unification
- ArgoCD
- ApplicationSet
- Application
- Multi Source
- Values Hierarchy
- Rendered Manifest Pattern
- GitOps
- Complexity
---
Kubernetes has a reputation for being too complex. Complexity in software
engineering is often categorized as essential or accidental. Methods to expose
services to the internet, deploy replicas, manage secrets, configure network
connectivity, and control access are examples of essential complexity. They're
unavoidable no matter what tools we use.
Nevertheless, popular tools in the ecosystem bolster this reputation by
accidentally piling up layers of complexity unnecessarily. For example, Helm
value override hierarchies and ArgoCD ApplicationSet templates. Both tools
solve challenging, pervasive problems resulting in their widespread adoption.
Unfortunately the trade offs each one makes independently combine together
poorly. Helm and ArgoCD ApplicationSets contribute more than their fair share
of accidental complexity to the Kubernetes ecosystem.
Consider the use case of deploying different versions of the same service to
multiple environments. An ArgoCD ApplicationSet passing sets of values to one
Helm chart is a commonly recommended solution contributing three forms of
accidental complexity.
1. There are multiple layers of Go template abstractions.
2. Config values are silently written over at multiple layers in a hierarchy.
3. The intermediate and final configuration is remote and out of reach.
The rendered manifests pattern is an alternative solution leveraging Helm and
ArgoCD with less complexity. The pattern reduces complexity by collapsing
multiple layers of Go templates and brings the configuration local, within our
reach. It still relies on Helm value hierarchies when charts are reused across
environments, so it's no silver bullet, but we'll explain in follow up post how
Holos can eliminate the accidental complexity of a Helm value files hierarchy.
There is no widely agreed upon, freely available implementation of the rendered
manifest pattern. Engineering teams have to decide whether to implement the
pattern from scratch or stick with the more complex, but built-in features of
Helm and ArgoCD. These few options indicate a tooling gap in the ecosystem.
This gap pushes many organizations toward the accidental complexity of the
ApplicationSet solution.
Holos fills this gap by offering a thoughtful and complete implementation of the
rendered manifest pattern in one command line tool. This article is the first
in a series exploring how Holos solves the same use case while avoiding the
accidental complexity.
This article walks step-by-step through the process of migrating an
ApplicationSet to Holos. I'll explain why we feel the trades we made in Holos
are a net improvement. At the end of the article you'll see how you can
continue leveraging GitOps with ArgoCD and Helm while gaining the ability to see
and comprehend complex configurations clearly, with fewer layers of abstraction.
{/* truncate */}
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import CodeBlock from '@theme/CodeBlock';
## Layers of Complexity
Before diving into the step by step migration, let's explore exactly what we
mean by accidental complexity. Consider the ApplicationSet and Helm value
hierarchy described in the final recommendation of _[Using Helm Hierarchies in
Multi-Source Argo CD Applications for Promoting to Different GitOps
Environments][tfa]_. This particular ApplicationSet is a prime example because
the approach it takes is often recommended as a solution for deploying a service
to multiple clusters and environments.
Unfortunately the example provided requires us to hold _at least 7 layers of
abstraction_ in mind while considering the impact of a change.
- The ApplicationSet renders an Application template for each `config.json` file.
- Each Application `valueFiles` field introduces 5 more layers of potential overrides.
- The Helm Chart is another layer of text templates.
:::important
Consider the multiple layers of abstraction in the example provided compared
with their replacements shown in the [Goals](#goals) section.
:::
import AppSetPath from '!!raw-loader!./_migrate_appset/script-02-layers-of-complexity/appset.path';
import AppSetYAML from '!!raw-loader!./_migrate_appset/script-02-layers-of-complexity/appset.yaml';
import DeploymentPath from '!!raw-loader!./_migrate_appset/script-02-layers-of-complexity/deployment.path';
import DeploymentYAML from '!!raw-loader!./_migrate_appset/script-02-layers-of-complexity/deployment.yaml';
<Tabs groupId="layers-of-complexity">
<TabItem value="Application Template" label="Application Template">
The ApplicationSet template renders a Helm template.
<CodeBlock language="txt">{AppSetPath}</CodeBlock>
<CodeBlock language="yaml">{AppSetYAML}</CodeBlock>
</TabItem>
<TabItem value="Deployment Template" label="Deployment Template">
The Helm template renders the final Deployment configuration.
<CodeBlock language="txt">{DeploymentPath}</CodeBlock>
<CodeBlock language="yaml">{DeploymentYAML}</CodeBlock>
</TabItem>
</Tabs>
These templates would be easier to comprehend if their intermediate state and
final configuration were within reach. Unfortunately, it's stored in multiple
remote repositories and processed remotely in ArgoCD. ArgoCD is like a black
box. Intermediate state is inaccessible. The final configuration is only
accessible after it's been applied, far too late to consider what impact a
change might have.
We know the complexity is accidental because we bypass these layers when we run
`kubectl edit`. The configuration is fetched, manipulated locally, then pushed
back to the cluster without obfuscation.
We also know the complexity is real. It's too easy to make mistakes when
configuration values are silently written over many times along the way. For
example, we'll see how the very ApplicationSet we're migrating contains errors
which are difficult to fix without insight a tool like `holos` provides.
## Goals
We want to eliminate as many layers of accidental complexity as possible with as
few changes as possible. Ideally we'll be able to directly see and manipulate
the final manifests as they will be applied to the cluster. We also want the
migration to balance the conventions and idioms of Holos, Helm, and ArgoCD as
much as possible.
1. Eliminate as many layers of accidental complexity as possible.
2. Make as few changes as possible.
3. Bring intermediate state and the final configuration within our reach.
4. Use each tool idiomatically.
We'll relocate the unmodified Helm chart, config.json files, and value files to
make as few changes as possible. We'll also generate Applications for ArgoCD
identical to those generated by the ApplicationSet, but we'll do so with the
same [CUE] layer that configures everything else in Holos.
The migration achieves these goals by rendering clearly readable Application and
Deployment resources to local files for ArgoCD to sync via GitOps. Here's how
it will look.
:::important
Compare the Application and Deployment with the templates in the [Layers of
Complexity](#layers-of-complexity) section.
:::
import AppTreeCommand from '!!raw-loader!./_migrate_appset/script-05-application/tree-deploy.sh'
import AppTreeOutput from '!!raw-loader!./_migrate_appset/script-05-application/tree-deploy.txt'
import AppPath from '!!raw-loader!./_migrate_appset/script-05-application/app.path'
import AppYAML from '!!raw-loader!./_migrate_appset/script-05-application/app.yaml'
import ManifestPath from '!!raw-loader!./_migrate_appset/script-05-application/manifest.path'
import ManifestYAML from '!!raw-loader!./_migrate_appset/script-05-application/manifest.yaml'
<Tabs groupId="goal-summary">
<TabItem value="Application" label="Application">
<CodeBlock language="yaml">
{"# "+AppPath}
{AppYAML}
</CodeBlock>
</TabItem>
<TabItem value="Deployment" label="Deployment">
<CodeBlock language="yaml">
{"# "+ManifestPath}
{ManifestYAML}
</CodeBlock>
</TabItem>
<TabItem value="Tree" label="Tree">
<CodeBlock language="bash">{AppTreeCommand}</CodeBlock>
<CodeBlock language="txt">{AppTreeOutput}</CodeBlock>
</TabItem>
</Tabs>
The manifests rendered at the end of the migration achieve the goals.
1. The Application rendered by Holos is readable and complete, replacing the ApplicationSet template rendered remotely.
2. The Application is equivalent to those produced by the ApplicationSet.
3. The final configuration is within reach in local files. We'll see how Holos exposes intermediate state as we step through the migration in the next section.
4. Both ArgoCD and Helm are used idiomatically, passing the value files to render the chart for GitOps.
## Migration Steps
We'll migrate each of the three major behaviors of the ApplicationSet to Holos
to achieve the goals and complete the migration.
1. Generate an Application from a template using values provided by each `config.json` file.
2. Render `my-chart` to a manifest by providing a hierarchy of helm values determined by `config.json` values.
3. Reconcile the rendered manifest with the cluster state.
We'll start by loading the `config.json` environment data files into CUE without
modifying the original data. Then we'll manage a Holos [Platform] [Component]
for each environment. We'll wrap `my-chart` in a component definition
and pass the value hierarchy to `helm template` the same as ArgoCD does.
Finally, we'll mix an ArgoCD Application into each platform component to achieve
the same output as the ApplicationSet.
Along the way we'll see how Holos eliminates accidental complexity and makes it
easier to work with the intermediate and final configuration.
### Initial Setup
The main branch of the [multi-sources-example] is a fork of the example code
from the original article that has already been migrated. We'll roll back to
the fork point then step through each of the commits to complete the migration.
First, clone the repository.
import GitCloneCommand from '!!raw-loader!./_migrate_appset/script-01-clone/clone.sh';
import GitCloneOutput from '!!raw-loader!./_migrate_appset/script-01-clone/clone.txt';
<Tabs groupId="clone">
<TabItem value="Command" label="Command">
<CodeBlock language="bash">{GitCloneCommand}</CodeBlock>
</TabItem>
<TabItem value="Output" label="Output">
<CodeBlock language="txt">{GitCloneOutput}</CodeBlock>
</TabItem>
</Tabs>
Then, reset to where it was forked from upstream.
import GitResetCommand from '!!raw-loader!./_migrate_appset/script-01-clone/reset.sh';
import GitResetOutput from '!!raw-loader!./_migrate_appset/script-01-clone/reset.txt';
<Tabs groupId="reset">
<TabItem value="Command" label="Command">
<CodeBlock language="bash">{GitResetCommand}</CodeBlock>
</TabItem>
<TabItem value="Output" label="Output">
<CodeBlock language="txt">{GitResetOutput}</CodeBlock>
</TabItem>
</Tabs>
[Install Holos] if you haven't already. This article has been tested with version:
import HolosVersionCmd from '!!raw-loader!./_migrate_appset/script-01-clone/version.sh';
import HolosVersionTxt from '!!raw-loader!./_migrate_appset/script-01-clone/version.txt';
<Tabs groupId="version">
<TabItem value="Version" label="Version">
<CodeBlock language="txt">{HolosVersionTxt}</CodeBlock>
</TabItem>
<TabItem value="Command" label="Command">
<CodeBlock language="bash">{HolosVersionCmd}</CodeBlock>
</TabItem>
</Tabs>
### Holos Structure
Holos is organized for use as a GitOps repository. The main folders are:
| Folder | Description |
| - | - |
| platform | Entry point for `holos render platform` which renders all manifests. |
| config | Configuration data, we'll move the `config.json` files here. |
| components | Component definitions live here. Components produce build plans for `holos` to render manifests. We'll wrap `my-chart` in a reusable component definition. |
| deploy | Fully rendered manifests are written here for ArgoCD to sync. |
| cue.mod | [CUE] type definitions and schemas reside here. |
Initialize the platform.
import HolosInit from '!!raw-loader!./_migrate_appset/script-03-holos-structure/holos-init.sh';
<CodeBlock language="bash">{"# --force is necessary when the current directory is not empty\n"+HolosInit}</CodeBlock>
Now that we've initialized the current directory as a Holos platform repository
we can move the example files from the original article into their conventional
locations in Holos.
import MoveFiles from '!!raw-loader!./_migrate_appset/script-03-holos-structure/move-files-around.sh';
<CodeBlock language="bash">{MoveFiles}</CodeBlock>
### Environment Configs
The ApplicationSet generators field iterates over 8 config.json files to
instantiate each Application from the spec.template field. We'll migrate this
to a similar mechanism in Holos by using CUE's `@embed` feature to load the same
files into one struct. We'll manage one Helm Component for each config.json
value in the struct. This struct will reside in the `config` field of the
`environments` package. Like Go, CUE supports package imports for reuse.
These `config.json` files moved to the `config/environments/` folder. The
`config` folder is the conventional place in Holos for reusable config values
like these.
:::important
Holos offers one unified layer with CUE to configure an entire platform
holistically, different from other tools like Helm and Kustomize.
:::
Here's how the environments package is defined in CUE.
import EnvironmentsPackageHeader from '!!raw-loader!./_migrate_appset/script-03-holos-structure/environments-header.sh';
import EnvironmentsPackageBody from '!!raw-loader!./_migrate_appset/script-03-holos-structure/environments.cue';
import EnvironmentsPackageTrailer from '!!raw-loader!./_migrate_appset/script-03-holos-structure/environments-trailer.sh';
<CodeBlock language="bash">{EnvironmentsPackageHeader}</CodeBlock>
<CodeBlock language="cue" showLineNumbers>{EnvironmentsPackageBody}</CodeBlock>
<CodeBlock language="bash">{EnvironmentsPackageTrailer}</CodeBlock>
We moved the original `config.json` files without modifying them, then used
CUE's `@embed` feature to load them into the `config` struct. This structure is
accessible in CUE by importing the `environments` package, then referencing
`environments.config`.
:::tip
Holos and CUE offer fast, local query and manipulation of your configuration
data, even in intermediate states.
:::
Here's how the environments package exports to YAML. `cue export` and `cue
eval` are handy ways to query intermediate state.
import InspectEnvironmentsCommand from '!!raw-loader!./_migrate_appset/script-03-holos-structure/inspect-environments.sh';
import InspectEnvironmentsOutput from '!!raw-loader!./_migrate_appset/script-03-holos-structure/inspect-environments.txt';
<Tabs groupId="inspect-environments">
<TabItem value="Command" label="Command">
<CodeBlock language="bash">{InspectEnvironmentsCommand}</CodeBlock>
</TabItem>
<TabItem value="Output" label="Output">
<CodeBlock language="yaml" showLineNumbers>{InspectEnvironmentsOutput}</CodeBlock>
</TabItem>
</Tabs>
:::important
CUE populates the `config` field on the first line of output from the `config: _ @embed(...)`
lines in `environments.cue`.
:::
Holos and CUE offer multiple improvements over the ApplicationSet we're
migrating from.
1. **Validation** - Each `config.json` file is validated against the `#Config` schema we defined.
2. **Ergonomics** - Holos enables fast, local queries over one unified
configuration. The data is at hand. We don't need a controller running in a
remote cluster. We can query the intermediate config data structure, improving
our ability to troubleshoot problems the same way an interactive debugger gives
access to intermediate state.
3. **Flexibility** - We're no longer locked into ArgoCD functionality. The
`environments` package can be imported and re-used across the unified platform
configuration layer. We're able to continue processing the rendered manifests
with other tools beyond just ArgoCD.
We've successfully migrated the ApplicationSet generator field to Holos. Next,
we'll iterate over this structure to render `my-chart` for each environment.
### Rendering my-chart
The next step is to render `my-chart` into complete configuration manifests.
Holos implements the same behavior as the Application's `spec.sources.helm`
field. Both tools use Helm to render the chart. Holos is different though, it
offers a flexible way to transform and validate the output of Helm, then stops
once the manifest is written to a local file. ArgoCD doesn't persist rendered
manifests, instead it applies them directly to the cluster.
Where Holos stops is another major difference from ArgoCD and all other tools
operating as a controller in the cluster. Holos is designed for GitOps and
integration with the rest of the Kubernetes ecosystem. Holos stops after it
writes manifests to local files. This clear cut responsibility leaves ample
space for subsequent automated workflow operating on the configuration Holos
produces.
For example, consider a progressive delivery pipeline at the right side of this
diagram to incrementally roll out configuration changes. ApplicationSets with a
Helm source prevents this kind of integration. Holos with an Application Git
source enables this kind of integration.
import RenderingOverview from '@site/src/diagrams/rendering-overview.mdx';
<RenderingOverview />
ArgoCD pairs well with other ecosystem tools when it keeps to what it does best:
drift detection and reconciliation following GitOps principles. ArgoCD locks
out other tools when it renders manifests. The configuration is transient and
locked away in the cluster.
:::important
Holos renders `my-chart` to local files, one for each of the environment configs
we migrated to the `environments` package.
:::
### Platform Components
The primary entrypoint for Holos is the `platform/` directory. The `holos
render platform` command processes a [Platform] specification exported by CUE
from this directory.
Each Application produced by the ApplicationSet we're migrating maps to a
[Component] listed in the `Platform.spec.components` field. Here's how the
components are added to the Platform in CUE.
{/* 987df87 add platform components to replace ApplicationSets.spec.generators */}
import PlatformChartHeader from '!!raw-loader!./_migrate_appset/script-04-helm-component/platform-my-chart-header.sh';
import PlatformChartBody from '!!raw-loader!./_migrate_appset/script-04-helm-component/platform-my-chart.cue';
import PlatformChartTrailer from '!!raw-loader!./_migrate_appset/script-04-helm-component/platform-my-chart-trailer.sh';
<CodeBlock language="bash">{PlatformChartHeader}</CodeBlock>
<CodeBlock language="cue" showLineNumbers>{PlatformChartBody}</CodeBlock>
<CodeBlock language="bash">{PlatformChartTrailer}</CodeBlock>
For each of the data objects defined in the `config.json` files, we define a
field in the `Platform.Components` struct. We use a struct for convenience,
it's easier to compose components into a struct than it is into an ordered list.
The Platform author schema converts this struct into the `spec.components` list.
`#MyChart` is a schema definition acting as a reusable template. For each of
the environment config files we build the component configuration from
parameters. This is an example of how we compose configuration. The
`outputBaseDir` field is composed in from the `env` field configured in the
original `config.json` files migrated to CUE.
We need to add a configuration snippet so each component accepts this parameter
and renders manifests into folders organized by environment. The use of `@tag`
with the `OutputBaseDir` field indicates the field value is provided by the
Platform spec when we run `holos render platform`.
import ComponentConfigHeader from '!!raw-loader!./_migrate_appset/script-04-helm-component/componentconfig-header.sh';
import ComponentConfigBody from '!!raw-loader!./_migrate_appset/script-04-helm-component/componentconfig.cue';
import ComponentConfigTrailer from '!!raw-loader!./_migrate_appset/script-04-helm-component/componentconfig-trailer.sh';
<CodeBlock language="bash">{ComponentConfigHeader}</CodeBlock>
<CodeBlock language="cue" showLineNumbers>{ComponentConfigBody}</CodeBlock>
<CodeBlock language="bash">{ComponentConfigTrailer}</CodeBlock>
We can gain insight into how `holos` renders the helm charts from the
`config.json` files with the following command. CUE exports the `Platform`
specification to `holos`, which iterates over each of the listed components to
produce a `BuildPlan`.
import ShowPlatformCommand from '!!raw-loader!./_migrate_appset/script-04-helm-component/holos-show-platform.sh';
import ShowPlatformOutput from '!!raw-loader!./_migrate_appset/script-04-helm-component/holos-show-platform.txt';
<Tabs groupId="show-platform">
<TabItem value="Command" label="Command">
<CodeBlock language="bash">{ShowPlatformCommand}</CodeBlock>
</TabItem>
<TabItem value="Output" label="Output">
<CodeBlock language="yaml" showLineNumbers>{ShowPlatformOutput}</CodeBlock>
</TabItem>
</Tabs>
### Component Definition
The next step is to wrap `my-chart` in a Holos [Helm] component definition.
Here's how:
import ComponentHeader from '!!raw-loader!./_migrate_appset/script-04-helm-component/component-my-chart-header.sh';
import ComponentBody from '!!raw-loader!./_migrate_appset/script-04-helm-component/component-my-chart.cue';
import ComponentFooter from '!!raw-loader!./_migrate_appset/script-04-helm-component/component-my-chart-trailer.sh';
<CodeBlock language="bash">{ComponentHeader}</CodeBlock>
<CodeBlock language="cue" showLineNumbers>{ComponentBody}</CodeBlock>
<CodeBlock language="bash">{ComponentFooter}</CodeBlock>
Note how each parameter we added in the Platform spec is reflected in the
component definition with a `@tag`. These are CUE build tags, and the mechanism
by which parameters are passed from `holos render platform` to each component.
Similar to the `config.json` files we migrated, we moved the Helm value files
without modifying them. These files are loaded into one struct in CUE using
`valueFiles: _ @embed(...)`.
Like the Platform spec, we can inspect the BuildPlans `holos` executes to render
each component to manifest files.
import ShowBuildPlansCmd from '!!raw-loader!./_migrate_appset/script-04-helm-component/show-buildplans.sh'
import ShowBuildPlansOut from '!!raw-loader!./_migrate_appset/script-04-helm-component/show-buildplans.txt'
<Tabs groupId="show-buildplans">
<TabItem value="Command" label="Command">
<CodeBlock language="bash">{ShowBuildPlansCmd}</CodeBlock>
</TabItem>
<TabItem value="Output" label="Output">
<CodeBlock language="yaml" showLineNumbers>{ShowBuildPlansOut}</CodeBlock>
</TabItem>
</Tabs>
We can also inspect intermediate configuration like the `valueFiles` struct.
import ValueFilesCommand from '!!raw-loader!./_migrate_appset/script-04-helm-component/inspect-value-files.sh'
import ValueFilesOutput from '!!raw-loader!./_migrate_appset/script-04-helm-component/inspect-value-files.txt'
<Tabs groupId="inspect-value-files">
<TabItem value="Command" label="Command">
<CodeBlock language="bash">{ValueFilesCommand}</CodeBlock>
</TabItem>
<TabItem value="Output" label="Output">
<CodeBlock language="yaml" showLineNumbers>{ValueFilesOutput}</CodeBlock>
</TabItem>
</Tabs>
Render the platform to render `my-chart` for each of the configured
environments.
import RenderCommand from '!!raw-loader!./_migrate_appset/script-04-helm-component/render.sh'
import RenderOutput from '!!raw-loader!./_migrate_appset/script-04-helm-component/render.txt'
<CodeBlock language="bash">{RenderCommand}</CodeBlock>
<CodeBlock language="txt">{RenderOutput}</CodeBlock>
Holos processes the Platform spec.components field concurrently, rendering each
environment to a manifest file into the `deploy` folder. The output looks like:
import TreeCommand from '!!raw-loader!./_migrate_appset/script-04-helm-component/tree-deploy.sh'
import TreeOutput from '!!raw-loader!./_migrate_appset/script-04-helm-component/tree-deploy.txt'
<Tabs groupId="tree-deploy">
<TabItem value="Command" label="Command">
<CodeBlock language="bash">{TreeCommand}</CodeBlock>
</TabItem>
<TabItem value="Output" label="Output">
<CodeBlock language="txt">{TreeOutput}</CodeBlock>
</TabItem>
</Tabs>
### ArgoCD Application
At this point we're rendering `my-chart` with Holos for each environment. The
intermediate and final configuration is within our reach. The final step is to
render an Application resource for each environment like the original
ApplicationSet did.
Holos offers [ComponentConfig] for the purpose of mixing in configuration to
components. The feature is often used to pass each component through
`kustomize` to add common labels and annotations. It's also used to mix in
GitOps resources like ArgoCD Applications and Flux Kustomizations.
Here's how to add an Application for every one of the `Platform` components:
{/* d9125b8 compose argocd application resources into every component */}
import ComponentConfigGitOpsHeader from '!!raw-loader!./_migrate_appset/script-05-application/componentconfig-gitops-header.sh';
import ComponentConfigGitOpsBody from '!!raw-loader!./_migrate_appset/script-05-application/componentconfig-gitops.cue';
import ComponentConfigGitOpsTrailer from '!!raw-loader!./_migrate_appset/script-05-application/componentconfig-gitops-trailer.sh';
<CodeBlock language="bash">{ComponentConfigGitOpsHeader}</CodeBlock>
<CodeBlock language="cue" showLineNumbers>{ComponentConfigGitOpsBody}</CodeBlock>
<CodeBlock language="bash">{ComponentConfigGitOpsTrailer}</CodeBlock>
More information about how this works is available in the following pages. For
now, it's sufficient to know the ComponentConfig is something we usually set and
forget.
1. [ComponentConfig]
2. [GitOps](/docs/v1alpha5/topics/gitops/)
Now we can render the platform and see each of the Application manifest files.
They go into a `gitops` folder so it easy to apply them individually or all at
once for ArgoCD to sync the component manifests.
import AppRenderCommand from '!!raw-loader!./_migrate_appset/script-05-application/render.sh'
import AppRenderOutput from '!!raw-loader!./_migrate_appset/script-05-application/render.txt'
<CodeBlock language="bash">{AppRenderCommand}</CodeBlock>
<CodeBlock language="txt">{AppRenderOutput}</CodeBlock>
<Tabs groupId="tree-deploy-with-application">
<TabItem value="Command" label="Command">
<CodeBlock language="bash">{AppTreeCommand}</CodeBlock>
</TabItem>
<TabItem value="Output" label="Output">
<CodeBlock language="txt">{AppTreeOutput}</CodeBlock>
</TabItem>
</Tabs>
The Applications are also fully rendered.
<CodeBlock language="txt">{AppPath}</CodeBlock>
<CodeBlock language="yaml">{AppYAML}</CodeBlock>
Note how the Application resources Holos produces are easier to read and
understand than the original ApplicationSet.
1. There is no templating.
2. There is no helm source, no value hierarchy to comprehend.
We also have the fully rendered manifest clearly readable and within reach locally.
<CodeBlock language="yaml">{"# "+ManifestPath+ManifestYAML}</CodeBlock>
## Wrapping it all up
1. The manifests are fully rendered and within our reach.
2. The Application is clear and at-hand.
3. We now have a unified platform configuration layer.
## Next Steps
- Part 2 - Why are there 8 config.json files but only 7 components rendered? There's a bug!
- Part 3 - Can we eliminate the layers of helm value overrides? Yes!
- Part 4 - Progressive Delivery. Maybe?
[tfa]: https://medium.com/containers-101/using-helm-hierarchies-in-multi-source-argo-cd-applications-for-promoting-to-different-gitops-133c3bc93678
[ApplicationSet]: https://github.com/holos-run/multi-sources-example/blob/v0.1.0/appsets/4-final/all-my-envs-appset-with-version.yaml
[multi-sources-example]: https://github.com/holos-run/multi-sources-example
[Platform]: https://holos.run/docs/api/core/#Platform
[Component]: https://holos.run/docs/api/core/#Component
[ComponentConfig]: https://holos.run/docs/api/author/#ComponentConfig
[Helm]: https://holos.run/docs/api/core/#Helm
[CUE]: https://cuelang.org
[Install Holos]: https://holos.run/docs/setup/

View File

@@ -0,0 +1,752 @@
---
unlisted: true
slug: simplify-helm-value-files-with-unification
title: Simplify Helm Value Files with Unification
authors: [jeff]
tags: [holos, helm, gitops]
# image: /img/cards/validators.png
description: Migrate an ApplicationSet to the rendered manifest pattern with Holos, removing unnecessary complexity.
keywords:
- Holos
- CUE
- Configuration
- Kubernetes
- Hydrated
- Rendered
- Manifest
- Pattern
- Rendered Manifest Pattern
- Unification
- ArgoCD
- ApplicationSet
- Application
- Multi Source
- Values
- Hierarchy
- Merge
- Override
- GitOps
- Complexity
---
## Simplifying a Hierarchy
In [Part 1] of our series on migrating a complex multi environment
ApplicationSet to Holos we reduced complexity by replacing the ApplicationSet Go
template generator with an Application resource exported from CUE. We completed
the migration to implement the rendered manifest pattern, but complexity remains
in the 5 layers of Helm value file overrides.
We'll continue eliminating accidental complexity by refactoring the Helm value
file hierarchy into one unified layer. The data will be visible and within
reach through the command line. Any future changes will either unify
successfully or produce an immediate error, significantly reducing the
complexity of managing Kubernetes by eliminating the accidental complexity of a
Helm value hierarchy.
[Part 1]: ./2025-01-13-replace-an-applicationset-with-the-rendered-manifest-pattern.mdx
{/* truncate */}
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import CodeBlock from '@theme/CodeBlock';
### Why are overrides a problem?
Consider this snippet form the [original ApplicationSet] we migrated:
```yaml
helm:
valueFiles:
- $values/my-values/common-values.yaml
- $values/my-values/app-version/{{.version}}-values.yaml
- $values/my-values/env-type/{{.type}}-values.yaml
- $values/my-values/regions/{{.region}}-values.yaml
- $values/my-values/envs/{{.env}}-values.yaml
```
Both Holos and ArgoCD pass these value files to Helm in order, resulting in the
following Helm command:
```bash
helm template my-chart \
-f my-values/common-values.yaml \
-f my-values/app-version/prod-values.yaml \
-f my-values/env-type/prod-values.yaml \
-f my-values/regions/us-values.yaml \
-f my-values/envs/prod-us-values.yaml \
...
```
Helm merges each of the value files on top of one another in a last in first out
manner. Imagine we're troubleshooting an issue where the Deployment in the live
system has `replicas: 3` but it should have `replicas: 5`. We need to
reconfigure the replicas.
1. **It's unclear where the `replicas: 3` value came from** when looking at the live system.
2. **It's unclear if or where `replicaCount` is used** when looking at each value file.
3. **It's unclear where we need to set a value of `5` to fix the problem.** Common
values would probably affect too many deployments. Prod-us values would likely
be too specific, missing other prod environments.
4. **Complexity accumulates over time.** Each time we make a change we're invited
to add a new level of overrides. If we're adding a value that's not broad
enough at one level, but too broad at the next level, then we have no choice but
to add one more level in between.
5. **It's difficult to see the actual values passed to Helm.** We have to implement
the last in first out merge algorithm in our head as we page through each file,
an extremely error prone chore.
For example, `common-values.yaml` has `replicaCount: 1`. Is that value actually
used anywhere? The only way to know is to walk through all permutations and
determine if it's overridden or not. Spoiler, `common-values.yaml` is _always_
overridden in the original ApplicationSet we migrated. It serves no purpose,
yet it lays a hidden trap for us to trip over in the future. If we remove a
value overriding common-values.yaml then we don't know if the freshly uncovered
value had an important purpose relevant now or if it no longer serves a purpose.
This particular form of override hierarchy is similar to inheritance, suffering
from the same problems. The central concept of unification in CUE elegantly
solves these problems.
> Like with other configuration languages, CUE can add complexity if values are organized to come from multiple places. However, as CUE disallows overrides, deep layerings are naturally prevented. More importantly, CUE can also enhance readability. A definition in one file may apply to values in many other files. Where one would usually have to open all these files to verify validity; with CUE one can see it at a glance.
Attribution: [Simplicity at Scale](https://cuelang.org/docs/concept/configuration-use-case/#simplicity-at-scale)
> Inheritance is not commutative and idempotent in the general case. In other words, order matters. This makes it hard to track where values are coming from. This is not only true for humans, but also machines. It makes it very complicated, if not impossible, to do any kind of automation.
>
> The basic operation of CUE is commutative, associative and idempotent. This order independence helps both humans and machines. The resulting model is much less complex.
Attribution: [Inheritance-based configuration languages](https://cuelang.org/docs/concept/configuration-use-case/#inheritance-based-configuration-languages)
In CUE, **order is irrelevant**. This property greatly simplifies configuration
as we'll see by unifying these value files.
### What's the desired outcome?
For this refactoring, we'd like to achieve the following goals:
1. Prune unused values.
2. Reduce the five layers of abstraction down to one.
3. Gain insight into the values passed into Helm.
4. Fail fast if a future change creates a conflict.
5. Fail fast if a necessary value is not provided.
6. Prevent complex layers of abstraction from accumulating.
7. Add type checks, schema validation, and constraints to Helm values.
### Starting Context
If you didn't work through [Part 1] the final results are available in the
[end-of-part-1] branch for your reference.
### Pruning Unused Values
We need a way to identify which values are used so we can prune the unused ones.
Holos simplifies this task by rendering all of the manifests to the local
filesystem. We can simply grep the deploy directory to see what values are
actually used.
import GrepReplicasCmd from '!!raw-loader!./_migrate_appset/script-06-unification/grep.sh';
import GrepReplicasOut from '!!raw-loader!./_migrate_appset/script-06-unification/grep.txt';
<Tabs groupId="grep-replicas">
<TabItem value="Command" label="Command">
<CodeBlock language="bash">{GrepReplicasCmd}</CodeBlock>
</TabItem>
<TabItem value="Output" label="Output">
<CodeBlock language="txt">{GrepReplicasOut}</CodeBlock>
</TabItem>
</Tabs>
The `grep` output indicates **3, 8 and 10** are the only values used, yet
`common-values.yaml` configures a value of 1.
import CommonPath from '!!raw-loader!./_migrate_appset/script-06-unification/common.path';
import CommonYAML from '!!raw-loader!./_migrate_appset/script-06-unification/common.yaml';
<CodeBlock language="txt">{CommonPath}</CodeBlock>
<CodeBlock language="yaml" showLineNumbers>{CommonYAML}</CodeBlock>
The `common-values.yaml` file is therefore useless. Let's remove it.
import RemoveHead from '!!raw-loader!./_migrate_appset/script-06-unification/remove.head';
import RemoveBody from '!!raw-loader!./_migrate_appset/script-06-unification/remove.body';
import RemoveTail from '!!raw-loader!./_migrate_appset/script-06-unification/remove.tail';
import RemoveOut from '!!raw-loader!./_migrate_appset/script-06-unification/remove.out';
<Tabs groupId="remove-common-values">
<TabItem value="Command" label="Command">
<CodeBlock language="bash">{RemoveHead}</CodeBlock>
<CodeBlock language="diff" showLineNumbers>{RemoveBody}</CodeBlock>
<CodeBlock language="bash">{RemoveTail}</CodeBlock>
</TabItem>
<TabItem value="Output" label="Output">
<CodeBlock language="txt">{RemoveOut}</CodeBlock>
</TabItem>
</Tabs>
We double check by rendering the platform. There should be no changes to the
deploy directory even though we removed `common-values.yaml` from the Helm
hierarchy.
import RenderCmd from '!!raw-loader!./_migrate_appset/script-06-unification/render.sh';
import RenderOut from '!!raw-loader!./_migrate_appset/script-06-unification/render.txt';
<Tabs groupId="render-command">
<TabItem value="Command" label="Command">
<CodeBlock language="bash">{RenderCmd}</CodeBlock>
</TabItem>
<TabItem value="Output" label="Output">
<CodeBlock language="txt">{RenderOut}</CodeBlock>
</TabItem>
</Tabs>
import CheckCmd from '!!raw-loader!./_migrate_appset/script-06-unification/check.sh';
import CheckOut from '!!raw-loader!./_migrate_appset/script-06-unification/check.txt';
<Tabs groupId="git-status">
<TabItem value="Command" label="Command">
<CodeBlock language="bash">{CheckCmd}</CodeBlock>
</TabItem>
<TabItem value="Output" label="Output">
<CodeBlock language="txt">{CheckOut}</CodeBlock>
</TabItem>
</Tabs>
:::tip
Holos makes it easier to confidently remove values which are never used.
:::
If we didn't have the rendered manifests in hand this would be a much more
difficult task.
### Eliminating Complexity
Let's unify all the levels of the value hierarchy into one layer and see where
the conflicts are. This is straight forward with `holos`.
:::important
One unified layer is significantly less complicated than a hierarchy of
overrides.
:::
import UnifyHead from '!!raw-loader!./_migrate_appset/script-06-unification/unify.head';
import UnifyBody from '!!raw-loader!./_migrate_appset/script-06-unification/unify.body';
import UnifyTail from '!!raw-loader!./_migrate_appset/script-06-unification/unify.tail';
import UnifyOut from '!!raw-loader!./_migrate_appset/script-06-unification/unify.out';
<Tabs groupId="unify-values">
<TabItem value="Command" label="Command">
<CodeBlock language="bash">{UnifyHead}</CodeBlock>
<CodeBlock language="diff" showLineNumbers>{UnifyBody}</CodeBlock>
<CodeBlock language="bash">{UnifyTail}</CodeBlock>
</TabItem>
<TabItem value="Output" label="Output">
<CodeBlock language="txt">{UnifyOut}</CodeBlock>
</TabItem>
</Tabs>
:::important
The 4 ordered list elements convert into one unified struct.
:::
Render all components to see conflicting values. We use concurrency 1 to return
the first error encountered.
import Render2Cmd from '!!raw-loader!./_migrate_appset/script-06-unification/render2.sh';
import Render2Out from '!!raw-loader!./_migrate_appset/script-06-unification/render2.txt';
<Tabs groupId="render2-command">
<TabItem value="Command" label="Command">
<CodeBlock language="bash">{Render2Cmd}</CodeBlock>
</TabItem>
<TabItem value="Output" label="Output">
<CodeBlock language="txt">{Render2Out}</CodeBlock>
</TabItem>
</Tabs>
The `prod-eu` environment has a conflict:
```txt
values.replicaCount: conflicting values 8 and 5
```
The error is detailed, helpfully listing all of the potential locations the
conflict may arise from. We can hone in on the yaml files in the `my-values`
directory:
1. my-values/env-type/prod-values.yaml:2:15
2. my-values/envs/prod-eu-values.yaml:2:15
These files we originally migrated without modifying them are:
import ConflictA from '!!raw-loader!./_migrate_appset/script-06-unification/conflict/prod-values.yaml';
import ConflictB from '!!raw-loader!./_migrate_appset/script-06-unification/conflict/prod-eu-values.yaml';
<Tabs groupId="conflict-files">
<TabItem value="prod-values.yaml" label="prod-values.yaml">
<CodeBlock language="yaml" showLineNumbers>{ConflictA}</CodeBlock>
</TabItem>
<TabItem value="prod-eu-values.yaml" label="prod-eu-values.yaml">
<CodeBlock language="yaml" showLineNumbers>{ConflictB}</CodeBlock>
</TabItem>
</Tabs>
:::important
The conflict arises from the original article specifying a value for the same
field along two different aspects of our configuration: specific environments
(envs) and broad types of environments (env-types).
:::
### Resolving Conflicts
In Holos and CUE we resolve these conflicts by picking one aspect to specify a
given field value. We'll pick specific environments in this case because it's
not too broad and not too specific.
We previously determined **3, 8 and 10** are the only values actually used in
the final configurations, so we already know `replicaCount: 5` in the
`my-values/env-type/prod-values.yaml` file is useless.
Let's remove it.
import Unify2Head from '!!raw-loader!./_migrate_appset/script-06-unification/unify2.head';
import Unify2Body from '!!raw-loader!./_migrate_appset/script-06-unification/unify2.body';
import Unify2Tail from '!!raw-loader!./_migrate_appset/script-06-unification/unify2.tail';
import Unify2Out from '!!raw-loader!./_migrate_appset/script-06-unification/unify2.out';
<Tabs groupId="unify2-values">
<TabItem value="Command" label="Command">
<CodeBlock language="bash">{Unify2Head}</CodeBlock>
<CodeBlock language="diff" showLineNumbers>{Unify2Body}</CodeBlock>
<CodeBlock language="bash">{Unify2Tail}</CodeBlock>
</TabItem>
<TabItem value="Output" label="Output">
<CodeBlock language="txt">{Unify2Out}</CodeBlock>
</TabItem>
</Tabs>
Now we can render the platform again and verify there are no changes to the
rendered manifests in the `deploy` directory.
import Render3Cmd from '!!raw-loader!./_migrate_appset/script-06-unification/render3.sh';
import Render3Out from '!!raw-loader!./_migrate_appset/script-06-unification/render3.txt';
<Tabs groupId="render3-command">
<TabItem value="Command" label="Command">
<CodeBlock language="bash">{Render3Cmd}</CodeBlock>
</TabItem>
<TabItem value="Output" label="Output">
<CodeBlock language="txt">{Render3Out}</CodeBlock>
</TabItem>
</Tabs>
import Status3Cmd from '!!raw-loader!./_migrate_appset/script-06-unification/status3.sh';
import Status3Out from '!!raw-loader!./_migrate_appset/script-06-unification/status3.txt';
<Tabs groupId="status3-command">
<TabItem value="Command" label="Command">
<CodeBlock language="bash">{Status3Cmd}</CodeBlock>
</TabItem>
<TabItem value="Output" label="Output">
<CodeBlock language="txt">{Status3Out}</CodeBlock>
</TabItem>
</Tabs>
Voila! We've successfully refactored all of the value files for Helm into a
single unified data structure. We know we're done because unification succeeds
and there are no changes to the deploy directory.
## Simplifying a Complex Hierarchy
We've removed the complexity of the Go template in the ApplicationSet, and 5
more layers of complexity in the Helm value files, but the original data didn't
have many overrides. It was the best case scenario, flattening the hierarchy by
trial and error was straight forward.
What about a complex hierarchy with lots of overrides? This same trial and
error approach would take too long. Holos offers a solution in this scenario.
In this section we'll use `holos` to create a flattened value file for each of
the config.json deployment configs without using trial and error.
The layout of the deployment configs is a key aspect of this method. The
deployment config inputs always map 1:1 to the rendered manifests and associated
Application resource. Therefore, any intermediate layers of overrides are not
strictly necessary, an opportunity to reduce complexity significantly.
In this example the deployment configs are organized by customer and cluster, so
we'll work to flatten the unnecessary levels of the hierarchy while also
organizing the output around customers and clusters to get everything lined up
nicely.
Using the same deployment configs at each step, we'll:
1. Render my-app using the helm values hierarchy to get a reference point.
2. Render a special chart, render-values, to flatten the value files hierarchy into one file.
3. Render my-app using the flattened values file.
4. Verify there are no changes to the deploy artifacts.
We'll be able to quickly locate and read the configuration given a customer and
a cluster.
### What's the worst case scenario?
In the worst case scenario there is little rhyme or reason to how values are set
and overridden in a hierarchy. We'll start with this worst case scenario,
spraying data across clusters and override layers. This starting point looks
like the following, with the highest precedence values at the top of the table.
| Num | Layer | Description |
| - | - | - |
| 1 | Customer | customer-zzsbbmfc, customer-xxxxxxx, ... |
| 2 | Namespace | prod-myapp, dev-myapp, ... |
| 3 | Application | myapp, yourapp, ... |
| 4 | Cluster | prod1-customer, dev2-internal, uat3-management, ... |
| 5 | Environment | prod, uat, test, dev |
| 6 | Tier | prod, nonprod |
| 7 | Scope | customer, internal, management |
| 8 | Zone | us-east1-a, us-east1-b, ... |
| 9 | Region | us-east1, us-west1, ... |
| 10 | Location | us, eu, ap, ... |
| 11 | Common | Base layer |
Across these layers the following fields may be set:
| Field | Type | Description |
| - | - | - |
| enabled | bool | Feature flag |
| image | string | Container image URI, e.g. "oci://example.com/myservice" |
| version | string | Version string, e.g. "v0.1.0" |
| domain | string | DNS domain, e.g. "example.com" |
| replicas | int | number of replicas |
| clientID | string | OAuth 2.0 client ID |
| issuer | string | OIDC issuer uri, e.g. `https://login.example.com` |
| projectID | string | cloud project id, e.g. "my-project-123456" |
| accountID | int | cloud account id, e.g. 012345678901 |
| arn | string | resource name, e.g. "arn:partition:service:region:account-id:resource-type:resource-id" |
| cores | float | cpu cores, e.g. 2.0 |
| memory | int | memory in MiB, e.g. 2048 |
| labels | map[string]string | resource labels |
### Generating complexity
Generate the deployment configs and the value files. The RNG is seeded with
constant values so the output is deterministic. If you'd like truly random data
set `RANDOMIZE=1` in the environment before running this command..
import GeneratorCmd from '!!raw-loader!./_migrate_appset/script-08-complex-unification/generator.sh';
<CodeBlock language="bash">{GeneratorCmd}</CodeBlock>
{/* Show the generated structure */}
Deployment configs are generated in the following directory. The `config`
directory contains concrete values by convention, organized in packages.
import ShowDeploymentConfigsCmd from '!!raw-loader!./_migrate_appset/script-08-complex-unification/show-deployment-configs.sh';
import ShowDeploymentConfigsOut from '!!raw-loader!./_migrate_appset/script-08-complex-unification/show-deployment-configs.txt';
<CodeBlock language="bash">{ShowDeploymentConfigsCmd}</CodeBlock>
<CodeBlock language="txt">{ShowDeploymentConfigsOut}</CodeBlock>
Each of these {ShowDeploymentConfigsOut} deployment config files map 1:1 to a
`holos` `BuildPlan` and an ArgoCD `Application`.
The Helm value files hierarchy files are written into the `my-app` component
directory. By convention values reside in the component directory so they're
closely associated with the helm chart using them.
import ShowValueFilesCmd from '!!raw-loader!./_migrate_appset/script-08-complex-unification/show-value-files.sh';
import ShowValueFilesOut from '!!raw-loader!./_migrate_appset/script-08-complex-unification/show-value-files.txt';
<Tabs groupId="show-value-files">
<TabItem value="Command" label="Command">
<CodeBlock language="bash">{ShowValueFilesCmd}</CodeBlock>
</TabItem>
<TabItem value="Output" label="Output">
<CodeBlock language="txt">{ShowValueFilesOut}</CodeBlock>
</TabItem>
</Tabs>
### Flattening Step 1
We'll use CUE build tags to render `my-app` instead of `my-chart` for this
example. Build tags alow us to exclude files used in the previous example, like
`platform/my-chart.cue`, and include files for this example like
`platform/my-app.cue`.
This example is about flattening the helm values file, so we use `-t flatten`.
There are three steps to flatten the hierarchy, so we'll use `-t stepX` for each
step along the way.
Finally, we'll use selectors to focus on one customer specifically. It takes a
few minutes (about 4 on my machine) to render all {ShowDeploymentConfigsOut}
deployments for all customers, so we can develop and test the migration process
quickly with one customer, then roll the change out to all customers once we're
ready.
Let's take a look at one `BuildPlan` for one customer. Take note of the
valueFiles hierarchy in the output. Our goal is to reduce complexity by
collapsing this down to one layer with at most one override of a default value.
First, set the customer as a variable:
```bash
export CUSTOMER="customer-zzsbbmfc"
```
import ShowOneBuildPlanCmd from '!!raw-loader!./_migrate_appset/script-08-complex-unification/show-one-buildplan.sh';
import ShowOneBuildPlanOut from '!!raw-loader!./_migrate_appset/script-08-complex-unification/show-one-buildplan.txt';
<Tabs groupId="show-one-buildplan">
<TabItem value="Command" label="Command">
<CodeBlock language="bash">{ShowOneBuildPlanCmd}</CodeBlock>
</TabItem>
<TabItem value="Output" label="Output">
<CodeBlock language="yaml" showLineNumbers>{ShowOneBuildPlanOut}</CodeBlock>
</TabItem>
</Tabs>
The other `BuildPlan` specifications are similar. Values are randomly
distributed through the hierarchy making it a challenge to comprehend which
value is used in the final configuration.
Render the final manifests for this customer so we have a reference point for
the rest of the migration.
import RenderOneCustomerCmd from '!!raw-loader!./_migrate_appset/script-08-complex-unification/render-one-customer.sh';
import RenderOneCustomerOut from '!!raw-loader!./_migrate_appset/script-08-complex-unification/render-one-customer.txt';
<Tabs groupId="render-one-customer">
<TabItem value="Command" label="Command">
<CodeBlock language="bash">{RenderOneCustomerCmd}</CodeBlock>
</TabItem>
<TabItem value="Output" label="Output">
<CodeBlock language="txt">{RenderOneCustomerOut}</CodeBlock>
</TabItem>
</Tabs>
Add and commit these changes so we can easily see any future changes. For the
rest of the migration there should be no changes to the final configuration.
import AddCommitStep1Cmd from '!!raw-loader!./_migrate_appset/script-08-complex-unification/add-and-commit-step1.sh';
import AddCommitStep1Out from '!!raw-loader!./_migrate_appset/script-08-complex-unification/add-and-commit-step1.txt';
<Tabs groupId="add-commit-step1">
<TabItem value="Command" label="Command">
<CodeBlock language="bash">{AddCommitStep1Cmd}</CodeBlock>
</TabItem>
<TabItem value="Output" label="Output">
<CodeBlock language="txt">{AddCommitStep1Out}</CodeBlock>
</TabItem>
</Tabs>
### Flattening Step 2
Now that we have a reference point we can use `holos` to flatten the hierarchy.
We'll pass the same value files to a special `render-values` chart that simply
echos back the final merged values. Holos writes the value files to
`deploy/values` organized by customer and cluster to match the organization of
the source deployment configs, the `config.json` files.
import RenderStep2Cmd from '!!raw-loader!./_migrate_appset/script-08-complex-unification/render-step2.sh';
import RenderStep2Out from '!!raw-loader!./_migrate_appset/script-08-complex-unification/render-step2.txt';
<Tabs groupId="render-step2">
<TabItem value="Command" label="Command">
<CodeBlock language="bash">{RenderStep2Cmd}</CodeBlock>
</TabItem>
<TabItem value="Output" label="Output">
<CodeBlock language="txt">{RenderStep2Out}</CodeBlock>
</TabItem>
</Tabs>
This command produces flattened `values.yaml` files for each one of the input
deployment configs.
import ShowFlattenedValuesCmd from '!!raw-loader!./_migrate_appset/script-08-complex-unification/show-flattened-values.sh';
import ShowFlattenedValuesOut from '!!raw-loader!./_migrate_appset/script-08-complex-unification/show-flattened-values.txt';
<Tabs groupId="show-flattened-values">
<TabItem value="Command" label="Command">
<CodeBlock language="bash">{ShowFlattenedValuesCmd}</CodeBlock>
</TabItem>
<TabItem value="Output" label="Output">
<CodeBlock language="txt">{ShowFlattenedValuesOut}</CodeBlock>
</TabItem>
</Tabs>
Now we can proceed to change the `my-app` Holos component to use these flattened
values in place of the value files hierarchy.
### Flattening Step 3
The final step of the migration is to use the flattened values in place of the
complex hierarchy. In the component definition we replace the use of
`valueFiles` with one `values` struct.
Normally we'd move the `deploy/values` directory into `components/my-app`, but
for the article we use a symlink instead: `components/my-app/flattened-values ->
../../deploy/values`
Here's how the final `BuildPlan` looks. Note how the values are flattened and
the complexity of the override layers are gone.
import Step3ShowOneBuildPlanCmd from '!!raw-loader!./_migrate_appset/script-09-step3/show-one-buildplan.sh';
import Step3ShowOneBuildPlanOut from '!!raw-loader!./_migrate_appset/script-09-step3/show-one-buildplan.txt';
<Tabs groupId="step3-show-one-buildplan">
<TabItem value="Command" label="Command">
<CodeBlock language="bash">{Step3ShowOneBuildPlanCmd}</CodeBlock>
</TabItem>
<TabItem value="Output" label="Output">
<CodeBlock language="yaml" showLineNumbers>{Step3ShowOneBuildPlanOut}</CodeBlock>
</TabItem>
<TabItem value="Step 1" label="Step 1 Comparison">
This is the previous BuildPlan from Step 1 for comparison.
<CodeBlock language="yaml" showLineNumbers>{ShowOneBuildPlanOut}</CodeBlock>
</TabItem>
</Tabs>
Render the platform to ensure we're producing the same output as we did when we
started in step 1.
import RenderStep3Cmd from '!!raw-loader!./_migrate_appset/script-09-step3/render-one-customer.sh';
import RenderStep3Out from '!!raw-loader!./_migrate_appset/script-09-step3/render-one-customer.txt';
<Tabs groupId="render-step3">
<TabItem value="Command" label="Command">
<CodeBlock language="bash">{RenderStep3Cmd}</CodeBlock>
</TabItem>
<TabItem value="Output" label="Output">
<CodeBlock language="txt">{RenderStep3Out}</CodeBlock>
</TabItem>
</Tabs>
The manifests rendered with the flattened values are identical to those rendered
with the more complex hierarchy of overrides.
import GitStatusStep3Cmd from '!!raw-loader!./_migrate_appset/script-09-step3/git-status.sh';
import GitStatusStep3Out from '!!raw-loader!./_migrate_appset/script-09-step3/git-status.txt';
<Tabs groupId="git-status-step3">
<TabItem value="Command" label="Command">
<CodeBlock language="bash">{GitStatusStep3Cmd}</CodeBlock>
</TabItem>
<TabItem value="Output" label="Output">
<CodeBlock language="txt">{GitStatusStep3Out}</CodeBlock>
</TabItem>
</Tabs>
### Going deeper
If you'd like to dive deeper into these steps, take a look at the following
files which are the entrypoints for the `holos render platform` command at each
step. From there, take a look at the component definitions in
`components/my-app/*.cue` and `components/render-values/*.cue`
#### Entrypoints
import S2PlatformMyApp from '!!raw-loader!./_migrate_appset/script-08-complex-unification/files/platform-my-app.cue';
import S2PlatformRenderValues from '!!raw-loader!./_migrate_appset/script-08-complex-unification/files/platform-render-values.cue';
<Tabs groupId="review">
<TabItem value="Step 1" label="Step 1">
```
platform/my-app.cue
```
<CodeBlock language="cue" showLineNumbers>{S2PlatformMyApp}</CodeBlock>
</TabItem>
<TabItem value="Step 2" label="Step 2">
```
platform/render-values.cue
```
<CodeBlock language="cue" showLineNumbers>{S2PlatformRenderValues}</CodeBlock>
</TabItem>
<TabItem value="Step 3" label="Step 3">
:::note
Step 3 uses the same entrypoint as Step 1, completing the migration. The
main difference is in `components/my-app/step3.cue` where the flattened
values are used.
:::
</TabItem>
</Tabs>
#### Component Definition
The `my-app` component definition starts off in step 1, is not used in step 2,
and changes to a flattened values structure in step3. Here are the changes at
each step.
import ComponentDefMyAppXMain from '!!raw-loader!./_migrate_appset/script-09-step3/files/my-app.cue';
import ComponentDefMyAppStep1 from '!!raw-loader!./_migrate_appset/script-09-step3/files/step1.cue';
import ComponentDefMyAppStep3 from '!!raw-loader!./_migrate_appset/script-09-step3/files/step3.cue';
<Tabs groupId="review-component-definition">
<TabItem value="Component Definition" label="Component Definition">
```
components/my-app/my-app.cue
```
<CodeBlock language="cue" showLineNumbers>{ComponentDefMyAppXMain}</CodeBlock>
</TabItem>
<TabItem value="Step 1" label="Step 1">
```
components/my-app/step1.cue
```
<CodeBlock language="cue" showLineNumbers>{ComponentDefMyAppStep1}</CodeBlock>
</TabItem>
<TabItem value="Step 3" label="Step 3">
```
components/my-app/step3.cue
```
<CodeBlock language="cue" showLineNumbers>{ComponentDefMyAppStep3}</CodeBlock>
</TabItem>
</Tabs>
## Concluding Remarks
The original article used 5 layers of Helm value files. Migrating the
configuration to Holos uncovered issues and solved problems.
1. `replicaCount: 1` in common-values.yaml was unnecessary, it was always overridden.
2. `replicaCount: 5` in prod-values.yaml was _also_ unnecessary, it was never used.
3. Removing these two values allowed unification to succeed, which indicates
there was no need for a Helm value hierarchy at all.
The Helm value hierarchy added significant accidental complexity, accidental
because it was not necessary and complex because the layers of override made it
difficult to reason about the system.
Migrating to Holos achieves the same result, the deploy directory remains
unchanged, while leaving us with a much simpler solution.
1. **Complexity remains constant over time.** New aspects can be added, for
example specifying values by customer without introducing a new layer of
overrides. With the previous approach of Helm value overrides _each additional
layer accidentally increased complexity_.
2. **Conflicts are clear and immediate** indicating the exact files and line
numbers where we need to resolve the conflict.
3. **Data is within our reach** we no longer need to rely on ArgoCD running in a
remote cluster to see the desired state. We can inspect the intermediate state
with `holos show buildplans` and the final manifests with `holos render
platform`. We can use simple tools like `grep` to make well informed decisions
about the platform wide configuration.
For example, here's the `holos` `BuildPlan` for prod-us where we see the actual
unified values provided to `helm`.
import ShowBuildPlansCmd from '!!raw-loader!./_migrate_appset/script-06-unification/buildplan.sh';
import ShowBuildPlansOut from '!!raw-loader!./_migrate_appset/script-06-unification/buildplan.txt';
<Tabs groupId="show-buildplans-command">
<TabItem value="Command" label="Command">
<CodeBlock language="bash">{ShowBuildPlansCmd}</CodeBlock>
</TabItem>
<TabItem value="Output" label="Output">
<CodeBlock language="yaml" showLineNumbers>{ShowBuildPlansOut}</CodeBlock>
</TabItem>
</Tabs>
[Part 1]: ./2025-01-13-replace-an-applicationset-with-the-rendered-manifest-pattern.mdx
[original ApplicationSet]: https://github.com/holos-run/multi-sources-example/blob/v0.1.0/appsets/4-final/all-my-envs-appset-with-version.yaml#L27-L32
[end-of-part-1]: https://github.com/holos-run/multi-sources-example/tree/end-of-part-1

View File

@@ -0,0 +1,37 @@
# This doc tested with holos version...
exec bash -c 'bash -euo pipefail $WORK/version.sh 2>&1'
cmp stdout version.txt
# Remove the directory if it already exists
exec rm -rf multi-sources-example
# Clone the repository
exec bash -c 'bash -euo pipefail $WORK/clone.sh 2>&1'
cmp stdout clone.txt
# Get the git commit
cd multi-sources-example
exec git rev-parse --verify origin/HEAD
cp stdout $WORK/git.commit
# Reset to TFA 4-final recommendation
exec bash -c 'bash -euo pipefail $WORK/reset.sh 2>&1'
cmp stdout $WORK/reset.txt
# Set the committer
exec git config user.email go-test@example.com
exec git config user.name 'go test'
-- version.sh --
holos --version
-- version.txt --
0.103.0
-- clone.sh --
git clone https://github.com/holos-run/multi-sources-example.git
cd multi-sources-example
-- clone.txt --
Cloning into 'multi-sources-example'...
-- reset.sh --
git branch -f work start
git checkout work
-- reset.txt --
Switched to branch 'work'

View File

@@ -0,0 +1,25 @@
# Work in the root of the example repo
cd ../script-01-clone/multi-sources-example
# Reset to where 01-clone left us.
exec git clean -fdx
exec git reset --hard start
# Consider the ApplicationSet from the final recommendation...
exec bash -c 'cat $(<$WORK/appset.path)'
cp stdout $WORK/appset.yaml
# The Deployment
exec bash -c 'cat $(<$WORK/deployment.path)'
cp stdout $WORK/deployment.yaml
# The Service
exec bash -c 'cat $(<$WORK/service.path)'
cp stdout $WORK/service.yaml
-- appset.path --
appsets/4-final/all-my-envs-appset-with-version.yaml
-- service.path --
my-chart/templates/service.yaml
-- deployment.path --
my-chart/templates/deployment.yaml

View File

@@ -0,0 +1,83 @@
# Work in the root of the example repo
cd ../script-01-clone/multi-sources-example
# 7ce4feb initialize the platform
exec bash -c 'bash -euo pipefail $WORK/holos-init.sh 2>&1'
# f7102d6 reorganize to conventional holos layout
exec bash -c 'bash -euo pipefail $WORK/move-files-around.sh 2>&1'
# 49183ca git mv appsets/4-final/env-config config/environments
# Note, the v0.3.0 tag contains the environments.cue prior to being updated to
# fix the duplicated config.json file. Commit 1a73e77b fixes this issue as a
# later migration step.
exec cat $WORK/environments-header.sh $WORK/environments.cue $WORK/environments-trailer.sh
stdin stdout
exec bash -xeuo pipefail
# Inspect the structure
exec bash -c 'bash -euo pipefail $WORK/inspect-environments.sh 2>&1'
cp stdout $WORK/inspect-environments.txt
-- holos-init.sh --
holos init platform v1alpha5 --force
-- move-files-around.sh --
# First, we'll move my-chart the original article vendored in
# Git to the conventional location Holos uses to vendor charts.
mkdir -p components/my-chart/vendor/0.1.0
git mv my-chart components/my-chart/vendor/0.1.0/my-chart
# Helm value files move into the directory that will contain
# my-chart component definition. components/my-chart is
# conventionally called the "my-chart component directory"
git mv my-values components/my-chart/my-values
# The config.json files are moved without changing their folder structure.
# We'll package the data up into an "environments config package" for reuse.
mkdir config
git mv appsets/4-final/env-config config/environments
# All of our components will reside in the components directory so
# the CUE files `holos init` produced may be moved to keep the
# repository root tidy.
mv *.cue components/
# The following files and directories from the original article and
# holos init are no longer relevant after the migration.
mkdir not-used
git mv appsets not-used/appsets
git mv example-apps not-used/example-apps
rm -f platform.metadata.json
# Make the commit
git add platform components cue.mod .gitignore
git commit -m 'reorganize to conventional holos layout'
-- environments-header.sh --
cat <<'EOF' > config/environments/environments.cue
-- environments.cue --
@extern(embed)
package environments
// We use cue embed functionality as an equivalent replacement for
// ApplicationSet generators.
config: _ @embed(glob=*/config.json)
config: _ @embed(glob=staging/*/config.json)
config: _ @embed(glob=prod/*/config.json)
config: _ @embed(glob=integration/*/config.json)
// With CUE we can constrain the data with a schema.
config: [FILEPATH=string]: #Config
// #Config defines the schema of each config.json file.
#Config: {
env: "qa" | "integration-gpu" | "integration-non-gpu" | "staging-us" | "staging-eu" | "prod-us" | "prod-eu"
region: "us" | "eu"
type: "prod" | "non-prod"
version: "qa" | "staging" | "prod"
chart: =~"^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
-- environments-trailer.sh --
EOF
-- inspect-environments.sh --
CUE_EXPERIMENT=embed holos cue export --out=yaml \
./config/environments

View File

@@ -0,0 +1,205 @@
# Work in the root of the example repo
cd ../script-01-clone/multi-sources-example
env HOME=$WORK/.tmp
chmod 0755 $WORK/update.sh
# 987df87 add platform components to replace ApplicationSets.spec.generators
exec cat $WORK/platform-my-chart-header.sh $WORK/platform-my-chart.cue $WORK/platform-my-chart-trailer.sh
stdin stdout
exec bash -xeuo pipefail
exec diff platform/my-chart.cue $WORK/platform-my-chart.cue
# Configure where manifests are written.
exec cat $WORK/componentconfig-header.sh $WORK/componentconfig.cue $WORK/componentconfig-trailer.sh
stdin stdout
exec bash -xeuo pipefail
exec diff components/componentconfig.cue $WORK/componentconfig.cue
# Show the platform
exec bash -c 'bash -euo pipefail $WORK/holos-show-platform.sh 2>&1'
cp stdout $WORK/holos-show-platform.txt
# Component Definition
exec cat $WORK/component-my-chart-header.sh $WORK/component-my-chart.cue $WORK/component-my-chart-trailer.sh
stdin stdout
exec bash -xeuo pipefail
exec diff components/my-chart/my-chart.cue $WORK/component-my-chart.cue
# Show the BuildPlans
exec bash -c 'bash -euo pipefail $WORK/show-buildplans.sh 2>&1'
cp stdout $WORK/show-buildplans.txt
# Inspect the values
exec bash -c 'bash -euo pipefail $WORK/inspect-value-files.sh 2>&1'
cp stdout $WORK/inspect-value-files.txt
# Render the platform, capture stdout, and use update.sh to gate whether the
# output file should be updated.
exec bash -c 'bash -euo pipefail $WORK/render.sh 2>&1'
stdin stdout
exec $WORK/update.sh $WORK/render.txt
exec bash -c 'bash -euo pipefail $WORK/tree-deploy.sh 2>&1'
cp stdout $WORK/tree-deploy.txt
# Make a commit
exec git add .
exec git commit -m '04-helm-component.txtar'
-- tree-deploy.sh --
tree deploy
-- render.sh --
holos render platform
-- update.sh --
#! /bin/bash
set -euo pipefail
[[ -s "$1" ]] && [[ -z "${HOLOS_UPDATE_SCRIPTS:-}" ]] && exit 0
cat > "$1"
-- show-buildplans.sh --
holos show buildplans
-- inspect-value-files.sh --
CUE_EXPERIMENT=embed holos cue export --out=yaml \
./components/my-chart \
-e valueFiles
-- component-my-chart-header.sh --
cat <<'EOF' > components/my-chart/my-chart.cue
-- component-my-chart-trailer.sh --
EOF
-- component-my-chart.cue --
@extern(embed)
package holos
import "holos.example/config/environments"
parameters: {
environments.#Config & {
env: _ @tag(env)
region: _ @tag(region)
type: _ @tag(type)
version: _ @tag(version)
chart: _ @tag(chart)
}
}
// component represents the holos component definition, which produces a
// BuildPlan for holos to execute, rendering the manifests.
component: #Helm & {
Chart: {
// Migrated from https://github.com/holos-run/multi-sources-example/blob/v0.1.0/appsets/4-final/all-my-envs-appset-with-version.yaml#L25
version: parameters.chart
repository: {
name: "multi-sources-example"
// Migrated from https://github.com/holos-run/multi-sources-example/blob/v0.1.0/appsets/4-final/all-my-envs-appset-with-version.yaml#L23
url: "https://kostis-codefresh.github.io/multi-sources-example"
}
}
// Migrated from https://github.com/holos-run/multi-sources-example/blob/v0.1.0/appsets/4-final/all-my-envs-appset-with-version.yaml#L40
// We use kustomize to manage the namespace, similar to how the original
// article uses the ApplicationSet template to specify the namespace.
KustomizeConfig: Kustomization: namespace: parameters.env
// Migrate the Helm Hierarchy preserving the behavior of over writing values.
// Migrated from [valueFiles]. Later files win.
//
// [valueFiles]: https://github.com/holos-run/multi-sources-example/blob/v0.1.0/appsets/4-final/all-my-envs-appset-with-version.yaml#L27-L32
ValueFiles: [{
name: "common-values.yaml"
values: valueFiles["my-values/common-values.yaml"]
}, {
name: "version-values.yaml"
values: valueFiles["my-values/app-version/\(parameters.version)-values.yaml"]
}, {
name: "type-values.yaml"
values: valueFiles["my-values/env-type/\(parameters.type)-values.yaml"]
}, {
name: "region-values.yaml"
values: valueFiles["my-values/regions/\(parameters.region)-values.yaml"]
}, {
name: "env-values.yaml"
values: valueFiles["my-values/envs/\(parameters.env)-values.yaml"]
}]
}
// holos represents the output for the holos command line to process. The holos
// command line processes a BuildPlan to render the helm chart component.
//
// Use the holos show buildplans command to see the BuildPlans that holos render
// platform renders.
holos: component.BuildPlan
// Migrated from https://github.com/holos-run/multi-sources-example/blob/v0.1.0/appsets/4-final/all-my-envs-appset-with-version.yaml#L27-L32
valueFiles: _ @embed(glob=my-values/*.yaml)
valueFiles: _ @embed(glob=my-values/*/*-values.yaml)
-- holos-show-platform.sh --
holos show platform
-- componentconfig-header.sh --
cat <<'EOF' > components/componentconfig.cue
-- componentconfig-trailer.sh --
EOF
-- componentconfig.cue --
package holos
#ComponentConfig: {
// Inject the output base directory from platform component parameters.
OutputBaseDir: string | *"" @tag(outputBaseDir, type=string)
}
-- platform-my-chart-header.sh --
cat <<'EOF' > platform/my-chart.cue
-- platform-my-chart-trailer.sh --
EOF
-- platform-my-chart.cue --
package holos
// Imports ./config/environments/*.cue as the environments cue package. The
// package exposes ./config/environments/**/config.json files via the
// environments.config struct
import "holos.example/config/environments"
// Manage my-chart for each of the three environments. Platform components are
// rendered by the holos render platform command.
//
// Use the following command command to inspect the Platform spec holos render
// platform processes.
//
// holos show platform
//
// CONFIG represents each migrated environments/**/config.json file.
for CONFIG in environments.config {
// Add one holos component for each config.json file to the
// Platform.spec.components list.
Platform: Components: "\(CONFIG.env)-my-chart": #MyChart & {
parameters: {
env: CONFIG.env
region: CONFIG.region
type: CONFIG.type
version: CONFIG.version
chart: CONFIG.chart
}
}
}
// #MyChart defines a re-usable way to manage my-chart across qa, staging, and
// production.
#MyChart: {
name: "my-chart"
path: "components/my-chart"
// CUE supports constraints, here we constrain environment to one of three
// possible values.
parameters: {
// Embed the config.json schema (env, region, type, version, chart fields)
environments.#Config
// Define the env field here as any value (_) so we can refer to it.
// Otherwise cue complains #MyChart.parameters.outputBaseDir: reference
// "env" not found
env: _
// Write output manifests organized by environment env in this case refers
// to the env field defined by environments.#Config
outputBaseDir: "environments/\(env)"
}
// CUE supports string substitution.
annotations: "app.holos.run/description": "my-chart \(parameters.chart) for environment \(parameters.env)"
// Selector labels
labels: env: parameters.env
}

View File

@@ -0,0 +1,142 @@
# Work in the root of the example repo
cd ../script-01-clone/multi-sources-example
chmod 0755 $WORK/update.sh
# d9125b8 compose argocd application resources into every component
exec cat $WORK/componentconfig-gitops-header.sh $WORK/componentconfig-gitops.cue $WORK/componentconfig-gitops-trailer.sh
stdin stdout
exec bash -xeuo pipefail
exec diff components/componentconfig-gitops.cue $WORK/componentconfig-gitops.cue
# Render the platform, capture stdout, and use update.sh to gate whether the
# output file should be updated.
exec bash -c 'bash -euo pipefail $WORK/render.sh 2>&1'
stdin stdout
exec $WORK/update.sh $WORK/render.txt
exec bash -c 'bash -euo pipefail $WORK/tree-deploy.sh 2>&1'
cp stdout $WORK/tree-deploy.txt
# Show an example application
exec bash -c 'cat $(<$WORK/app.path)'
stdin stdout
exec $WORK/update.sh $WORK/app.yaml
# Show an example manifest
exec bash -c 'cat $(<$WORK/manifest.path)'
stdin stdout
exec $WORK/update.sh $WORK/manifest.yaml
# Make a commit
exec git add .
exec git commit -m '05-application.txtar'
exec git branch end-of-part-1
-- manifest.path --
deploy/environments/prod-us/components/my-chart/my-chart.gen.yaml
-- app.path --
deploy/gitops/prod-us-my-chart-application.gen.yaml
-- tree-deploy.sh --
tree deploy
-- render.sh --
holos render platform
-- update.sh --
#! /bin/bash
set -euo pipefail
[[ -s "$1" ]] && [[ -z "${HOLOS_UPDATE_SCRIPTS:-}" ]] && exit 0
cat > "$1"
-- componentconfig-gitops-header.sh --
cat <<'EOF' > components/componentconfig-gitops.cue
-- componentconfig-gitops-trailer.sh --
EOF
-- componentconfig-gitops.cue --
package holos
import (
"path"
app "argoproj.io/application/v1alpha1"
)
parameters: {
env: string @tag(env)
}
// #ComponentConfig composes configuration into every Holos Component. Here we
// compose an ArgoCD Application resource along side each component to reconcile
// the hydrated manifests with the cluster.
#ComponentConfig: {
Name: _
OutputBaseDir: _
// Application resources are Environment scoped. Note the combination of
// component name and environment must be unique.
_ArgoAppName: "\(parameters.env)-\(Name)"
// Allow other aspects of the platform configuration to refer to
// `Component._ArgoApplication` to get a handle on the Application resource
// and mix additional configuration in.
_ArgoApplication: app.#Application & {
metadata: name: _ArgoAppName
metadata: namespace: "argocd"
metadata: labels: Labels
// Label the Application with the env so we can easily filter in the UI.
metadata: labels: env: parameters.env
spec: {
// Migrated from https://github.com/holos-run/multi-sources-example/blob/v0.1.0/appsets/4-final/all-my-envs-appset-with-version.yaml#L40
destination: server: "https://kubernetes.default.svc"
destination: namespace: parameters.env
project: "default"
// source migrated from sources
// https://github.com/holos-run/multi-sources-example/blob/v0.1.0/appsets/4-final/all-my-envs-appset-with-version.yaml#L22-L35
source: {
path: string | *ResourcesPath
repoURL: "https://github.com/holos-run/multi-sources-example"
targetRevision: string | *"main"
}
// Migrated from https://github.com/holos-run/multi-sources-example/blob/v0.1.0/appsets/4-final/all-my-envs-appset-with-version.yaml#L43-L48
syncPolicy: syncOptions: ["CreateNamespace=true"]
syncPolicy: automated: prune: true
syncPolicy: automated: selfHeal: true
}
}
// We combine all Application resources into the deploy/gitops/ folder
// assuming Application.metadata.name is unique. This makes it easy to apply
// all of the hydrated Application resources in one shot.
let ArtifactPath = path.Join(["gitops", "\(_ArgoApplication.metadata.name)-application.gen.yaml"], path.Unix)
// Alternatively we could write the Applications along side the OutputBaseDir
// let ArtifactPath = path.Join([OutputBaseDir, "gitops", "\(Name)-application.gen.yaml"], path.Unix)
// ResourcesPath represents the configuration path the Application is
// configured to read as a source. This path contains the fully rendered
// manifests produced by Holos and written to the GitOps repo.
//
// For example, to reconcile my-chart.gen.yaml for prod-us:
// let ResourcesPath = "deploy/environments/prod-us/components/my-chart"
let ResourcesPath = path.Join(["deploy", OutputBaseDir, "components", Name], path.Unix)
// Add the argocd Application instance label to GitOps so resources are in sync.
// This is an example of how Holos makes it easy to add common labels to all
// resources regardless of if they come from Helm, CUE, Kustomize, plain YAML
// manifests, etc...
KustomizeConfig: CommonLabels: "argocd.argoproj.io/instance": _ArgoAppName
// Labels for the Application itself. We filter the argocd application
// instance label so ArgoCD doesn't think the Application resource manages
// itself.
let Labels = {
for k, v in KustomizeConfig.CommonLabels {
if k != "argocd.argoproj.io/instance" {
(k): v
}
}
}
Artifacts: "\(Name)-application": {
artifact: ArtifactPath
generators: [{
kind: "Resources"
output: artifact
resources: Application: (_ArgoAppName): _ArgoApplication
}]
}
}

View File

@@ -0,0 +1,198 @@
# Work in the root of the example repo
cd ../script-01-clone/multi-sources-example
chmod 0755 $WORK/update.sh
# Grep for replicas: to see what's used
exec bash -c 'bash -euo pipefail $WORK/grep.sh 2>&1'
stdin stdout
exec $WORK/update.sh $WORK/grep.txt
# Common values
exec bash -c 'cp $(<$WORK/common.path) $WORK/common.yaml'
# Remove common values
exec cat $WORK/remove.head $WORK/remove.body $WORK/remove.tail
stdin stdout
exec bash -xeuo pipefail
cp stdout $WORK/remove.out
# Render platform
exec bash -c 'bash -euo pipefail $WORK/render.sh 2>&1'
stdin stdout
exec $WORK/update.sh $WORK/render.txt
# Check results
exec bash -c 'bash -euo pipefail $WORK/check.sh 2>&1'
stdin stdout
exec $WORK/update.sh $WORK/check.txt
# Commit 1
exec bash -c 'bash -euo pipefail $WORK/commit1.sh 2>&1'
stdin stdout
exec $WORK/update.sh $WORK/commit1.txt
# Unification Section
# Unify Values
exec cat $WORK/unify.head $WORK/unify.body $WORK/unify.tail
stdin stdout
exec bash -xeuo pipefail
cp stdout $WORK/unify.out
# Render the platform to see the conflicts
! exec bash -euo pipefail -c 'bash -euo pipefail $WORK/render2.sh 2>&1 | sed s,$(pwd)/,,g'
stdin stdout
exec $WORK/update.sh $WORK/render2.txt
# Copy the value files to show the conflict
mkdir $WORK/conflict
cp components/my-chart/my-values/env-type/prod-values.yaml $WORK/conflict/prod-values.yaml
cp components/my-chart/my-values/envs/prod-eu-values.yaml $WORK/conflict/prod-eu-values.yaml
# Commit 2
exec bash -c 'bash -euo pipefail $WORK/commit2.sh 2>&1'
stdin stdout
exec $WORK/update.sh $WORK/commit2.txt
# Remove replicaCount: 5 from env-type/prod-values.yaml
exec cat $WORK/unify2.head $WORK/unify2.body $WORK/unify2.tail
stdin stdout
exec bash -xeuo pipefail
cp stdout $WORK/unify2.out
# Render the platform again: no conflicts
exec bash -euo pipefail -c 'bash -euo pipefail $WORK/render3.sh 2>&1'
stdin stdout
exec $WORK/update.sh $WORK/render3.txt
# Verify there are no changes to the deploy directory
exec bash -c 'bash -euo pipefail $WORK/status3.sh 2>&1'
stdin stdout
exec $WORK/update.sh $WORK/status3.txt
# Commit 3
exec bash -c 'bash -euo pipefail $WORK/commit3.sh 2>&1'
stdin stdout
exec $WORK/update.sh $WORK/commit3.txt
# Final wrap up - show build plan
exec bash -c 'bash -euo pipefail $WORK/buildplan.sh 2>&1'
cp stdout $WORK/buildplan.txt
-- buildplan.sh --
holos show buildplans --selector env=prod-us
-- status3.sh --
git status deploy
-- render3.sh --
rm -rf deploy
holos render platform
-- unify2.head --
patch -p1 <<'EOF'
-- unify2.tail --
EOF
-- unify2.body --
diff --git a/components/my-chart/my-values/env-type/prod-values.yaml b/components/my-chart/my-values/env-type/prod-values.yaml
index fef58f4..a3cce4a 100644
--- a/components/my-chart/my-values/env-type/prod-values.yaml
+++ b/components/my-chart/my-values/env-type/prod-values.yaml
@@ -1,11 +1,5 @@
-# Kubernetes settings
-replicaCount: 5
-
# Environment settings
environmentType: production
paypalUrl: "production.paypal.com"
dbUser: "prod_username"
dbPassword: "prod_password"
-
-
-
-- render2.sh --
holos render platform --concurrency 1
-- unify.head --
patch -p1 <<'EOF'
-- unify.tail --
EOF
-- unify.body --
diff --git a/components/my-chart/my-chart.cue b/components/my-chart/my-chart.cue
index cf1dae4..4f01828 100644
--- a/components/my-chart/my-chart.cue
+++ b/components/my-chart/my-chart.cue
@@ -31,23 +31,14 @@ component: #Helm & {
// article uses the ApplicationSet template to specify the namespace.
KustomizeConfig: Kustomization: namespace: parameters.env
- // Migrate the Helm Hierarchy preserving the behavior of over writing values.
- // Migrated from [valueFiles]. Later files win.
+ // Migrate the Helm Hierarchy to CUE. Conflicts are errors.
+ // Migrated from [valueFiles].
//
// [valueFiles]: https://github.com/holos-run/multi-sources-example/blob/v0.1.0/appsets/4-final/all-my-envs-appset-with-version.yaml#L27-L32
- ValueFiles: [{
- name: "version-values.yaml"
- values: valueFiles["my-values/app-version/\(parameters.version)-values.yaml"]
- }, {
- name: "type-values.yaml"
- values: valueFiles["my-values/env-type/\(parameters.type)-values.yaml"]
- }, {
- name: "region-values.yaml"
- values: valueFiles["my-values/regions/\(parameters.region)-values.yaml"]
- }, {
- name: "env-values.yaml"
- values: valueFiles["my-values/envs/\(parameters.env)-values.yaml"]
- }]
+ Values: valueFiles["my-values/app-version/\(parameters.version)-values.yaml"]
+ Values: valueFiles["my-values/env-type/\(parameters.type)-values.yaml"]
+ Values: valueFiles["my-values/regions/\(parameters.region)-values.yaml"]
+ Values: valueFiles["my-values/envs/\(parameters.env)-values.yaml"]
}
// holos represents the output for the holos command line to process. The holos
-- commit3.sh --
git add .
git commit -m '06-unification step 3'
-- commit2.sh --
git add .
git commit -m '06-unification step 2'
-- commit1.sh --
git add .
git commit -m '06-unification step 1'
-- render.sh --
rm -rf deploy
holos render platform
-- check.sh --
git status deploy
-- remove.head --
git rm -f components/my-chart/my-values/common-values.yaml
patch -p1 <<'EOF'
-- remove.tail --
EOF
-- remove.body --
diff --git a/components/my-chart/my-chart.cue b/components/my-chart/my-chart.cue
index 2809d1a..cf1dae4 100644
--- a/components/my-chart/my-chart.cue
+++ b/components/my-chart/my-chart.cue
@@ -36,9 +36,6 @@ component: #Helm & {
//
// [valueFiles]: https://github.com/holos-run/multi-sources-example/blob/v0.1.0/appsets/4-final/all-my-envs-appset-with-version.yaml#L27-L32
ValueFiles: [{
- name: "common-values.yaml"
- values: valueFiles["my-values/common-values.yaml"]
- }, {
name: "version-values.yaml"
values: valueFiles["my-values/app-version/\(parameters.version)-values.yaml"]
}, {
@@ -61,5 +58,4 @@ component: #Helm & {
holos: component.BuildPlan
// Migrated from https://github.com/holos-run/multi-sources-example/blob/v0.1.0/appsets/4-final/all-my-envs-appset-with-version.yaml#L27-L32
-valueFiles: _ @embed(glob=my-values/*.yaml)
valueFiles: _ @embed(glob=my-values/*/*-values.yaml)
-- common.path --
components/my-chart/my-values/common-values.yaml
-- grep.sh --
grep -r replicas: deploy
-- update.sh --
#! /bin/bash
set -euo pipefail
[[ -s "$1" ]] && [[ -z "${HOLOS_UPDATE_SCRIPTS:-}" ]] && exit 0
cat > "$1"

View File

@@ -0,0 +1,5 @@
# Work in the root of the example repo
cd ../script-01-clone/multi-sources-example
# Checkout the v0.4.x branch
exec git checkout v0.4.x

View File

@@ -0,0 +1,77 @@
# Work in the root of the example repo
cd ../script-01-clone/multi-sources-example
env HOME=$WORK/.tmp
env CUSTOMER=customer-zzsbbmfc
chmod 0755 $WORK/update.sh
mkdir $WORK/files
# Reset
exec git reset --hard origin/v0.4.x
# Values Generator
exec bash -c 'bash -euo pipefail $WORK/generator.sh 2>&1'
# Show the generated structure
exec bash -c 'bash -eu $WORK/show-deployment-configs.sh 2>&1'
cp stdout $WORK/show-deployment-configs.txt
exec bash -c 'bash -euo pipefail $WORK/show-value-files.sh 2>&1'
cp stdout $WORK/show-value-files.txt
# Show one buildplan
exec bash -c 'bash -euo pipefail $WORK/show-one-buildplan.sh 2>&1'
cp stdout $WORK/show-one-buildplan.txt
# Render one customer
exec bash -c 'bash -euo pipefail $WORK/render-one-customer.sh 2>&1'
stdin stdout
exec $WORK/update.sh $WORK/render-one-customer.txt
# Add and commit
exec bash -c 'bash -euo pipefail $WORK/add-and-commit-step1.sh 2>&1'
stdin stdout
exec $WORK/update.sh $WORK/add-and-commit-step1.txt
# Flattening Values
exec bash -c 'bash -euo pipefail $WORK/render-step2.sh 2>&1'
stdin stdout
exec $WORK/update.sh $WORK/render-step2.txt
# Show the flattened values
exec bash -c 'bash -euo pipefail $WORK/show-flattened-values.sh 2>&1'
cp stdout $WORK/show-flattened-values.txt
# Step 2 - Take a look at the following files
cp platform/my-app.cue $WORK/files/platform-my-app.cue
cp platform/render-values.cue $WORK/files/platform-render-values.cue
-- show-flattened-values.sh --
# Flattened values written here
tree deploy/values/customers/$CUSTOMER
# Note how the input deployment configs now map 1:1 to values files.
tree config/my-app/deployment/customers/$CUSTOMER
-- render-step2.sh --
holos render platform -t flatten -t step2 \
--selector customer=$CUSTOMER
-- add-and-commit-step1.sh --
git add deploy
git commit -m 'render step1 with helm value file overrides'
-- render-one-customer.sh --
holos render platform -t flatten -t step1 \
--selector customer=$CUSTOMER
-- show-one-buildplan.sh --
holos show buildplans -t flatten -t step1 \
--selector customer=$CUSTOMER,cluster=prod9-management
-- show-deployment-configs.sh --
ls config/my-app/deployment/customers/*/clusters/*/config.json | wc -l
-- show-value-files.sh --
tree -d components/my-app/values/
tree components/my-app/values/05-environments
tree components/my-app/values/06-tiers
tree components/my-app/values/07-scopes
-- generator.sh --
go run ./generator
-- update.sh --
#! /bin/bash
set -euo pipefail
[[ -s "$1" ]] && [[ -z "${HOLOS_UPDATE_SCRIPTS:-}" ]] && exit 0
cat > "$1"

View File

@@ -0,0 +1,38 @@
# Work in the root of the example repo
cd ../script-01-clone/multi-sources-example
env CUSTOMER=customer-zzsbbmfc
chmod 0755 $WORK/update.sh
# Show one buildplan
exec bash -c 'bash -euo pipefail $WORK/show-one-buildplan.sh 2>&1'
cp stdout $WORK/show-one-buildplan.txt
# Render one customer
exec bash -c 'bash -euo pipefail $WORK/render-one-customer.sh 2>&1'
stdin stdout
exec $WORK/update.sh $WORK/render-one-customer.txt
# Git status
exec bash -c 'bash -euo pipefail $WORK/git-status.sh 2>&1'
cp stdout $WORK/git-status.txt
# Component definitions
mkdir $WORK/files
cp components/my-app/my-app.cue $WORK/files/my-app.cue
cp components/my-app/step1.cue $WORK/files/step1.cue
cp components/my-app/step3.cue $WORK/files/step3.cue
-- git-status.sh --
git status
-- render-one-customer.sh --
find deploy/clusters -name $CUSTOMER -print0 | xargs -0 rm -rf
holos render platform -t flatten -t step3 \
--selector customer=$CUSTOMER
-- show-one-buildplan.sh --
holos show buildplans -t flatten -t step3 \
--selector customer=$CUSTOMER,cluster=prod9-management
-- update.sh --
#! /bin/bash
set -euo pipefail
[[ -s "$1" ]] && [[ -z "${HOLOS_UPDATE_SCRIPTS:-}" ]] && exit 0
cat > "$1"

View File

@@ -0,0 +1,2 @@
git clone https://github.com/holos-run/multi-sources-example.git
cd multi-sources-example

View File

@@ -0,0 +1 @@
Cloning into 'multi-sources-example'...

View File

@@ -0,0 +1 @@
6a882ac5aee7241e0130a59737cc46db5f636a21

View File

@@ -0,0 +1,2 @@
git branch -f work start
git checkout work

View File

@@ -0,0 +1 @@
Switched to branch 'work'

View File

@@ -0,0 +1 @@
holos --version

View File

@@ -0,0 +1 @@
0.103.0

View File

@@ -0,0 +1 @@
appsets/4-final/all-my-envs-appset-with-version.yaml

View File

@@ -0,0 +1,49 @@
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: all-my-envs-from-repo-with-version
namespace: argocd
spec:
goTemplate: true
goTemplateOptions: ["missingkey=error"]
generators:
- git:
repoURL: https://github.com/kostis-codefresh/multi-sources-example.git
revision: HEAD
files:
- path: "appsets/4-final/env-config/**/config.json"
template:
metadata:
name: '{{.env}}'
spec:
# The project the application belongs to.
project: default
sources:
- repoURL: https://kostis-codefresh.github.io/multi-sources-example
chart: my-chart
targetRevision: '{{.chart}}'
helm:
valueFiles:
- $values/my-values/common-values.yaml
- $values/my-values/app-version/{{.version}}-values.yaml
- $values/my-values/env-type/{{.type}}-values.yaml
- $values/my-values/regions/{{.region}}-values.yaml
- $values/my-values/envs/{{.env}}-values.yaml
- repoURL: 'https://github.com/kostis-codefresh/multi-sources-example.git'
targetRevision: HEAD
ref: values
# Destination cluster and namespace to deploy the application
destination:
server: https://kubernetes.default.svc
namespace: '{{.env}}'
# Sync policy
syncPolicy:
syncOptions:
- CreateNamespace=true
automated:
prune: true
selfHeal: true

View File

@@ -0,0 +1 @@
my-chart/templates/deployment.yaml

View File

@@ -0,0 +1,49 @@
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: simple-deployment
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
app: trivial-go-web-app
template:
metadata:
labels:
app: trivial-go-web-app
spec:
containers:
- name: webserver-simple
imagePullPolicy: Always
image: docker.io/kostiscodefresh/simple-env-app:{{ .Values.imageVersion }}
ports:
- containerPort: 8080
env:
- name: ENV
value: {{ quote .Values.environment }}
- name: ENV_TYPE
value: {{ quote .Values.environmentType }}
- name: REGION
value: {{ quote .Values.region }}
- name: PAYPAL_URL
value: {{ quote .Values.paypalUrl }}
- name: DB_USER
value: {{ quote .Values.dbUser }}
- name: DB_PASSWORD
value: {{ quote .Values.dbPassword }}
- name: GPU_ENABLED
value: {{ quote .Values.gpuEnabled }}
- name: UI_THEME
value: {{ quote .Values.userInterfaceTheme }}
- name: CACHE_SIZE
value: {{ quote .Values.cacheSize }}
- name: PAGE_LIMIT
value: {{ quote .Values.pageLimit }}
- name: SORTING
value: {{ quote .Values.sorting }}
- name: N_BUCKETS
value: {{ quote .Values.nBuckets }}

View File

@@ -0,0 +1 @@
my-chart/templates/service.yaml

View File

@@ -0,0 +1,12 @@
apiVersion: v1
kind: Service
metadata:
name: simple-service
spec:
type: ClusterIP
selector:
app: trivial-go-web-app
ports:
- protocol: TCP
port: 80
targetPort: 8080

View File

@@ -0,0 +1 @@
cat <<'EOF' > config/environments/environments.cue

View File

@@ -0,0 +1,21 @@
@extern(embed)
package environments
// We use cue embed functionality as an equivalent replacement for
// ApplicationSet generators.
config: _ @embed(glob=*/config.json)
config: _ @embed(glob=staging/*/config.json)
config: _ @embed(glob=prod/*/config.json)
config: _ @embed(glob=integration/*/config.json)
// With CUE we can constrain the data with a schema.
config: [FILEPATH=string]: #Config
// #Config defines the schema of each config.json file.
#Config: {
env: "qa" | "integration-gpu" | "integration-non-gpu" | "staging-us" | "staging-eu" | "prod-us" | "prod-eu"
region: "us" | "eu"
type: "prod" | "non-prod"
version: "qa" | "staging" | "prod"
chart: =~"^[0-9]+\\.[0-9]+\\.[0-9]+$"
}

View File

@@ -0,0 +1 @@
holos init platform v1alpha5 --force

View File

@@ -0,0 +1,2 @@
CUE_EXPERIMENT=embed holos cue export --out=yaml \
./config/environments

View File

@@ -0,0 +1,49 @@
config:
qa/config.json:
env: qa
region: us
type: non-prod
version: qa
chart: 0.2.0
staging/asia/config.json:
env: qa
region: us
type: non-prod
version: qa
chart: 0.2.0
staging/eu/config.json:
env: staging-eu
region: eu
type: non-prod
version: staging
chart: 0.2.0
prod/eu/config.json:
env: prod-eu
region: eu
type: prod
version: prod
chart: 0.1.0
integration/gpu/config.json:
env: integration-gpu
region: us
type: non-prod
version: prod
chart: 0.1.0
staging/us/config.json:
env: staging-us
region: us
type: non-prod
version: staging
chart: 0.2.0
prod/us/config.json:
env: prod-us
region: us
type: prod
version: prod
chart: 0.1.0
integration/non-gpu/config.json:
env: integration-non-gpu
region: us
type: non-prod
version: qa
chart: 0.2.0

View File

@@ -0,0 +1,30 @@
# First, we'll move my-chart the original article vendored in
# Git to the conventional location Holos uses to vendor charts.
mkdir -p components/my-chart/vendor/0.1.0
git mv my-chart components/my-chart/vendor/0.1.0/my-chart
# Helm value files move into the directory that will contain
# my-chart component definition. components/my-chart is
# conventionally called the "my-chart component directory"
git mv my-values components/my-chart/my-values
# The config.json files are moved without changing their folder structure.
# We'll package the data up into an "environments config package" for reuse.
mkdir config
git mv appsets/4-final/env-config config/environments
# All of our components will reside in the components directory so
# the CUE files `holos init` produced may be moved to keep the
# repository root tidy.
mv *.cue components/
# The following files and directories from the original article and
# holos init are no longer relevant after the migration.
mkdir not-used
git mv appsets not-used/appsets
git mv example-apps not-used/example-apps
rm -f platform.metadata.json
# Make the commit
git add platform components cue.mod .gitignore
git commit -m 'reorganize to conventional holos layout'

View File

@@ -0,0 +1 @@
cat <<'EOF' > components/my-chart/my-chart.cue

View File

@@ -0,0 +1,65 @@
@extern(embed)
package holos
import "holos.example/config/environments"
parameters: {
environments.#Config & {
env: _ @tag(env)
region: _ @tag(region)
type: _ @tag(type)
version: _ @tag(version)
chart: _ @tag(chart)
}
}
// component represents the holos component definition, which produces a
// BuildPlan for holos to execute, rendering the manifests.
component: #Helm & {
Chart: {
// Migrated from https://github.com/holos-run/multi-sources-example/blob/v0.1.0/appsets/4-final/all-my-envs-appset-with-version.yaml#L25
version: parameters.chart
repository: {
name: "multi-sources-example"
// Migrated from https://github.com/holos-run/multi-sources-example/blob/v0.1.0/appsets/4-final/all-my-envs-appset-with-version.yaml#L23
url: "https://kostis-codefresh.github.io/multi-sources-example"
}
}
// Migrated from https://github.com/holos-run/multi-sources-example/blob/v0.1.0/appsets/4-final/all-my-envs-appset-with-version.yaml#L40
// We use kustomize to manage the namespace, similar to how the original
// article uses the ApplicationSet template to specify the namespace.
KustomizeConfig: Kustomization: namespace: parameters.env
// Migrate the Helm Hierarchy preserving the behavior of over writing values.
// Migrated from [valueFiles]. Later files win.
//
// [valueFiles]: https://github.com/holos-run/multi-sources-example/blob/v0.1.0/appsets/4-final/all-my-envs-appset-with-version.yaml#L27-L32
ValueFiles: [{
name: "common-values.yaml"
values: valueFiles["my-values/common-values.yaml"]
}, {
name: "version-values.yaml"
values: valueFiles["my-values/app-version/\(parameters.version)-values.yaml"]
}, {
name: "type-values.yaml"
values: valueFiles["my-values/env-type/\(parameters.type)-values.yaml"]
}, {
name: "region-values.yaml"
values: valueFiles["my-values/regions/\(parameters.region)-values.yaml"]
}, {
name: "env-values.yaml"
values: valueFiles["my-values/envs/\(parameters.env)-values.yaml"]
}]
}
// holos represents the output for the holos command line to process. The holos
// command line processes a BuildPlan to render the helm chart component.
//
// Use the holos show buildplans command to see the BuildPlans that holos render
// platform renders.
holos: component.BuildPlan
// Migrated from https://github.com/holos-run/multi-sources-example/blob/v0.1.0/appsets/4-final/all-my-envs-appset-with-version.yaml#L27-L32
valueFiles: _ @embed(glob=my-values/*.yaml)
valueFiles: _ @embed(glob=my-values/*/*-values.yaml)

Some files were not shown because too many files have changed in this diff Show More