Compare commits

..

33 Commits

Author SHA1 Message Date
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
40 changed files with 1807 additions and 220 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",
@@ -153,6 +158,7 @@
"jetstack",
"jiralert",
"Jsonnet",
"Kargo",
"kfbh",
"killall",
"kubeadm",
@@ -277,6 +283,7 @@
"serviceentries",
"serviceentry",
"servicemonitor",
"sigstore",
"somevalue",
"SOMEVAR",
"sortoptions",
@@ -317,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

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

@@ -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

@@ -263,7 +263,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 +303,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 +320,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

@@ -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>)
@@ -169,6 +171,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 +188,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}
@@ -279,6 +296,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 +376,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"`
}

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

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,7 +1,7 @@
---
description: Private Helm Repositories
slug: private-helm
sidebar_position: 999
sidebar_position: 700
---
# Private Helm

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

@@ -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

@@ -2,15 +2,15 @@ import styles from './styles.module.css';
//Pulled from: https://gaudion.dev/blog/mdx-youtube-embed
//components/mdx/YouTube.tsx
export default function YouTube ({ id } : { id : string }){
return (
<div class={styles.videoWrapper}>
<iframe
className="aspect-video w-full"
src={"https://www.youtube.com/embed/" + id}
title="YouTube Video Player"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
></iframe>
</div>
);
};
export default function YouTube({ id }: { id: string }) {
return (
<div className={styles.videoWrapper}>
<iframe
className="aspect-video w-full"
src={"https://www.youtube.com/embed/" + id + "?rel=0"}
title="YouTube Video Player"
allow="picture-in-picture; fullscreen; accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope;"
></iframe>
</div>
);
};

View File

@@ -1,16 +1,22 @@
/docs /docs/v1alpha5/ 301
/docs/ /docs/v1alpha5/ 301
/docs/tutorial /docs/v1alpha5/tutorial/ 301
/docs/tutorial/ /docs/v1alpha5/tutorial/ 301
/docs/tutorial /docs/v1alpha5/tutorial/overview/ 301
/docs/tutorial/ /docs/v1alpha5/tutorial/overview/ 301
/docs/quickstart /docs/v1alpha5/tutorial/overview/ 301
/docs/quickstart/ /docs/v1alpha5/tutorial/overview/ 301
/docs/overview /docs/v1alpha5/tutorial/overview/ 301
/docs/overview/ /docs/v1alpha5/tutorial/overview/ 301
/docs/topics /docs/v1alpha5/topics/structures/ 301
/docs/topics/ /docs/v1alpha5/topics/structures/ 301
/docs/topics /docs/v1alpha5/topics/ 301
/docs/topics/ /docs/v1alpha5/topics/ 301
/docs/setup /docs/v1alpha5/tutorial/setup/ 301
/docs/setup/ /docs/v1alpha5/tutorial/setup/ 301
/docs/local-cluster /docs/v1alpha5/topics/local-cluster/ 301
/docs/local-cluster/ /docs/v1alpha5/topics/local-cluster/ 301
/docs/guides/helm /docs/v1alpha5/tutorial/helm-values/ 301
/docs/guides/helm/ /docs/v1alpha5/tutorial/helm-values/ 301
/docs/kargo /docs/v1alpha5/topics/kargo/ 301
/docs/kargo/ /docs/v1alpha5/topics/kargo/ 301
/docs/comparison /docs/v1alpha5/topics/comparison/ 301
/docs/comparison/ /docs/v1alpha5/topics/comparison/ 301
/docs/support /docs/v1alpha5/tutorial/overview/#getting-help 301
/docs/support/ /docs/v1alpha5/tutorial/overview/#getting-help 301

9
go.mod
View File

@@ -10,7 +10,7 @@ require (
connectrpc.com/grpcreflect v1.2.0
connectrpc.com/otelconnect v0.7.0
connectrpc.com/validate v0.1.0
cuelang.org/go v0.11.0
cuelang.org/go v0.11.1
entgo.io/ent v0.13.1
github.com/bufbuild/buf v1.35.1
github.com/choria-io/machine-room v0.0.0-20240417064836-c604da2f005e
@@ -46,6 +46,7 @@ require (
k8s.io/client-go v0.31.1
k8s.io/kubectl v0.31.1
modernc.org/sqlite v1.29.6
sigs.k8s.io/kustomize/kustomize/v5 v5.5.0
sigs.k8s.io/yaml v1.4.0
)
@@ -354,7 +355,6 @@ require (
go.opentelemetry.io/otel/metric v1.28.0 // indirect
go.opentelemetry.io/otel/sdk v1.28.0 // indirect
go.opentelemetry.io/otel/trace v1.28.0 // indirect
go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/automaxprocs v1.5.3 // indirect
go.uber.org/multierr v1.11.0 // indirect
@@ -394,7 +394,8 @@ require (
oras.land/oras-go v1.2.5 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/kind v0.23.0 // indirect
sigs.k8s.io/kustomize/api v0.17.2 // indirect
sigs.k8s.io/kustomize/kyaml v0.17.1 // indirect
sigs.k8s.io/kustomize/api v0.18.0 // indirect
sigs.k8s.io/kustomize/cmd/config v0.15.0 // indirect
sigs.k8s.io/kustomize/kyaml v0.18.1 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
)

19
go.sum
View File

@@ -52,8 +52,8 @@ connectrpc.com/validate v0.1.0 h1:r55jirxMK7HO/xZwVHj3w2XkVFarsUM77ZDy367NtH4=
connectrpc.com/validate v0.1.0/go.mod h1:GU47c9/x/gd+u9wRSPkrQOP46gx2rMN+Wo37EHgI3Ow=
cuelabs.dev/go/oci/ociregistry v0.0.0-20240906074133-82eb438dd565 h1:R5wwEcbEZSBmeyg91MJZTxfd7WpBo2jPof3AYjRbxwY=
cuelabs.dev/go/oci/ociregistry v0.0.0-20240906074133-82eb438dd565/go.mod h1:5A4xfTzHTXfeVJBU6RAUf+QrlfTCW+017q/QiW+sMLg=
cuelang.org/go v0.11.0 h1:2af2nhipqlUHtXk2dtOP5xnMm1ObGvKqIsJUJL1sRE4=
cuelang.org/go v0.11.0/go.mod h1:PBY6XvPUswPPJ2inpvUozP9mebDVTXaeehQikhZPBz0=
cuelang.org/go v0.11.1 h1:pV+49MX1mmvDm8Qh3Za3M786cty8VKPWzQ1Ho4gZRP0=
cuelang.org/go v0.11.1/go.mod h1:PBY6XvPUswPPJ2inpvUozP9mebDVTXaeehQikhZPBz0=
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
@@ -1077,8 +1077,6 @@ go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVf
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
go.starlark.net v0.0.0-20230525235612-a134d8f9ddca h1:VdD38733bfYv5tUZwEIskMM93VanwNIi5bIKnDrJdEY=
go.starlark.net v0.0.0-20230525235612-a134d8f9ddca/go.mod h1:jxU+3+j+71eXOW14274+SmmuW82qJzl6iZSeqEtTGds=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8=
@@ -1282,7 +1280,6 @@ golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
@@ -1557,10 +1554,14 @@ sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMm
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
sigs.k8s.io/kind v0.23.0 h1:8fyDGWbWTeCcCTwA04v4Nfr45KKxbSPH1WO9K+jVrBg=
sigs.k8s.io/kind v0.23.0/go.mod h1:ZQ1iZuJLh3T+O8fzhdi3VWcFTzsdXtNv2ppsHc8JQ7s=
sigs.k8s.io/kustomize/api v0.17.2 h1:E7/Fjk7V5fboiuijoZHgs4aHuexi5Y2loXlVOAVAG5g=
sigs.k8s.io/kustomize/api v0.17.2/go.mod h1:UWTz9Ct+MvoeQsHcJ5e+vziRRkwimm3HytpZgIYqye0=
sigs.k8s.io/kustomize/kyaml v0.17.1 h1:TnxYQxFXzbmNG6gOINgGWQt09GghzgTP6mIurOgrLCQ=
sigs.k8s.io/kustomize/kyaml v0.17.1/go.mod h1:9V0mCjIEYjlXuCdYsSXvyoy2BTsLESH7TlGV81S282U=
sigs.k8s.io/kustomize/api v0.18.0 h1:hTzp67k+3NEVInwz5BHyzc9rGxIauoXferXyjv5lWPo=
sigs.k8s.io/kustomize/api v0.18.0/go.mod h1:f8isXnX+8b+SGLHQ6yO4JG1rdkZlvhaCf/uZbLVMb0U=
sigs.k8s.io/kustomize/cmd/config v0.15.0 h1:WkdY8V2+8J+W00YbImXa2ke9oegfrHH79e+kywW7EdU=
sigs.k8s.io/kustomize/cmd/config v0.15.0/go.mod h1:Jq57b0nPaoYUlOqg//0JtAh6iibboqMcfbtCYoWPM00=
sigs.k8s.io/kustomize/kustomize/v5 v5.5.0 h1:o1mtt6vpxsxDYaZKrw3BnEtc+pAjLz7UffnIvHNbvW0=
sigs.k8s.io/kustomize/kustomize/v5 v5.5.0/go.mod h1:AeFCmgCrXzmvjWWaeZCyBp6XzG1Y0w1svYus8GhJEOE=
sigs.k8s.io/kustomize/kyaml v0.18.1 h1:WvBo56Wzw3fjS+7vBjN6TeivvpbW9GmRaWZ9CIVmt4E=
sigs.k8s.io/kustomize/kyaml v0.18.1/go.mod h1:C3L2BFVU1jgcddNBE1TxuVLgS46TjObMwW5FT9FcjYo=
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4=
sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08=
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=

View File

@@ -5,17 +5,68 @@ import (
"encoding/json"
"fmt"
"log/slog"
"os"
"path/filepath"
"strings"
"cuelang.org/go/cue"
"cuelang.org/go/cue/cuecontext"
"cuelang.org/go/cue/interpreter/embed"
"cuelang.org/go/cue/load"
"cuelang.org/go/encoding/yaml"
"github.com/holos-run/holos/internal/errors"
"github.com/holos-run/holos/internal/holos"
"github.com/holos-run/holos/internal/util"
)
func LoadInstance(path string, tags []string) (*Instance, error) {
// ExtractYAML extracts yaml encoded data from file paths. The data is unified
// into one [cue.Value]. If a path element is a directory, all files in the
// directory are loaded non-recursively.
//
// Attribution: https://github.com/cue-lang/cue/issues/3504
func ExtractYAML(ctxt *cue.Context, filepaths []string) (cue.Value, error) {
value := ctxt.CompileString("")
files := make([]string, 0, 10*len(filepaths))
for _, path := range filepaths {
info, err := os.Stat(path)
if err != nil {
return value, errors.Wrap(err)
}
if !info.IsDir() {
files = append(files, path)
continue
}
entries, err := os.ReadDir(path)
if err != nil {
return value, errors.Wrap(err)
}
for _, entry := range entries {
if entry.IsDir() {
continue
}
files = append(files, filepath.Join(path, entry.Name()))
}
}
for _, file := range files {
f, err := yaml.Extract(file, nil)
if err != nil {
return value, errors.Wrap(err)
}
value = value.Unify(ctxt.BuildFile(f))
}
return value, nil
}
// LoadInstance loads the cue configuration instance at path. External data
// file paths are loaded by calling [ExtractYAML] providing filepaths. The
// extracted data values are unified with the platform configuration [cue.Value]
// in the returned [Instance].
func LoadInstance(path string, filepaths []string, tags []string) (*Instance, error) {
root, leaf, err := util.FindRootLeaf(path)
if err != nil {
return nil, errors.Wrap(err)
@@ -26,20 +77,26 @@ func LoadInstance(path string, tags []string) (*Instance, error) {
ModuleRoot: root,
Tags: tags,
}
ctxt := cuecontext.New(cuecontext.Interpreter(embed.New()))
ctx := cuecontext.New()
instances := load.Instances([]string{leaf}, cfg)
values, err := ctx.BuildInstances(instances)
bis := load.Instances([]string{path}, cfg)
values, err := ctxt.BuildInstances(bis)
if err != nil {
return nil, errors.Wrap(err)
}
value, err := ExtractYAML(ctxt, filepaths)
if err != nil {
return nil, errors.Wrap(err)
}
// TODO: https://cuelang.org/docs/howto/place-data-go-api/
value = value.Unify(values[0])
inst := &Instance{
path: leaf,
ctx: ctx,
ctx: ctxt,
cfg: cfg,
value: values[0],
value: value,
}
return inst, nil

View File

@@ -107,6 +107,20 @@ func (c *Component) Path() string {
return util.DotSlash(c.Component.Path)
}
// ExtractYAML returns the path values for the --extract-yaml command line flag.
func (c *Component) ExtractYAML() ([]string, error) {
if c == nil {
return nil, nil
}
instances := make([]string, 0, len(c.Component.Instances))
for _, instance := range c.Component.Instances {
if instance.Kind == "ExtractYAML" {
instances = append(instances, instance.ExtractYAML.Path)
}
}
return instances, nil
}
var _ holos.BuildPlan = &BuildPlan{}
var _ task = generatorTask{}
var _ task = transformersTask{}

View File

@@ -1,110 +1 @@
package cli
import (
"context"
"fmt"
"log/slog"
"os"
"runtime/pprof"
"runtime/trace"
"connectrpc.com/connect"
cue "cuelang.org/go/cue/errors"
"github.com/holos-run/holos/internal/errors"
"github.com/holos-run/holos/internal/holos"
"google.golang.org/genproto/googleapis/rpc/errdetails"
)
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 {
_ = HandleError(ctx, err, cfg)
}
}
}
// 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 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 HandleError(ctx, err, cfg)
}
}
feature := &holos.EnvFlagger{}
if err := New(cfg, feature).ExecuteContext(ctx); err != nil {
return HandleError(ctx, err, cfg)
}
return 0
}
}
// HandleError is the top level error handler that unwraps and logs errors.
func HandleError(ctx context.Context, err error, hc *holos.Config) (exitCode int) {
// Connect errors have codes, log them.
log := hc.NewTopLevelLogger().With("code", connect.CodeOf(err))
var cueErr cue.Error
var errAt *errors.ErrorAt
if errors.As(err, &errAt) {
loc := errAt.Source.Loc()
err2 := errAt.Unwrap()
log.ErrorContext(ctx, fmt.Sprintf("could not run: %s at %s", err2, loc), "err", err2, "loc", loc)
} else {
log.ErrorContext(ctx, fmt.Sprintf("could not run: %s", err), "err", err)
}
// cue errors are bundled up as a list and refer to multiple files / lines.
if errors.As(err, &cueErr) {
msg := cue.Details(cueErr, nil)
if _, err := fmt.Fprint(hc.Stderr(), msg); err != nil {
log.ErrorContext(ctx, "could not write CUE error details: "+err.Error(), "err", err)
}
}
// connect errors have details and codes.
// Refer to https://connectrpc.com/docs/go/errors
if connectErr := new(connect.Error); errors.As(err, &connectErr) {
for _, detail := range connectErr.Details() {
msg, valueErr := detail.Value()
if valueErr != nil {
log.WarnContext(ctx, "could not decode error detail", "err", err, "type", detail.Type(), "note", "this usually means we don't have the schema for the protobuf message type")
continue
}
if info, ok := msg.(*errdetails.ErrorInfo); ok {
logDetail := log.With("reason", info.GetReason(), "domain", info.GetDomain())
for k, v := range info.GetMetadata() {
logDetail = logDetail.With(k, v)
}
logDetail.ErrorContext(ctx, info.String())
}
}
}
return 1
}

View File

@@ -43,6 +43,8 @@ func newPlatform(cfg *holos.Config, feature holos.Flagger) *cobra.Command {
cmd.Flags().IntVar(&concurrency, "concurrency", runtime.NumCPU(), "number of components to render concurrently")
var platform string
cmd.Flags().StringVar(&platform, "platform", "./platform", "platform directory path")
var extractYAMLs holos.StringSlice
cmd.Flags().Var(&extractYAMLs, "extract-yaml", "data file paths to extract and unify with the platform config")
var selector holos.Selector
cmd.Flags().VarP(&selector, "selector", "l", "label selector (e.g. label==string,label!=string)")
tagMap := make(holos.TagMap)
@@ -57,7 +59,7 @@ func newPlatform(cfg *holos.Config, feature holos.Flagger) *cobra.Command {
log.WarnContext(ctx, fmt.Sprintf(msg, platform))
}
inst, err := builder.LoadInstance(platform, tagMap.Tags())
inst, err := builder.LoadInstance(platform, extractYAMLs, tagMap.Tags())
if err != nil {
return errors.Wrap(err)
}
@@ -107,12 +109,14 @@ func newComponent(cfg *holos.Config, feature holos.Flagger) *cobra.Command {
cmd.Flags().VarP(&tagMap, "inject", "t", tagHelp)
var concurrency int
cmd.Flags().IntVar(&concurrency, "concurrency", runtime.NumCPU(), "number of concurrent build steps")
var extractYAMLs holos.StringSlice
cmd.Flags().Var(&extractYAMLs, "extract-yaml", "data file paths to extract and unify with the platform config")
cmd.RunE = func(cmd *cobra.Command, args []string) error {
ctx := cmd.Root().Context()
path := args[0]
inst, err := builder.LoadInstance(path, tagMap.Tags())
inst, err := builder.LoadInstance(path, extractYAMLs, tagMap.Tags())
if err != nil {
return errors.Wrap(err)
}
@@ -146,7 +150,11 @@ func makeComponentRenderFunc(w io.Writer, prefixArgs, cliTags []string) func(con
if err != nil {
return errors.Wrap(err)
}
args := make([]string, 0, 10+len(prefixArgs)+(len(tags)*2))
filepaths, err := component.ExtractYAML()
if err != nil {
return errors.Wrap(err)
}
args := make([]string, 0, 10+len(prefixArgs)+(len(tags)*2+len(filepaths)*2))
args = append(args, prefixArgs...)
args = append(args, "render", "component")
for _, tag := range cliTags {
@@ -155,6 +163,9 @@ func makeComponentRenderFunc(w io.Writer, prefixArgs, cliTags []string) func(con
for _, tag := range tags {
args = append(args, "--inject", tag)
}
for _, path := range filepaths {
args = append(args, "--extract-yaml", path)
}
args = append(args, component.Path())
if _, err := util.RunCmdA(ctx, w, "holos", args...); err != nil {
return errors.Format("could not render component: %w", err)

View File

@@ -1,14 +1,18 @@
package cli
import (
"context"
_ "embed"
"fmt"
"log/slog"
"connectrpc.com/connect"
"github.com/spf13/cobra"
"google.golang.org/genproto/googleapis/rpc/errdetails"
"github.com/holos-run/holos/version"
"github.com/holos-run/holos/internal/errors"
"github.com/holos-run/holos/internal/holos"
"github.com/holos-run/holos/internal/logger"
"github.com/holos-run/holos/internal/server"
@@ -28,7 +32,8 @@ import (
"github.com/holos-run/holos/internal/cli/token"
"github.com/holos-run/holos/internal/cli/txtar"
cue "cuelang.org/go/cmd/cue/cmd"
cueCmd "cuelang.org/go/cmd/cue/cmd"
cue_errors "cuelang.org/go/cue/errors"
)
//go:embed help.txt
@@ -119,7 +124,7 @@ func newOrgCmd(feature holos.Flagger) (cmd *cobra.Command) {
func newCueCmd() (cmd *cobra.Command) {
// Get a handle on the cue root command fields.
root, _ := cue.New([]string{})
root, _ := cueCmd.New([]string{})
// Copy the fields to our embedded command.
cmd = command.New("cue")
cmd.Short = root.Short
@@ -130,8 +135,52 @@ func newCueCmd() (cmd *cobra.Command) {
// We do it this way so we handle errors correctly.
cmd.RunE = func(cmd *cobra.Command, args []string) error {
cueRootCommand, _ := cue.New(args)
cueRootCommand, _ := cueCmd.New(args)
return cueRootCommand.Run(cmd.Root().Context())
}
return cmd
}
// HandleError is the top level error handler that unwraps and logs errors.
func HandleError(ctx context.Context, err error, hc *holos.Config) (exitCode int) {
// Connect errors have codes, log them.
log := hc.NewTopLevelLogger().With("code", connect.CodeOf(err))
var cueErr cue_errors.Error
var errAt *errors.ErrorAt
if errors.As(err, &errAt) {
loc := errAt.Source.Loc()
err2 := errAt.Unwrap()
log.ErrorContext(ctx, fmt.Sprintf("could not run: %s at %s", err2, loc), "err", err2, "loc", loc)
} else {
log.ErrorContext(ctx, fmt.Sprintf("could not run: %s", err), "err", err)
}
// cue errors are bundled up as a list and refer to multiple files / lines.
if errors.As(err, &cueErr) {
msg := cue_errors.Details(cueErr, nil)
if _, err := fmt.Fprint(hc.Stderr(), msg); err != nil {
log.ErrorContext(ctx, "could not write CUE error details: "+err.Error(), "err", err)
}
}
// connect errors have details and codes.
// Refer to https://connectrpc.com/docs/go/errors
if connectErr := new(connect.Error); errors.As(err, &connectErr) {
for _, detail := range connectErr.Details() {
msg, valueErr := detail.Value()
if valueErr != nil {
log.WarnContext(ctx, "could not decode error detail", "err", err, "type", detail.Type(), "note", "this usually means we don't have the schema for the protobuf message type")
continue
}
if info, ok := msg.(*errdetails.ErrorInfo); ok {
logDetail := log.With("reason", info.GetReason(), "domain", info.GetDomain())
for k, v := range info.GetMetadata() {
logDetail = logDetail.With(k, v)
}
logDetail.ErrorContext(ctx, info.String())
}
}
}
return 1
}

View File

@@ -30,13 +30,15 @@ func newShowPlatformCmd() (cmd *cobra.Command) {
var platform string
cmd.Flags().StringVar(&platform, "platform", "./platform", "platform directory path")
var extractYAMLs holos.StringSlice
cmd.Flags().Var(&extractYAMLs, "extract-yaml", "data file paths to extract and unify with the platform config")
var format string
cmd.Flags().StringVar(&format, "format", "yaml", "yaml or json format")
tagMap := make(holos.TagMap)
cmd.Flags().VarP(&tagMap, "inject", "t", "set the value of a cue @tag field from a key=value pair")
cmd.RunE = func(c *cobra.Command, args []string) (err error) {
inst, err := builder.LoadInstance(platform, tagMap.Tags())
inst, err := builder.LoadInstance(platform, extractYAMLs, tagMap.Tags())
if err != nil {
return errors.Wrap(err)
}
@@ -64,6 +66,8 @@ func newShowBuildPlanCmd() (cmd *cobra.Command) {
var platform string
cmd.Flags().StringVar(&platform, "platform", "./platform", "platform directory path")
var extractYAMLs holos.StringSlice
cmd.Flags().Var(&extractYAMLs, "extract-yaml", "data file paths to extract and unify with the platform config")
var format string
cmd.Flags().StringVar(&format, "format", "yaml", "yaml or json format")
var selector holos.Selector
@@ -75,7 +79,7 @@ func newShowBuildPlanCmd() (cmd *cobra.Command) {
cmd.RunE = func(c *cobra.Command, args []string) (err error) {
path := platform
inst, err := builder.LoadInstance(path, tagMap.Tags())
inst, err := builder.LoadInstance(path, extractYAMLs, tagMap.Tags())
if err != nil {
return errors.Wrap(err)
}
@@ -122,7 +126,11 @@ func makeBuildFunc(encoder holos.OrderedEncoder, opts holos.BuildOpts) func(cont
return errors.Wrap(err)
}
tags = append(tags, opts.Tags...)
inst, err := builder.LoadInstance(component.Path(), tags)
filepaths, err := component.ExtractYAML()
if err != nil {
return errors.Wrap(err)
}
inst, err := builder.LoadInstance(component.Path(), filepaths, tags)
if err != nil {
return errors.Wrap(err)
}

View File

@@ -290,7 +290,7 @@ package core
labels?: {[string]: string} @go(Labels,map[string]string)
// 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?: {[string]: string} @go(Annotations,map[string]string)
}
@@ -334,6 +334,11 @@ package core
// Injected as the tag variable "holos_component_path".
path: string @go(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] @go(Instances,[]Instance)
// WriteTo represents the holos render component --write-to flag. If empty,
// the default value for the --write-to flag is used.
writeTo?: string @go(WriteTo)
@@ -350,6 +355,31 @@ package core
labels?: {[string]: string} @go(Labels,map[string]string)
// 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?: {[string]: string} @go(Annotations,map[string]string)
}
// 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
#Instance: {
// Kind is a discriminator.
kind: string & "ExtractYAML" @go(Kind)
// Ignored unless kind is ExtractYAML.
extractYAML?: #ExtractYAML @go(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.
#ExtractYAML: {
path: string @go(Path)
}

View File

@@ -0,0 +1,154 @@
// Code generated by timoni. DO NOT EDIT.
//timoni:generate timoni vendor crd -f /Users/jeff/Holos/kargo-demo/deploy/components/kargo/kargo.gen.yaml
package v1alpha1
import "strings"
// Freight represents a collection of versioned artifacts.
#Freight: {
// Alias is a human-friendly alias for a piece of Freight. This is
// an optional
// field. A defaulting webhook will sync this field with the value
// of the
// kargo.akuity.io/alias label. When the alias label is not
// present or differs
// from the value of this field, the defaulting webhook will set
// the label to
// the value of this field. If the alias label is present and this
// field is
// empty, the defaulting webhook will set the value of this field
// to the value
// of the alias label. If this field is empty and the alias label
// is not
// present, the defaulting webhook will choose an available alias
// and assign
// it to both the field and label.
alias?: string
// APIVersion defines the versioned schema of this representation
// of an object.
// Servers should convert recognized schemas to the latest
// internal value, and
// may reject unrecognized values.
// More info:
// https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
apiVersion: "kargo.akuity.io/v1alpha1"
// Charts describes specific versions of specific Helm charts.
charts?: [...{
// Name specifies the name of the chart.
name?: string
// RepoURL specifies the URL of a Helm chart repository. Classic
// chart
// repositories (using HTTP/S) can contain differently named
// charts. When this
// field points to such a repository, the Name field will specify
// the name of
// the chart within the repository. In the case of a repository
// within an OCI
// registry, the URL implicitly points to a specific chart and the
// Name field
// will be empty.
repoURL?: string
// Version specifies a particular version of the chart.
version?: string
}]
// Commits describes specific Git repository commits.
commits?: [...{
// Author is the author of the commit.
author?: string
// Branch denotes the branch of the repository where this commit
// was found.
branch?: string
// Committer is the person who committed the commit.
committer?: string
// ID is the ID of a specific commit in the Git repository
// specified by
// RepoURL.
id?: string
// Message is the message associated with the commit. At present,
// this only
// contains the first line (subject) of the commit message.
message?: string
// RepoURL is the URL of a Git repository.
repoURL?: string
// Tag denotes a tag in the repository that matched selection
// criteria and
// resolved to this commit.
tag?: string
}]
// Images describes specific versions of specific container
// images.
images?: [...{
// Digest identifies a specific version of the image in the
// repository
// specified by RepoURL. This is a more precise identifier than
// Tag.
digest?: string
// GitRepoURL specifies the URL of a Git repository that contains
// the source
// code for the image repository referenced by the RepoURL field
// if Kargo was
// able to infer it.
gitRepoURL?: string
// RepoURL describes the repository in which the image can be
// found.
repoURL?: string
// Tag identifies a specific version of the image in the
// repository specified
// by RepoURL.
tag?: string
}]
// Kind is a string value representing the REST resource this
// object represents.
// Servers may infer this from the endpoint the client submits
// requests to.
// Cannot be updated.
// In CamelCase.
// More info:
// https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
kind: "Freight"
metadata!: {
name!: strings.MaxRunes(253) & strings.MinRunes(1) & {
string
}
namespace!: strings.MaxRunes(63) & strings.MinRunes(1) & {
string
}
labels?: {
[string]: string
}
annotations?: {
[string]: string
}
}
// Origin describes a kind of Freight in terms of its origin.
origin: {
// Kind is the kind of resource from which Freight may have
// originated. At
// present, this can only be "Warehouse".
kind: "Warehouse"
// Name is the name of the resource of the kind indicated by the
// Kind field
// from which Freight may originated.
name: string
}
}

View File

@@ -0,0 +1,72 @@
// Code generated by timoni. DO NOT EDIT.
//timoni:generate timoni vendor crd -f /Users/jeff/Holos/kargo-demo/deploy/components/kargo/kargo.gen.yaml
package v1alpha1
import "strings"
// Project is a resource type that reconciles to a specially
// labeled namespace
// and other TODO: TBD project-level resources.
#Project: {
// APIVersion defines the versioned schema of this representation
// of an object.
// Servers should convert recognized schemas to the latest
// internal value, and
// may reject unrecognized values.
// More info:
// https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
apiVersion: "kargo.akuity.io/v1alpha1"
// Kind is a string value representing the REST resource this
// object represents.
// Servers may infer this from the endpoint the client submits
// requests to.
// Cannot be updated.
// In CamelCase.
// More info:
// https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
kind: "Project"
metadata!: {
name!: strings.MaxRunes(253) & strings.MinRunes(1) & {
string
}
namespace?: strings.MaxRunes(63) & strings.MinRunes(1) & {
string
}
labels?: {
[string]: string
}
annotations?: {
[string]: string
}
}
// Spec describes a Project.
spec!: #ProjectSpec
}
#ProjectSpec: {
// PromotionPolicies defines policies governing the promotion of
// Freight to
// specific Stages within this Project.
promotionPolicies?: [...{
// AutoPromotionEnabled indicates whether new Freight can
// automatically be
// promoted into the Stage referenced by the Stage field. Note:
// There are may
// be other conditions also required for an auto-promotion to
// occur. This
// field defaults to false, but is commonly set to true for Stages
// that
// subscribe to Warehouses instead of other, upstream Stages. This
// allows
// users to define Stages that are automatically updated as soon
// as new
// artifacts are detected.
autoPromotionEnabled?: bool
stage: strings.MinRunes(1) & {
=~"^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$"
}
}]
}

View File

@@ -0,0 +1,163 @@
// Code generated by timoni. DO NOT EDIT.
//timoni:generate timoni vendor crd -f /Users/jeff/Holos/kargo-demo/deploy/components/kargo/kargo.gen.yaml
package v1alpha1
import "strings"
// Promotion represents a request to transition a particular Stage
// into a
// particular Freight.
#Promotion: {
// APIVersion defines the versioned schema of this representation
// of an object.
// Servers should convert recognized schemas to the latest
// internal value, and
// may reject unrecognized values.
// More info:
// https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
apiVersion: "kargo.akuity.io/v1alpha1"
// Kind is a string value representing the REST resource this
// object represents.
// Servers may infer this from the endpoint the client submits
// requests to.
// Cannot be updated.
// In CamelCase.
// More info:
// https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
kind: "Promotion"
metadata!: {
name!: strings.MaxRunes(253) & strings.MinRunes(1) & {
string
}
namespace!: strings.MaxRunes(63) & strings.MinRunes(1) & {
string
}
labels?: {
[string]: string
}
annotations?: {
[string]: string
}
}
// Spec describes the desired transition of a specific Stage into
// a specific
// Freight.
spec!: #PromotionSpec
}
// Spec describes the desired transition of a specific Stage into
// a specific
// Freight.
#PromotionSpec: {
// Freight specifies the piece of Freight to be promoted into the
// Stage
// referenced by the Stage field.
freight: strings.MinRunes(1)
// Stage specifies the name of the Stage to which this Promotion
// applies. The Stage referenced by this field MUST be in the same
// namespace as the Promotion.
stage: strings.MinRunes(1) & {
=~"^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$"
}
// Steps specifies the directives to be executed as part of this
// Promotion.
// The order in which the directives are executed is the order in
// which they
// are listed in this field.
steps?: [...{
// As is the alias this step can be referred to as.
as?: string
// Config is opaque configuration for the PromotionStep that is
// understood
// only by each PromotionStep's implementation. It is legal to
// utilize
// expressions in defining values at any level of this block.
// See https://docs.kargo.io/references/expression-language for
// details.
config?: _
// Retry is the retry policy for this step.
retry?: {
// ErrorThreshold is the number of consecutive times the step must
// fail (for
// any reason) before retries are abandoned and the entire
// Promotion is marked
// as failed.
//
// If this field is set to 0, the effective default will be a
// step-specific
// one. If no step-specific default exists (i.e. is also 0), the
// effective
// default will be the system-wide default of 1.
//
// A value of 1 will cause the Promotion to be marked as failed
// after just
// a single failure; i.e. no retries will be attempted.
//
// There is no option to specify an infinite number of retries
// using a value
// such as -1.
//
// In a future release, Kargo is likely to become capable of
// distinguishing
// between recoverable and non-recoverable step failures. At that
// time, it is
// planned that unrecoverable failures will not be subject to this
// threshold
// and will immediately cause the Promotion to be marked as failed
// without
// further condition.
errorThreshold?: int
// Timeout is the soft maximum interval in which a step that
// returns a Running
// status (which typically indicates it's waiting for something to
// happen)
// may be retried.
//
// The maximum is a soft one because the check for whether the
// interval has
// elapsed occurs AFTER the step has run. This effectively means a
// step may
// run ONCE beyond the close of the interval.
//
// If this field is set to nil, the effective default will be a
// step-specific
// one. If no step-specific default exists (i.e. is also nil), the
// effective
// default will be the system-wide default of 0.
//
// A value of 0 will cause the step to be retried indefinitely
// unless the
// ErrorThreshold is reached.
timeout?: string
}
// Uses identifies a runner that can execute this step.
uses: strings.MinRunes(1)
}]
// Vars is a list of variables that can be referenced by
// expressions in
// promotion steps.
vars?: [...{
// Name is the name of the variable.
name: strings.MinRunes(1) & {
=~"^[a-zA-Z_]\\w*$"
}
// Value is the value of the variable. It is allowed to utilize
// expressions
// in the value.
// See https://docs.kargo.io/references/expression-language for
// details.
value: string
}]
}

View File

@@ -0,0 +1,264 @@
// Code generated by timoni. DO NOT EDIT.
//timoni:generate timoni vendor crd -f /Users/jeff/Holos/kargo-demo/deploy/components/kargo/kargo.gen.yaml
package v1alpha1
import "strings"
// Stage is the Kargo API's main type.
#Stage: {
// APIVersion defines the versioned schema of this representation
// of an object.
// Servers should convert recognized schemas to the latest
// internal value, and
// may reject unrecognized values.
// More info:
// https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
apiVersion: "kargo.akuity.io/v1alpha1"
// Kind is a string value representing the REST resource this
// object represents.
// Servers may infer this from the endpoint the client submits
// requests to.
// Cannot be updated.
// In CamelCase.
// More info:
// https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
kind: "Stage"
metadata!: {
name!: strings.MaxRunes(253) & strings.MinRunes(1) & {
string
}
namespace!: strings.MaxRunes(63) & strings.MinRunes(1) & {
string
}
labels?: {
[string]: string
}
annotations?: {
[string]: string
}
}
// Spec describes sources of Freight used by the Stage and how to
// incorporate
// Freight into the Stage.
spec!: #StageSpec
}
// Spec describes sources of Freight used by the Stage and how to
// incorporate
// Freight into the Stage.
#StageSpec: {
promotionTemplate?: {
// PromotionTemplateSpec describes the (partial) specification of
// a Promotion
// for a Stage. This is a template that can be used to create a
// Promotion for a
// Stage.
spec: {
// Steps specifies the directives to be executed as part of a
// Promotion.
// The order in which the directives are executed is the order in
// which they
// are listed in this field.
steps?: [...{
// As is the alias this step can be referred to as.
as?: string
// Config is opaque configuration for the PromotionStep that is
// understood
// only by each PromotionStep's implementation. It is legal to
// utilize
// expressions in defining values at any level of this block.
// See https://docs.kargo.io/references/expression-language for
// details.
config?: _
// Retry is the retry policy for this step.
retry?: {
// ErrorThreshold is the number of consecutive times the step must
// fail (for
// any reason) before retries are abandoned and the entire
// Promotion is marked
// as failed.
//
// If this field is set to 0, the effective default will be a
// step-specific
// one. If no step-specific default exists (i.e. is also 0), the
// effective
// default will be the system-wide default of 1.
//
// A value of 1 will cause the Promotion to be marked as failed
// after just
// a single failure; i.e. no retries will be attempted.
//
// There is no option to specify an infinite number of retries
// using a value
// such as -1.
//
// In a future release, Kargo is likely to become capable of
// distinguishing
// between recoverable and non-recoverable step failures. At that
// time, it is
// planned that unrecoverable failures will not be subject to this
// threshold
// and will immediately cause the Promotion to be marked as failed
// without
// further condition.
errorThreshold?: int
// Timeout is the soft maximum interval in which a step that
// returns a Running
// status (which typically indicates it's waiting for something to
// happen)
// may be retried.
//
// The maximum is a soft one because the check for whether the
// interval has
// elapsed occurs AFTER the step has run. This effectively means a
// step may
// run ONCE beyond the close of the interval.
//
// If this field is set to nil, the effective default will be a
// step-specific
// one. If no step-specific default exists (i.e. is also nil), the
// effective
// default will be the system-wide default of 0.
//
// A value of 0 will cause the step to be retried indefinitely
// unless the
// ErrorThreshold is reached.
timeout?: string
}
// Uses identifies a runner that can execute this step.
uses: strings.MinRunes(1)
}] & [_, ...]
// Vars is a list of variables that can be referenced by
// expressions in
// promotion steps.
vars?: [...{
// Name is the name of the variable.
name: strings.MinRunes(1) & {
=~"^[a-zA-Z_]\\w*$"
}
// Value is the value of the variable. It is allowed to utilize
// expressions
// in the value.
// See https://docs.kargo.io/references/expression-language for
// details.
value: string
}]
}
}
// RequestedFreight expresses the Stage's need for certain pieces
// of Freight,
// each having originated from a particular Warehouse. This list
// must be
// non-empty. In the common case, a Stage will request Freight
// having
// originated from just one specific Warehouse. In advanced cases,
// requesting
// Freight from multiple Warehouses provides a method of advancing
// new
// artifacts of different types through parallel pipelines at
// different
// speeds. This can be useful, for instance, if a Stage is home to
// multiple
// microservices that are independently versioned.
requestedFreight: [...{
// Origin specifies from where the requested Freight must have
// originated.
// This is a required field.
origin: {
// Kind is the kind of resource from which Freight may have
// originated. At
// present, this can only be "Warehouse".
kind: "Warehouse"
// Name is the name of the resource of the kind indicated by the
// Kind field
// from which Freight may originated.
name: string
}
// Sources describes where the requested Freight may be obtained
// from. This is
// a required field.
sources: {
// Direct indicates the requested Freight may be obtained directly
// from the
// Warehouse from which it originated. If this field's value is
// false, then
// the value of the Stages field must be non-empty. i.e. Between
// the two
// fields, at least one source must be specified.
direct?: bool
// Stages identifies other "upstream" Stages as potential sources
// of the
// requested Freight. If this field's value is empty, then the
// value of the
// Direct field must be true. i.e. Between the two fields, at
// least on source
// must be specified.
stages?: [...string]
}
}] & [_, ...]
// Shard is the name of the shard that this Stage belongs to. This
// is an
// optional field. If not specified, the Stage will belong to the
// default
// shard. A defaulting webhook will sync the value of the
// kargo.akuity.io/shard label with the value of this field. When
// this field
// is empty, the webhook will ensure that label is absent.
shard?: string
// Verification describes how to verify a Stage's current Freight
// is fit for
// promotion downstream.
verification?: {
// AnalysisRunMetadata contains optional metadata that should be
// applied to
// all AnalysisRuns.
analysisRunMetadata?: {
// Additional annotations to apply to an AnalysisRun.
annotations?: {
[string]: string
}
// Additional labels to apply to an AnalysisRun.
labels?: {
[string]: string
}
}
// AnalysisTemplates is a list of AnalysisTemplates from which
// AnalysisRuns
// should be created to verify a Stage's current Freight is fit to
// be promoted
// downstream.
analysisTemplates?: [...{
// Name is the name of the AnalysisTemplate in the same
// project/namespace as
// the Stage.
name: string
}]
// Args lists arguments that should be added to all AnalysisRuns.
args?: [...{
// Name is the name of the argument.
name: string
// Value is the value of the argument.
value: string
}]
}
}

View File

@@ -0,0 +1,402 @@
// Code generated by timoni. DO NOT EDIT.
//timoni:generate timoni vendor crd -f /Users/jeff/Holos/kargo-demo/deploy/components/kargo/kargo.gen.yaml
package v1alpha1
import "strings"
// Warehouse is a source of Freight.
#Warehouse: {
// APIVersion defines the versioned schema of this representation
// of an object.
// Servers should convert recognized schemas to the latest
// internal value, and
// may reject unrecognized values.
// More info:
// https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
apiVersion: "kargo.akuity.io/v1alpha1"
// Kind is a string value representing the REST resource this
// object represents.
// Servers may infer this from the endpoint the client submits
// requests to.
// Cannot be updated.
// In CamelCase.
// More info:
// https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
kind: "Warehouse"
metadata!: {
name!: strings.MaxRunes(253) & strings.MinRunes(1) & {
string
}
namespace!: strings.MaxRunes(63) & strings.MinRunes(1) & {
string
}
labels?: {
[string]: string
}
annotations?: {
[string]: string
}
}
// Spec describes sources of artifacts.
spec!: #WarehouseSpec
}
// Spec describes sources of artifacts.
#WarehouseSpec: {
// FreightCreationPolicy describes how Freight is created by this
// Warehouse.
// This field is optional. When left unspecified, the field is
// implicitly
// treated as if its value were "Automatic".
// Accepted values: Automatic, Manual
freightCreationPolicy?: "Automatic" | "Manual" | *"Automatic"
// Interval is the reconciliation interval for this Warehouse. On
// each
// reconciliation, the Warehouse will discover new artifacts and
// optionally
// produce new Freight. This field is optional. When left
// unspecified, the
// field is implicitly treated as if its value were "5m0s".
interval: =~"^([0-9]+(\\.[0-9]+)?(s|m|h))+$" | *"5m0s"
// Shard is the name of the shard that this Warehouse belongs to.
// This is an
// optional field. If not specified, the Warehouse will belong to
// the default
// shard. A defaulting webhook will sync this field with the value
// of the
// kargo.akuity.io/shard label. When the shard label is not
// present or differs
// from the value of this field, the defaulting webhook will set
// the label to
// the value of this field. If the shard label is present and this
// field is
// empty, the defaulting webhook will set the value of this field
// to the value
// of the shard label.
shard?: string
// Subscriptions describes sources of artifacts to be included in
// Freight
// produced by this Warehouse.
subscriptions: [...{
// Chart describes a subscription to a Helm chart repository.
chart?: {
// DiscoveryLimit is an optional limit on the number of chart
// versions that
// can be discovered for this subscription. The limit is applied
// after
// filtering charts based on the SemverConstraint field.
// When left unspecified, the field is implicitly treated as if
// its value
// were "20". The upper limit for this field is 100.
discoveryLimit?: int & <=100 & >=1 | *20
// Name specifies the name of a Helm chart to subscribe to within
// a classic
// chart repository specified by the RepoURL field. This field is
// required
// when the RepoURL field points to a classic chart repository and
// MUST
// otherwise be empty.
name?: string
// RepoURL specifies the URL of a Helm chart repository. It may be
// a classic
// chart repository (using HTTP/S) OR a repository within an OCI
// registry.
// Classic chart repositories can contain differently named
// charts. When this
// field points to such a repository, the Name field MUST also be
// used
// to specify the name of the desired chart within that
// repository. In the
// case of a repository within an OCI registry, the URL implicitly
// points to
// a specific chart and the Name field MUST NOT be used. The
// RepoURL field is
// required.
repoURL: strings.MinRunes(1) & {
=~"^(((https?)|(oci))://)([\\w\\d\\.\\-]+)(:[\\d]+)?(/.*)*$"
}
// SemverConstraint specifies constraints on what new chart
// versions are
// permissible. This field is optional. When left unspecified,
// there will be
// no constraints, which means the latest version of the chart
// will always be
// used. Care should be taken with leaving this field unspecified,
// as it can
// lead to the unanticipated rollout of breaking changes.
// More info:
// https://github.com/masterminds/semver#checking-version-constraints
semverConstraint?: string
}
// Git describes a subscriptions to a Git repository.
git?: {
// AllowTags is a regular expression that can optionally be used
// to limit the
// tags that are considered in determining the newest commit of
// interest. The
// value in this field only has any effect when the
// CommitSelectionStrategy is
// Lexical, NewestTag, or SemVer. This field is optional.
allowTags?: string
// Branch references a particular branch of the repository. The
// value in this
// field only has any effect when the CommitSelectionStrategy is
// NewestFromBranch or left unspecified (which is implicitly the
// same as
// NewestFromBranch). This field is optional. When left
// unspecified, (and the
// CommitSelectionStrategy is NewestFromBranch or unspecified),
// the
// subscription is implicitly to the repository's default branch.
branch?: strings.MinRunes(1) & {
=~"^\\w+([-/]\\w+)*$"
}
// CommitSelectionStrategy specifies the rules for how to identify
// the newest
// commit of interest in the repository specified by the RepoURL
// field. This
// field is optional. When left unspecified, the field is
// implicitly treated
// as if its value were "NewestFromBranch".
// Accepted values: Lexical, NewestFromBranch, NewestTag, SemVer
commitSelectionStrategy?: "Lexical" | "NewestFromBranch" | "NewestTag" | "SemVer" | *"NewestFromBranch"
// DiscoveryLimit is an optional limit on the number of commits
// that can be
// discovered for this subscription. The limit is applied after
// filtering
// commits based on the AllowTags and IgnoreTags fields.
// When left unspecified, the field is implicitly treated as if
// its value
// were "20". The upper limit for this field is 100.
discoveryLimit?: int & <=100 & >=1 | *20
// ExcludePaths is a list of selectors that designate paths in the
// repository
// that should NOT trigger the production of new Freight when
// changes are
// detected therein. When specified, changes in the identified
// paths will not
// trigger Freight production. When not specified, paths that
// should trigger
// Freight production will be defined solely by IncludePaths.
// Selectors may be
// defined using:
// 1. Exact paths to files or directories (ex. "charts/foo")
// 2. Glob patterns (prefix the pattern with "glob:"; ex.
// "glob:*.yaml")
// 3. Regular expressions (prefix the pattern with "regex:" or
// "regexp:";
// ex. "regexp:^.*\.yaml$")
// Paths selected by IncludePaths may be unselected by
// ExcludePaths. This
// is a useful method for including a broad set of paths and then
// excluding a
// subset of them.
excludePaths?: [...string]
// IgnoreTags is a list of tags that must be ignored when
// determining the
// newest commit of interest. No regular expressions or glob
// patterns are
// supported yet. The value in this field only has any effect when
// the
// CommitSelectionStrategy is Lexical, NewestTag, or SemVer. This
// field is
// optional.
ignoreTags?: [...string]
// IncludePaths is a list of selectors that designate paths in the
// repository
// that should trigger the production of new Freight when changes
// are detected
// therein. When specified, only changes in the identified paths
// will trigger
// Freight production. When not specified, changes in any path
// will trigger
// Freight production. Selectors may be defined using:
// 1. Exact paths to files or directories (ex. "charts/foo")
// 2. Glob patterns (prefix the pattern with "glob:"; ex.
// "glob:*.yaml")
// 3. Regular expressions (prefix the pattern with "regex:" or
// "regexp:";
// ex. "regexp:^.*\.yaml$")
// Paths selected by IncludePaths may be unselected by
// ExcludePaths. This
// is a useful method for including a broad set of paths and then
// excluding a
// subset of them.
includePaths?: [...string]
// InsecureSkipTLSVerify specifies whether certificate
// verification errors
// should be ignored when connecting to the repository. This
// should be enabled
// only with great caution.
insecureSkipTLSVerify?: bool
// URL is the repository's URL. This is a required field.
repoURL: strings.MinRunes(1) & {
=~"(?:^(https?)://(?:([\\w-]+):(.+)@)?([\\w-]+(?:\\.[\\w-]+)*)(?::(\\d{1,5}))?(/.*)$)|(?:^([\\w-]+)@([\\w+]+(?:\\.[\\w-]+)*):(/?.*))"
}
// SemverConstraint specifies constraints on what new tagged
// commits are
// considered in determining the newest commit of interest. The
// value in this
// field only has any effect when the CommitSelectionStrategy is
// SemVer. This
// field is optional. When left unspecified, there will be no
// constraints,
// which means the latest semantically tagged commit will always
// be used. Care
// should be taken with leaving this field unspecified, as it can
// lead to the
// unanticipated rollout of breaking changes.
semverConstraint?: string
// StrictSemvers specifies whether only "strict" semver tags
// should be
// considered. A "strict" semver tag is one containing ALL of
// major, minor,
// and patch version components. This is enabled by default, but
// only has any
// effect when the CommitSelectionStrategy is SemVer. This should
// be disabled
// cautiously, as it creates the potential for any tag containing
// numeric
// characters only to be mistaken for a semver string containing
// the major
// version number only.
strictSemvers: bool | *true
}
// Image describes a subscription to container image repository.
image?: {
// AllowTags is a regular expression that can optionally be used
// to limit the
// image tags that are considered in determining the newest
// version of an
// image. This field is optional.
allowTags?: string
// DiscoveryLimit is an optional limit on the number of image
// references
// that can be discovered for this subscription. The limit is
// applied after
// filtering images based on the AllowTags and IgnoreTags fields.
// When left unspecified, the field is implicitly treated as if
// its value
// were "20". The upper limit for this field is 100.
discoveryLimit?: int & <=100 & >=1 | *20
// GitRepoURL optionally specifies the URL of a Git repository
// that contains
// the source code for the image repository referenced by the
// RepoURL field.
// When this is specified, Kargo MAY be able to infer and link to
// the exact
// revision of that source code that was used to build the image.
gitRepoURL?: =~"^https?://(\\w+([\\.-]\\w+)*@)?\\w+([\\.-]\\w+)*(:[\\d]+)?(/.*)?$"
// IgnoreTags is a list of tags that must be ignored when
// determining the
// newest version of an image. No regular expressions or glob
// patterns are
// supported yet. This field is optional.
ignoreTags?: [...string]
// ImageSelectionStrategy specifies the rules for how to identify
// the newest version
// of the image specified by the RepoURL field. This field is
// optional. When
// left unspecified, the field is implicitly treated as if its
// value were
// "SemVer".
// Accepted values: Digest, Lexical, NewestBuild, SemVer
imageSelectionStrategy?: "Digest" | "Lexical" | "NewestBuild" | "SemVer" | *"SemVer"
// InsecureSkipTLSVerify specifies whether certificate
// verification errors
// should be ignored when connecting to the repository. This
// should be enabled
// only with great caution.
insecureSkipTLSVerify?: bool
// Platform is a string of the form <os>/<arch> that limits the
// tags that can
// be considered when searching for new versions of an image. This
// field is
// optional. When left unspecified, it is implicitly equivalent to
// the
// OS/architecture of the Kargo controller. Care should be taken
// to set this
// value correctly in cases where the image referenced by this
// ImageRepositorySubscription will run on a Kubernetes node with
// a different
// OS/architecture than the Kargo controller. At present this is
// uncommon, but
// not unheard of.
platform?: string
// RepoURL specifies the URL of the image repository to subscribe
// to. The
// value in this field MUST NOT include an image tag. This field
// is required.
repoURL: strings.MinRunes(1) & {
=~"^(\\w+([\\.-]\\w+)*(:[\\d]+)?/)?(\\w+([\\.-]\\w+)*)(/\\w+([\\.-]\\w+)*)*$"
}
// SemverConstraint specifies constraints on what new image
// versions are
// permissible. The value in this field only has any effect when
// the
// ImageSelectionStrategy is SemVer or left unspecified (which is
// implicitly
// the same as SemVer). This field is also optional. When left
// unspecified,
// (and the ImageSelectionStrategy is SemVer or unspecified),
// there will be no
// constraints, which means the latest semantically tagged version
// of an image
// will always be used. Care should be taken with leaving this
// field
// unspecified, as it can lead to the unanticipated rollout of
// breaking
// changes. Refer to Image Updater documentation for more details.
// More info:
// https://github.com/masterminds/semver#checking-version-constraints
semverConstraint?: string
// StrictSemvers specifies whether only "strict" semver tags
// should be
// considered. A "strict" semver tag is one containing ALL of
// major, minor,
// and patch version components. This is enabled by default, but
// only has any
// effect when the ImageSelectionStrategy is SemVer. This should
// be disabled
// cautiously, as it is not uncommon to tag container images with
// short Git
// commit hashes, which have the potential to contain numeric
// characters only
// and could be mistaken for a semver string containing the major
// version
// number only.
strictSemvers: bool | *true
}
}] & [_, ...]
}

View File

@@ -1,2 +1,2 @@
module: "user.holos.run/platform"
language: version: "v0.9.2"
module: "example.com/platform"
language: version: "v0.11.0"

View File

@@ -21,6 +21,8 @@ type Platform interface {
type Component interface {
Describe() string
Path() string
// ExtractYAML represents the values of the --extract-yaml flag
ExtractYAML() ([]string, error)
Tags() ([]string, error)
WriteTo() string
Labels() Labels

View File

@@ -42,6 +42,9 @@ func FindCueMod(path string) (root string, err error) {
return root, nil
}
// FindRootLeaf returns the root path containing the cue.mod and the leaf path
// relative to the root for the given target path. FindRootLeaf calls
// [filepath.Clean] on the returned paths.
func FindRootLeaf(target string) (root string, leaf string, err error) {
if root, err = FindCueMod(target); err != nil {
return "", "", err

View File

@@ -4,5 +4,5 @@ deps:
- remote: buf.build
owner: bufbuild
repository: protovalidate
commit: 5a7b106cbb87462d9a8c9ffecdbd2e38
digest: shake256:2f7efa5a904668219f039d4f6eeb51e871f8f7f5966055a10663cba335bd65f76cac84da3fa758ab7b5dcb489ec599521390ce3951d119fb56df1fc2def16bb0
commit: a3320276596649bcad929ac829d451f4
digest: shake256:a6e5f64fd3fd47e3e8568e9753f59a1566f56c11ec055baf65463d3bca3499f6f16c2d6f5628fa41cfd0f4fa7e72abe65be4efd77d269749492472ed4cc4070d

View File

@@ -6,6 +6,7 @@ package tools
// https://go.dev/wiki/Modules
import (
_ "sigs.k8s.io/kustomize/kustomize/v5"
_ "connectrpc.com/connect/cmd/protoc-gen-connect-go"
_ "cuelang.org/go/cmd/cue"
_ "github.com/bufbuild/buf/cmd/buf"

View File

@@ -1 +1 @@
101
102

View File

@@ -1 +1 @@
5
2