Compare commits

..

5 Commits

Author SHA1 Message Date
Gary Larizza
488f7ae985 Start Hello Holos
This is a very, very minimal framework using the Tokyo docs as a guide.
This code is going to have to change when `v1alpha5` is published, so
I'm just dropping it in here as a placeholder.

Like the Tokyo docs, I was intening to setup the code and commands at the
start, and then have a "Breaking it down" section later explaining all
the moving parts.
2024-11-05 16:27:10 -08:00
Gary Larizza
4817491aab Update groupId for uniqueness 2024-11-05 16:26:40 -08:00
Gary Larizza
30ce72a9b7 Add the Local Cluster guide 2024-11-05 14:50:16 -08:00
Gary Larizza
958f65ddcf Setup page first draft 2024-11-05 14:47:12 -08:00
Gary Larizza
a1c9111a05 Overview initial draft 2024-11-05 13:30:35 -08:00
67 changed files with 621 additions and 18490 deletions

View File

@@ -190,7 +190,6 @@
"oauthproxy",
"objectmap",
"objectmeta",
"omitempty",
"organizationconnect",
"orgid",
"otelconnect",

View File

@@ -1,141 +0,0 @@
// Package author contains a standard set of schemas for component authors to
// generate common [core] BuildPlans.
//
// Holos values stability, flexibility, and composition. This package
// intentionally defines only the minimal necessary set of structures.
// Component authors are encouraged to define their own structures building on
// our example [topics].
//
// The Holos Maintainers may add definitions to this package if the community
// identifies nearly all users must define the exact same structure. Otherwise,
// definitions should be added as a customizable example in [topics].
//
// For example, structures representing a cluster and environment almost always
// need to be defined. Their definition varies from one organization to the
// next. Therefore, customizable definitions for a cluster and environment are
// best maintained in [topics], not standardized in this package.
//
// [core]: https://holos.run/docs/api/core/
// [topics]: https://holos.run/docs/topics/
package author
import core "github.com/holos-run/holos/api/core/v1alpha5"
//go:generate ../../../hack/gendoc
// Platform assembles a core Platform in the Resource field for the holos render
// platform command. Use the Components field to register components with the
// platform.
type Platform struct {
Name string
Components map[NameLabel]core.Component
Resource core.Platform
}
// ComponentConfig represents the configuration common to all kinds of
// components for use with the holos render component command. All component
// kinds may be transformed with [kustomize] configured with the
// [KustomizeConfig] field.
//
// - [Helm] charts.
// - [Kubernetes] resources generated from CUE.
// - [Kustomize] bases.
//
// [kustomize]: https://kubectl.docs.kubernetes.io/references/kustomize/kustomization/
type ComponentConfig struct {
// Name represents the BuildPlan metadata.name field. Used to construct the
// fully rendered manifest file path.
Name string
// Path represents the path to the component producing the BuildPlan.
Path string
// Parameters are useful to reuse a component with various parameters.
// Injected as CUE @tag variables. Parameters with a "holos_" prefix are
// reserved for use by the Holos Authors.
Parameters map[string]string
// OutputBaseDir represents the output base directory used when assembling
// artifacts. Useful to organize components by clusters or other parameters.
// For example, holos writes resource manifests to
// {WriteTo}/{OutputBaseDir}/components/{Name}/{Name}.gen.yaml
OutputBaseDir string `cue:"string | *\"\""`
// Resources represents kubernetes resources mixed into the rendered manifest.
Resources core.Resources
// KustomizeConfig represents the configuration kustomize.
KustomizeConfig KustomizeConfig
// Artifacts represents additional artifacts to mix in. Useful for adding
// GitOps resources. Each Artifact is unified without modification into the
// BuildPlan.
Artifacts map[NameLabel]core.Artifact
}
// Helm assembles a BuildPlan rendering a helm chart. Useful to mix in
// additional resources from CUE and transform the helm output with kustomize.
type Helm struct {
ComponentConfig `json:",inline"`
// Chart represents a Helm chart.
Chart core.Chart
// Values represents data to marshal into a values.yaml for helm.
Values core.Values
// EnableHooks enables helm hooks when executing the `helm template` command.
EnableHooks bool `cue:"true | *false"`
// Namespace sets the helm chart namespace flag if provided.
Namespace string `json:",omitempty"`
// BuildPlan represents the derived BuildPlan produced for the holos render
// component command.
BuildPlan core.BuildPlan
}
// Kubernetes assembles a BuildPlan containing inline resources exported from
// CUE.
type Kubernetes struct {
ComponentConfig `json:",inline"`
// BuildPlan represents the derived BuildPlan produced for the holos render
// component command.
BuildPlan core.BuildPlan
}
// Kustomize assembles a BuildPlan rendering manifests from a [kustomize]
// kustomization.
//
// [kustomize]: https://kubectl.docs.kubernetes.io/references/kustomize/kustomization/
type Kustomize struct {
ComponentConfig `json:",inline"`
// BuildPlan represents the derived BuildPlan produced for the holos render
// component command.
BuildPlan core.BuildPlan
}
// KustomizeConfig represents the configuration for [kustomize] post processing.
// Use the Files field to mix in plain manifest files located in the component
// directory. Use the Resources field to mix in manifests from network urls.
//
// [kustomize]: https://kubectl.docs.kubernetes.io/references/kustomize/kustomization/
type KustomizeConfig struct {
// Kustomization represents the kustomization used to transform resources.
// Note the resources field is internally managed from the Files and Resources fields.
Kustomization map[string]any `json:",omitempty"`
// Files represents files to copy from the component directory for kustomization.
Files map[string]struct{ Source string } `cue:"{[NAME=_]: Source: NAME}"`
// Resources represents additional entries to included in the resources list.
Resources map[string]struct{ Source string } `cue:"{[NAME=_]: Source: NAME}"`
// CommonLabels represents common labels added without including selectors.
CommonLabels map[string]string
}
// NameLabel represents the common use case of converting a struct to a list
// where the name field of each value unifies with the field name of the outer
// struct.
//
// For example:
//
// S: [NameLabel=string]: name: NameLabel
// S: jeff: _
// S: gary: _
// S: nate: _
// L: [for x in S {x}]
// // L is [{name: "jeff"}, {name: "gary"}, {name: "nate"}]
type NameLabel string

View File

@@ -1,5 +0,0 @@
---
title: Author Schemas
description: Standardized schemas for component authors.
sidebar_position: 200
---

View File

@@ -1,5 +0,0 @@
---
title: Core Schemas
description: BuildPlan defines the holos rendering pipeline.
sidebar_position: 100
---

View File

@@ -1,275 +0,0 @@
// Package core contains schemas for a [Platform] and [BuildPlan]. Holos takes
// a [Platform] as input, then iterates over each [Component] to produce a
// [BuildPlan]. Holos processes the [BuildPlan] to produce fully rendered
// manifests, each an [Artifact].
package core
//go:generate ../../../hack/gendoc
// BuildPlan represents an implementation of the [rendered manifest pattern].
// Holos processes a BuildPlan to produce one or more [Artifact] output files.
// BuildPlan artifact files usually contain Kubernetes manifests, but they may
// have any content.
//
// A BuildPlan usually produces two artifacts. One artifact contains a manifest
// of resources. A second artifact contains a GitOps resource to manage the
// first, usually an ArgoCD Application resource.
//
// Holos uses CUE to construct a BuildPlan. A future enhancement will support
// user defined executables providing a BuildPlan to Holos in the style of an
// [external credential provider].
//
// [rendered manifest pattern]: https://akuity.io/blog/the-rendered-manifests-pattern
// [external credential provider]: https://github.com/kubernetes/enhancements/blob/313ad8b59c80819659e1fbf0f165230f633f2b22/keps/sig-auth/541-external-credential-providers/README.md
type BuildPlan struct {
// Kind represents the type of the resource.
Kind string `json:"kind" cue:"\"BuildPlan\""`
// APIVersion represents the versioned schema of the resource.
APIVersion string `json:"apiVersion" cue:"string | *\"v1alpha5\""`
// Metadata represents data about the resource such as the Name.
Metadata Metadata `json:"metadata"`
// Spec specifies the desired state of the resource.
Spec BuildPlanSpec `json:"spec"`
// Source reflects the origin of the BuildPlan.
Source BuildPlanSource `json:"source,omitempty"`
}
// BuildPlanSpec represents the specification of the [BuildPlan].
type BuildPlanSpec struct {
// Artifacts represents the artifacts for holos to build.
Artifacts []Artifact `json:"artifacts"`
// Disabled causes the holos cli to disregard the build plan.
Disabled bool `json:"disabled,omitempty"`
}
// BuildPlanSource reflects the origin of a [BuildPlan]. Useful to save a build
// plan to a file, then re-generate it without needing to process a [Platform]
// component collection.
type BuildPlanSource struct {
// Component reflects the component that produced the build plan.
Component Component `json:"component,omitempty"`
}
// Artifact represents one fully rendered manifest produced by a [Transformer]
// sequence, which transforms a [Generator] collection. A [BuildPlan] produces
// an [Artifact] collection.
//
// Each Artifact produces one manifest file artifact. Generator Output values
// are used as Transformer Inputs. The Output field of the final [Transformer]
// should have the same value as the Artifact field.
//
// When there is more than one [Generator] there must be at least one
// [Transformer] to combine outputs into one Artifact. If there is a single
// Generator, it may directly produce the Artifact output.
//
// An Artifact is processed concurrently with other artifacts in the same
// [BuildPlan]. An Artifact should not use an output from another Artifact as
// an input. Each [Generator] may also run concurrently. Each [Transformer] is
// executed sequentially starting after all generators have completed.
//
// Output fields are write-once. It is an error for multiple Generators or
// Transformers to produce the same Output value within the context of a
// [BuildPlan].
type Artifact struct {
Artifact FilePath `json:"artifact,omitempty"`
Generators []Generator `json:"generators,omitempty"`
Transformers []Transformer `json:"transformers,omitempty"`
Skip bool `json:"skip,omitempty"`
}
// Generator generates Kubernetes resources. [Helm] and [Resources] are the
// most commonly used, often paired together to mix-in resources to an
// unmodified Helm chart. A simple [File] generator is also available for use
// with the [Kustomize] transformer.
//
// Each Generator in an [Artifact] must have a distinct Output value for a
// [Transformer] to reference.
//
// 1. [Resources] - Generates resources from CUE code.
// 2. [Helm] - Generates rendered yaml from a [Chart].
// 3. [File] - Generates data by reading a file from the component directory.
type Generator struct {
// Kind represents the kind of generator. Must be Resources, Helm, or File.
Kind string `json:"kind" cue:"\"Resources\" | \"Helm\" | \"File\""`
// Output represents a file for a Transformer or Artifact to consume.
Output FilePath `json:"output"`
// Resources generator. Ignored unless kind is Resources. Resources are
// stored as a two level struct. The top level key is the Kind of resource,
// e.g. Namespace or Deployment. The second level key is an arbitrary
// InternalLabel. The third level is a map[string]any representing the
// Resource.
Resources Resources `json:"resources,omitempty"`
// Helm generator. Ignored unless kind is Helm.
Helm Helm `json:"helm,omitempty"`
// File generator. Ignored unless kind is File.
File File `json:"file,omitempty"`
}
// Resource represents one kubernetes api object.
type Resource map[string]any
// Resources represents Kubernetes resources. Most commonly used to mix
// resources into the [BuildPlan] generated from CUE, but may be generated from
// elsewhere.
type Resources map[Kind]map[InternalLabel]Resource
// File represents a simple single file copy [Generator]. Useful with a
// [Kustomize] [Transformer] to process plain manifest files stored in the
// component directory. Multiple File generators may be used to transform
// multiple resources.
type File struct {
// Source represents a file sub-path relative to the component path.
Source FilePath `json:"source"`
}
// Helm represents a [Chart] manifest [Generator].
type Helm struct {
// Chart represents a helm chart to manage.
Chart Chart `json:"chart"`
// Values represents values for holos to marshal into values.yaml when
// rendering the chart.
Values Values `json:"values"`
// EnableHooks enables helm hooks when executing the `helm template` command.
EnableHooks bool `json:"enableHooks,omitempty"`
// Namespace represents the helm namespace flag
Namespace string `json:"namespace,omitempty"`
}
// Values represents [Helm] Chart values generated from CUE.
type Values map[string]any
// Chart represents a [Helm] Chart.
type Chart struct {
// Name represents the chart name.
Name string `json:"name"`
// Version represents the chart version.
Version string `json:"version"`
// Release represents the chart release when executing helm template.
Release string `json:"release"`
// Repository represents the repository to fetch the chart from.
Repository Repository `json:"repository,omitempty"`
}
// Repository represents a [Helm] [Chart] repository.
type Repository struct {
Name string `json:"name"`
URL string `json:"url"`
}
// Transformer combines multiple inputs from prior [Generator] or [Transformer]
// outputs into one output. [Kustomize] is the most commonly used transformer.
// A simple [Join] is also supported for use with plain manifest files.
//
// 1. [Kustomize] - Patch and transform the output from prior generators or
// transformers. See [Introduction to Kustomize].
// 2. [Join] - Concatenate multiple prior outputs into one output.
//
// [Introduction to Kustomize]: https://kubectl.docs.kubernetes.io/guides/config_management/introduction/
type Transformer struct {
// Kind represents the kind of transformer. Must be Kustomize, or Join.
Kind string `json:"kind" cue:"\"Kustomize\" | \"Join\""`
// Inputs represents the files to transform. The Output of prior Generators
// and Transformers.
Inputs []FilePath `json:"inputs"`
// Output represents a file for a subsequent Transformer or Artifact to
// consume.
Output FilePath `json:"output"`
// Kustomize transformer. Ignored unless kind is Kustomize.
Kustomize Kustomize `json:"kustomize,omitempty"`
// Join transformer. Ignored unless kind is Join.
Join Join `json:"join,omitempty"`
}
// Join represents a [Transformer] using [bytes.Join] to concatenate multiple
// inputs into one output with a separator. Useful for combining output from
// [Helm] and [Resources] together into one [Artifact] when [Kustomize] is
// otherwise unnecessary.
//
// [bytes.Join]: https://pkg.go.dev/bytes#Join
type Join struct {
Separator string `json:"separator" cue:"string | *\"---\\n\""`
}
// Kustomize represents a kustomization [Transformer].
type Kustomize struct {
// Kustomization represents the decoded kustomization.yaml file
Kustomization Kustomization `json:"kustomization"`
// Files holds file contents for kustomize, e.g. patch files.
Files FileContentMap `json:"files,omitempty"`
}
// Kustomization represents a kustomization.yaml file for use with the
// [Kustomize] [Transformer]. Untyped to avoid tightly coupling holos to
// kubectl versions which was a problem for the Flux maintainers. Type checking
// is expected to happen in CUE against the kubectl version the user prefers.
type Kustomization map[string]any
// FileContent represents file contents.
type FileContent string
// FileContentMap represents a mapping of file paths to file contents.
type FileContentMap map[FilePath]FileContent
// FilePath represents a file path.
type FilePath string
// InternalLabel is an arbitrary unique identifier internal to holos itself.
// The holos cli is expected to never write a InternalLabel value to rendered
// output files, therefore use a InternalLabel when the identifier must be
// unique and internal. Defined as a type for clarity and type checking.
type InternalLabel string
// Kind is a discriminator. Defined as a type for clarity and type checking.
type Kind string
// Metadata represents data about the resource such as the Name.
type Metadata struct {
// Name represents the resource name.
Name string `json:"name"`
}
// Platform represents a platform to manage. A Platform specifies a [Component]
// collection and integrates the components together into a holistic platform.
// Holos iterates over the [Component] collection producing a [BuildPlan] for
// each, which holos then executes to render manifests.
//
// Inspect a Platform resource holos would process by executing:
//
// cue export --out yaml ./platform
type Platform struct {
// Kind is a string value representing the resource.
Kind string `json:"kind" cue:"\"Platform\""`
// APIVersion represents the versioned schema of this resource.
APIVersion string `json:"apiVersion" cue:"string | *\"v1alpha5\""`
// Metadata represents data about the resource such as the Name.
Metadata Metadata `json:"metadata"`
// Spec represents the platform specification.
Spec PlatformSpec `json:"spec"`
}
// PlatformSpec represents the platform specification.
type PlatformSpec struct {
// Components represents a collection of holos components to manage.
Components []Component `json:"components"`
}
// Component represents the complete context necessary to produce a [BuildPlan]
// from a path containing parameterized CUE configuration.
type Component struct {
// Name represents the name of the component. Injected as the tag variable
// "holos_component_name".
Name string `json:"name"`
// Path represents the path of the component relative to the platform root.
// Injected as the tag variable "holos_component_path".
Path string `json:"path"`
// 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"`
// Parameters represent user defined input variables to produce various
// [BuildPlan] resources from one component path. Injected as CUE @tag
// variables. Parameters with a "holos_" prefix are reserved for use by the
// Holos Authors. Multiple environments are a prime example of an input
// parameter that should always be user defined, never defined by Holos.
Parameters map[string]string `json:"parameters,omitempty"`
}

View File

@@ -17,16 +17,9 @@ func TestMain(m *testing.M) {
}))
}
func TestGuides_v1alpha4(t *testing.T) {
func TestGuides(t *testing.T) {
testscript.Run(t, params(filepath.Join("v1alpha4", "guides")))
}
func TestGuides_v1alpha5(t *testing.T) {
testscript.Run(t, params(filepath.Join("v1alpha5", "guides")))
}
func TestSchemas_v1alpha5(t *testing.T) {
testscript.Run(t, params(filepath.Join("v1alpha5", "schemas")))
}
func TestCLI(t *testing.T) {
testscript.Run(t, params("cli"))
@@ -36,9 +29,7 @@ func params(dir string) testscript.Params {
return testscript.Params{
Dir: filepath.Join("tests", dir),
RequireExplicitExec: true,
RequireUniqueNames: os.Getenv("HOLOS_WORKDIR_ROOT") == "",
WorkdirRoot: os.Getenv("HOLOS_WORKDIR_ROOT"),
UpdateScripts: os.Getenv("HOLOS_UPDATE_SCRIPTS") != "",
RequireUniqueNames: true,
Setup: func(env *testscript.Env) error {
// 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"))

View File

@@ -4,7 +4,7 @@
cd $WORK
# Generate the directory structure we're going to work in.
exec holos generate platform v1alpha4 --force
exec holos generate platform v1alpha4
# Platforms are empty by default.
exec holos render platform ./platform
@@ -1781,6 +1781,9 @@ _Platform: Components: blackbox: {
-- projects/platform/components/blackbox/blackbox.cue.disabled --
package holos
// Produce a helm chart build plan.
_Helm.BuildPlan
_Helm: #Helm & {
Chart: {
name: "prometheus-blackbox-exporter"
@@ -1791,9 +1794,6 @@ _Helm: #Helm & {
}
}
}
// Produce a helm chart build plan.
_Helm.BuildPlan
-- projects/blackbox.schema.cue.disabled --
package holos
@@ -1813,6 +1813,9 @@ _blackbox: #blackbox & {
-- projects/platform/components/prometheus/prometheus.cue.disabled --
package holos
// Produce a helm chart build plan.
_Helm.BuildPlan
_Helm: #Helm & {
Chart: {
name: "prometheus"
@@ -1823,9 +1826,6 @@ _Helm: #Helm & {
}
}
}
// Produce a helm chart build plan.
_Helm.BuildPlan
-- platform/prometheus.cue.disabled --
package holos
@@ -14983,6 +14983,9 @@ package holos
import "encoding/yaml"
// Produce a Kustomize BuildPlan for Holos
_Kustomize.BuildPlan
// https://github.com/mccutchen/go-httpbin/blob/v2.15.0/kustomize/README.md
_Kustomize: #Kustomize & {
KustomizeConfig: Files: "resources.yaml": _
@@ -15001,9 +15004,6 @@ _Kustomize: #Kustomize & {
patches: [for x in _patches {x}]
}
}
// Produce a Kustomize BuildPlan for Holos
_Kustomize.BuildPlan
-- projects/platform/components/httpbin/resources.yaml --
apiVersion: apps/v1
kind: Deployment

File diff suppressed because it is too large Load Diff

View File

@@ -1,52 +0,0 @@
# author.#Kubernetes
# Start in an empty directory.
cd $WORK
# Generate the directory structure we're going to work in.
exec holos generate platform v1alpha5 --force
# Platforms are empty by default.
exec holos render platform ./platform
stderr -count=1 '^rendered platform'
# When author.#Kubernetes is empty
exec holos cue export --expression holos --out=yaml ./components/empty
cmp stdout want.txt
-- components/empty/empty.cue --
package holos
Kubernetes: #Kubernetes & {}
holos: Kubernetes.BuildPlan
-- want.txt --
kind: BuildPlan
apiVersion: v1alpha5
metadata:
name: no-name
spec:
artifacts:
- artifact: components/no-name/no-name.gen.yaml
generators:
- kind: Resources
output: resources.gen.yaml
resources: {}
transformers:
- kind: Kustomize
inputs:
- resources.gen.yaml
output: components/no-name/no-name.gen.yaml
kustomize:
kustomization:
labels:
- includeSelectors: false
pairs: {}
resources:
- resources.gen.yaml
kind: Kustomization
apiVersion: kustomize.config.k8s.io/v1beta1
source:
component:
name: no-name
path: no-path
parameters: {}

View File

@@ -1,7 +1,7 @@
---
description: Build a local cluster for use with Holos.
slug: local-cluster
sidebar_position: 100
description: Build a local Cluster to use with these guides.
slug: /guides/local-cluster
sidebar_position: 999
---
import Tabs from '@theme/Tabs';

View File

@@ -23,7 +23,7 @@ improves cross team collaboration through well defined, typed structures at the
integration layer. These definitions provide golden paths for other teams to
easily integrate their own services into the platform.
{/* truncate */}
<!-- truncate -->
## The Problem

5
doc/md/api.md Normal file
View File

@@ -0,0 +1,5 @@
import DocCardList from '@theme/DocCardList';
# API Reference
<DocCardList />

View File

@@ -1,10 +0,0 @@
---
slug: api
description: Schema Reference
---
import DocCardList from '@theme/DocCardList';
# Schema Reference
<DocCardList />

View File

@@ -1,175 +1,3 @@
---
title: Author Schemas
description: Standardized schemas for component authors.
sidebar_position: 200
---
<!-- Code generated by gomarkdoc. DO NOT EDIT -->
# Author Schema
```go
import "github.com/holos-run/holos/api/author/v1alpha5"
```
Package author contains a standard set of schemas for component authors to generate common [core](<https://holos.run/docs/api/core/>) BuildPlans.
Holos values stability, flexibility, and composition. This package intentionally defines only the minimal necessary set of structures. Component authors are encouraged to define their own structures building on our example [topics](<https://holos.run/docs/topics/>).
The Holos Maintainers may add definitions to this package if the community identifies nearly all users must define the exact same structure. Otherwise, definitions should be added as a customizable example in [topics](<https://holos.run/docs/topics/>).
For example, structures representing a cluster and environment almost always need to be defined. Their definition varies from one organization to the next. Therefore, customizable definitions for a cluster and environment are best maintained in [topics](<https://holos.run/docs/topics/>), not standardized in this package.
## Index
- [type ComponentConfig](<#ComponentConfig>)
- [type Helm](<#Helm>)
- [type Kubernetes](<#Kubernetes>)
- [type Kustomize](<#Kustomize>)
- [type KustomizeConfig](<#KustomizeConfig>)
- [type NameLabel](<#NameLabel>)
- [type Platform](<#Platform>)
<a name="ComponentConfig"></a>
## type ComponentConfig {#ComponentConfig}
ComponentConfig represents the configuration common to all kinds of components for use with the holos render component command. All component kinds may be transformed with [kustomize](<https://kubectl.docs.kubernetes.io/references/kustomize/kustomization/>) configured with the [KustomizeConfig](<#KustomizeConfig>) field.
- [Helm](<#Helm>) charts.
- [Kubernetes](<#Kubernetes>) resources generated from CUE.
- [Kustomize](<#Kustomize>) bases.
```go
type ComponentConfig struct {
// Name represents the BuildPlan metadata.name field. Used to construct the
// fully rendered manifest file path.
Name string
// Path represents the path to the component producing the BuildPlan.
Path string
// Parameters are useful to reuse a component with various parameters.
// Injected as CUE @tag variables. Parameters with a "holos_" prefix are
// reserved for use by the Holos Authors.
Parameters map[string]string
// OutputBaseDir represents the output base directory used when assembling
// artifacts. Useful to organize components by clusters or other parameters.
// For example, holos writes resource manifests to
// {WriteTo}/{OutputBaseDir}/components/{Name}/{Name}.gen.yaml
OutputBaseDir string `cue:"string | *\"\""`
// Resources represents kubernetes resources mixed into the rendered manifest.
Resources core.Resources
// KustomizeConfig represents the configuration kustomize.
KustomizeConfig KustomizeConfig
// Artifacts represents additional artifacts to mix in. Useful for adding
// GitOps resources. Each Artifact is unified without modification into the
// BuildPlan.
Artifacts map[NameLabel]core.Artifact
}
```
<a name="Helm"></a>
## type Helm {#Helm}
Helm assembles a BuildPlan rendering a helm chart. Useful to mix in additional resources from CUE and transform the helm output with kustomize.
```go
type Helm struct {
ComponentConfig `json:",inline"`
// Chart represents a Helm chart.
Chart core.Chart
// Values represents data to marshal into a values.yaml for helm.
Values core.Values
// EnableHooks enables helm hooks when executing the `helm template` command.
EnableHooks bool `cue:"true | *false"`
// Namespace sets the helm chart namespace flag if provided.
Namespace string `json:",omitempty"`
// BuildPlan represents the derived BuildPlan produced for the holos render
// component command.
BuildPlan core.BuildPlan
}
```
<a name="Kubernetes"></a>
## type Kubernetes {#Kubernetes}
Kubernetes assembles a BuildPlan containing inline resources exported from CUE.
```go
type Kubernetes struct {
ComponentConfig `json:",inline"`
// BuildPlan represents the derived BuildPlan produced for the holos render
// component command.
BuildPlan core.BuildPlan
}
```
<a name="Kustomize"></a>
## type Kustomize {#Kustomize}
Kustomize assembles a BuildPlan rendering manifests from a [kustomize](<https://kubectl.docs.kubernetes.io/references/kustomize/kustomization/>) kustomization.
```go
type Kustomize struct {
ComponentConfig `json:",inline"`
// BuildPlan represents the derived BuildPlan produced for the holos render
// component command.
BuildPlan core.BuildPlan
}
```
<a name="KustomizeConfig"></a>
## type KustomizeConfig {#KustomizeConfig}
KustomizeConfig represents the configuration for [kustomize](<https://kubectl.docs.kubernetes.io/references/kustomize/kustomization/>) post processing. Use the Files field to mix in plain manifest files located in the component directory. Use the Resources field to mix in manifests from network urls.
```go
type KustomizeConfig struct {
// Kustomization represents the kustomization used to transform resources.
// Note the resources field is internally managed from the Files and Resources fields.
Kustomization map[string]any `json:",omitempty"`
// Files represents files to copy from the component directory for kustomization.
Files map[string]struct{ Source string } `cue:"{[NAME=_]: Source: NAME}"`
// Resources represents additional entries to included in the resources list.
Resources map[string]struct{ Source string } `cue:"{[NAME=_]: Source: NAME}"`
// CommonLabels represents common labels added without including selectors.
CommonLabels map[string]string
}
```
<a name="NameLabel"></a>
## type NameLabel {#NameLabel}
NameLabel represents the common use case of converting a struct to a list where the name field of each value unifies with the field name of the outer struct.
For example:
```
S: [NameLabel=string]: name: NameLabel
S: jeff: _
S: gary: _
S: nate: _
L: [for x in S {x}]
// L is [{name: "jeff"}, {name: "gary"}, {name: "nate"}]
```
```go
type NameLabel string
```
<a name="Platform"></a>
## type Platform {#Platform}
Platform assembles a core Platform in the Resource field for the holos render platform command. Use the Components field to register components with the platform.
```go
type Platform struct {
Name string
Components map[NameLabel]core.Component
Resource core.Platform
}
```
Generated by [gomarkdoc](<https://github.com/princjef/gomarkdoc>)
v1alpha5

View File

@@ -1,412 +1,3 @@
---
title: Core Schemas
description: BuildPlan defines the holos rendering pipeline.
sidebar_position: 100
---
<!-- Code generated by gomarkdoc. DO NOT EDIT -->
# Core Schema
```go
import "github.com/holos-run/holos/api/core/v1alpha5"
```
Package core contains schemas for a [Platform](<#Platform>) and [BuildPlan](<#BuildPlan>). Holos takes a [Platform](<#Platform>) as input, then iterates over each [Component](<#Component>) to produce a [BuildPlan](<#BuildPlan>). Holos processes the [BuildPlan](<#BuildPlan>) to produce fully rendered manifests, each an [Artifact](<#Artifact>).
## Index
- [type Artifact](<#Artifact>)
- [type BuildPlan](<#BuildPlan>)
- [type BuildPlanSource](<#BuildPlanSource>)
- [type BuildPlanSpec](<#BuildPlanSpec>)
- [type Chart](<#Chart>)
- [type Component](<#Component>)
- [type File](<#File>)
- [type FileContent](<#FileContent>)
- [type FileContentMap](<#FileContentMap>)
- [type FilePath](<#FilePath>)
- [type Generator](<#Generator>)
- [type Helm](<#Helm>)
- [type InternalLabel](<#InternalLabel>)
- [type Join](<#Join>)
- [type Kind](<#Kind>)
- [type Kustomization](<#Kustomization>)
- [type Kustomize](<#Kustomize>)
- [type Metadata](<#Metadata>)
- [type Platform](<#Platform>)
- [type PlatformSpec](<#PlatformSpec>)
- [type Repository](<#Repository>)
- [type Resource](<#Resource>)
- [type Resources](<#Resources>)
- [type Transformer](<#Transformer>)
- [type Values](<#Values>)
<a name="Artifact"></a>
## type Artifact {#Artifact}
Artifact represents one fully rendered manifest produced by a [Transformer](<#Transformer>) sequence, which transforms a [Generator](<#Generator>) collection. A [BuildPlan](<#BuildPlan>) produces an [Artifact](<#Artifact>) collection.
Each Artifact produces one manifest file artifact. Generator Output values are used as Transformer Inputs. The Output field of the final [Transformer](<#Transformer>) should have the same value as the Artifact field.
When there is more than one [Generator](<#Generator>) there must be at least one [Transformer](<#Transformer>) to combine outputs into one Artifact. If there is a single Generator, it may directly produce the Artifact output.
An Artifact is processed concurrently with other artifacts in the same [BuildPlan](<#BuildPlan>). An Artifact should not use an output from another Artifact as an input. Each [Generator](<#Generator>) may also run concurrently. Each [Transformer](<#Transformer>) is executed sequentially starting after all generators have completed.
Output fields are write\-once. It is an error for multiple Generators or Transformers to produce the same Output value within the context of a [BuildPlan](<#BuildPlan>).
```go
type Artifact struct {
Artifact FilePath `json:"artifact,omitempty"`
Generators []Generator `json:"generators,omitempty"`
Transformers []Transformer `json:"transformers,omitempty"`
Skip bool `json:"skip,omitempty"`
}
```
<a name="BuildPlan"></a>
## type BuildPlan {#BuildPlan}
BuildPlan represents an implementation of the [rendered manifest pattern](<https://akuity.io/blog/the-rendered-manifests-pattern>). Holos processes a BuildPlan to produce one or more [Artifact](<#Artifact>) output files. BuildPlan artifact files usually contain Kubernetes manifests, but they may have any content.
A BuildPlan usually produces two artifacts. One artifact contains a manifest of resources. A second artifact contains a GitOps resource to manage the first, usually an ArgoCD Application resource.
Holos uses CUE to construct a BuildPlan. A future enhancement will support user defined executables providing a BuildPlan to Holos in the style of an [external credential provider](<https://github.com/kubernetes/enhancements/blob/313ad8b59c80819659e1fbf0f165230f633f2b22/keps/sig-auth/541-external-credential-providers/README.md>).
```go
type BuildPlan struct {
// Kind represents the type of the resource.
Kind string `json:"kind" cue:"\"BuildPlan\""`
// APIVersion represents the versioned schema of the resource.
APIVersion string `json:"apiVersion" cue:"string | *\"v1alpha5\""`
// Metadata represents data about the resource such as the Name.
Metadata Metadata `json:"metadata"`
// Spec specifies the desired state of the resource.
Spec BuildPlanSpec `json:"spec"`
// Source reflects the origin of the BuildPlan.
Source BuildPlanSource `json:"source,omitempty"`
}
```
<a name="BuildPlanSource"></a>
## type BuildPlanSource {#BuildPlanSource}
BuildPlanSource reflects the origin of a [BuildPlan](<#BuildPlan>). Useful to save a build plan to a file, then re\-generate it without needing to process a [Platform](<#Platform>) component collection.
```go
type BuildPlanSource struct {
// Component reflects the component that produced the build plan.
Component Component `json:"component,omitempty"`
}
```
<a name="BuildPlanSpec"></a>
## type BuildPlanSpec {#BuildPlanSpec}
BuildPlanSpec represents the specification of the [BuildPlan](<#BuildPlan>).
```go
type BuildPlanSpec struct {
// Artifacts represents the artifacts for holos to build.
Artifacts []Artifact `json:"artifacts"`
// Disabled causes the holos cli to disregard the build plan.
Disabled bool `json:"disabled,omitempty"`
}
```
<a name="Chart"></a>
## type Chart {#Chart}
Chart represents a [Helm](<#Helm>) Chart.
```go
type Chart struct {
// Name represents the chart name.
Name string `json:"name"`
// Version represents the chart version.
Version string `json:"version"`
// Release represents the chart release when executing helm template.
Release string `json:"release"`
// Repository represents the repository to fetch the chart from.
Repository Repository `json:"repository,omitempty"`
}
```
<a name="Component"></a>
## type Component {#Component}
Component represents the complete context necessary to produce a [BuildPlan](<#BuildPlan>) from a path containing parameterized CUE configuration.
```go
type Component struct {
// Name represents the name of the component. Injected as the tag variable
// "holos_component_name".
Name string `json:"name"`
// Path represents the path of the component relative to the platform root.
// Injected as the tag variable "holos_component_path".
Path string `json:"path"`
// 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"`
// Parameters represent user defined input variables to produce various
// [BuildPlan] resources from one component path. Injected as CUE @tag
// variables. Parameters with a "holos_" prefix are reserved for use by the
// Holos Authors. Multiple environments are a prime example of an input
// parameter that should always be user defined, never defined by Holos.
Parameters map[string]string `json:"parameters,omitempty"`
}
```
<a name="File"></a>
## type File {#File}
File represents a simple single file copy [Generator](<#Generator>). Useful with a [Kustomize](<#Kustomize>) [Transformer](<#Transformer>) to process plain manifest files stored in the component directory. Multiple File generators may be used to transform multiple resources.
```go
type File struct {
// Source represents a file sub-path relative to the component path.
Source FilePath `json:"source"`
}
```
<a name="FileContent"></a>
## type FileContent {#FileContent}
FileContent represents file contents.
```go
type FileContent string
```
<a name="FileContentMap"></a>
## type FileContentMap {#FileContentMap}
FileContentMap represents a mapping of file paths to file contents.
```go
type FileContentMap map[FilePath]FileContent
```
<a name="FilePath"></a>
## type FilePath {#FilePath}
FilePath represents a file path.
```go
type FilePath string
```
<a name="Generator"></a>
## type Generator {#Generator}
Generator generates Kubernetes resources. [Helm](<#Helm>) and [Resources](<#Resources>) are the most commonly used, often paired together to mix\-in resources to an unmodified Helm chart. A simple [File](<#File>) generator is also available for use with the [Kustomize](<#Kustomize>) transformer.
Each Generator in an [Artifact](<#Artifact>) must have a distinct Output value for a [Transformer](<#Transformer>) to reference.
1. [Resources](<#Resources>) \- Generates resources from CUE code.
2. [Helm](<#Helm>) \- Generates rendered yaml from a [Chart](<#Chart>).
3. [File](<#File>) \- Generates data by reading a file from the component directory.
```go
type Generator struct {
// Kind represents the kind of generator. Must be Resources, Helm, or File.
Kind string `json:"kind" cue:"\"Resources\" | \"Helm\" | \"File\""`
// Output represents a file for a Transformer or Artifact to consume.
Output FilePath `json:"output"`
// Resources generator. Ignored unless kind is Resources. Resources are
// stored as a two level struct. The top level key is the Kind of resource,
// e.g. Namespace or Deployment. The second level key is an arbitrary
// InternalLabel. The third level is a map[string]any representing the
// Resource.
Resources Resources `json:"resources,omitempty"`
// Helm generator. Ignored unless kind is Helm.
Helm Helm `json:"helm,omitempty"`
// File generator. Ignored unless kind is File.
File File `json:"file,omitempty"`
}
```
<a name="Helm"></a>
## type Helm {#Helm}
Helm represents a [Chart](<#Chart>) manifest [Generator](<#Generator>).
```go
type Helm struct {
// Chart represents a helm chart to manage.
Chart Chart `json:"chart"`
// Values represents values for holos to marshal into values.yaml when
// rendering the chart.
Values Values `json:"values"`
// EnableHooks enables helm hooks when executing the `helm template` command.
EnableHooks bool `json:"enableHooks,omitempty"`
// Namespace represents the helm namespace flag
Namespace string `json:"namespace,omitempty"`
}
```
<a name="InternalLabel"></a>
## type InternalLabel {#InternalLabel}
InternalLabel is an arbitrary unique identifier internal to holos itself. The holos cli is expected to never write a InternalLabel value to rendered output files, therefore use a InternalLabel when the identifier must be unique and internal. Defined as a type for clarity and type checking.
```go
type InternalLabel string
```
<a name="Join"></a>
## type Join {#Join}
Join represents a [Transformer](<#Transformer>) using [bytes.Join](<https://pkg.go.dev/bytes#Join>) to concatenate multiple inputs into one output with a separator. Useful for combining output from [Helm](<#Helm>) and [Resources](<#Resources>) together into one [Artifact](<#Artifact>) when [Kustomize](<#Kustomize>) is otherwise unnecessary.
```go
type Join struct {
Separator string `json:"separator" cue:"string | *\"---\\n\""`
}
```
<a name="Kind"></a>
## type Kind {#Kind}
Kind is a discriminator. Defined as a type for clarity and type checking.
```go
type Kind string
```
<a name="Kustomization"></a>
## type Kustomization {#Kustomization}
Kustomization represents a kustomization.yaml file for use with the [Kustomize](<#Kustomize>) [Transformer](<#Transformer>). Untyped to avoid tightly coupling holos to kubectl versions which was a problem for the Flux maintainers. Type checking is expected to happen in CUE against the kubectl version the user prefers.
```go
type Kustomization map[string]any
```
<a name="Kustomize"></a>
## type Kustomize {#Kustomize}
Kustomize represents a kustomization [Transformer](<#Transformer>).
```go
type Kustomize struct {
// Kustomization represents the decoded kustomization.yaml file
Kustomization Kustomization `json:"kustomization"`
// Files holds file contents for kustomize, e.g. patch files.
Files FileContentMap `json:"files,omitempty"`
}
```
<a name="Metadata"></a>
## type Metadata {#Metadata}
Metadata represents data about the resource such as the Name.
```go
type Metadata struct {
// Name represents the resource name.
Name string `json:"name"`
}
```
<a name="Platform"></a>
## type Platform {#Platform}
Platform represents a platform to manage. A Platform specifies a [Component](<#Component>) collection and integrates the components together into a holistic platform. Holos iterates over the [Component](<#Component>) collection producing a [BuildPlan](<#BuildPlan>) for each, which holos then executes to render manifests.
Inspect a Platform resource holos would process by executing:
```
cue export --out yaml ./platform
```
```go
type Platform struct {
// Kind is a string value representing the resource.
Kind string `json:"kind" cue:"\"Platform\""`
// APIVersion represents the versioned schema of this resource.
APIVersion string `json:"apiVersion" cue:"string | *\"v1alpha5\""`
// Metadata represents data about the resource such as the Name.
Metadata Metadata `json:"metadata"`
// Spec represents the platform specification.
Spec PlatformSpec `json:"spec"`
}
```
<a name="PlatformSpec"></a>
## type PlatformSpec {#PlatformSpec}
PlatformSpec represents the platform specification.
```go
type PlatformSpec struct {
// Components represents a collection of holos components to manage.
Components []Component `json:"components"`
}
```
<a name="Repository"></a>
## type Repository {#Repository}
Repository represents a [Helm](<#Helm>) [Chart](<#Chart>) repository.
```go
type Repository struct {
Name string `json:"name"`
URL string `json:"url"`
}
```
<a name="Resource"></a>
## type Resource {#Resource}
Resource represents one kubernetes api object.
```go
type Resource map[string]any
```
<a name="Resources"></a>
## type Resources {#Resources}
Resources represents Kubernetes resources. Most commonly used to mix resources into the [BuildPlan](<#BuildPlan>) generated from CUE, but may be generated from elsewhere.
```go
type Resources map[Kind]map[InternalLabel]Resource
```
<a name="Transformer"></a>
## type Transformer {#Transformer}
Transformer combines multiple inputs from prior [Generator](<#Generator>) or [Transformer](<#Transformer>) outputs into one output. [Kustomize](<#Kustomize>) is the most commonly used transformer. A simple [Join](<#Join>) is also supported for use with plain manifest files.
1. [Kustomize](<#Kustomize>) \- Patch and transform the output from prior generators or transformers. See [Introduction to Kustomize](<https://kubectl.docs.kubernetes.io/guides/config_management/introduction/>).
2. [Join](<#Join>) \- Concatenate multiple prior outputs into one output.
```go
type Transformer struct {
// Kind represents the kind of transformer. Must be Kustomize, or Join.
Kind string `json:"kind" cue:"\"Kustomize\" | \"Join\""`
// Inputs represents the files to transform. The Output of prior Generators
// and Transformers.
Inputs []FilePath `json:"inputs"`
// Output represents a file for a subsequent Transformer or Artifact to
// consume.
Output FilePath `json:"output"`
// Kustomize transformer. Ignored unless kind is Kustomize.
Kustomize Kustomize `json:"kustomize,omitempty"`
// Join transformer. Ignored unless kind is Join.
Join Join `json:"join,omitempty"`
}
```
<a name="Values"></a>
## type Values {#Values}
Values represents [Helm](<#Helm>) Chart values generated from CUE.
```go
type Values map[string]any
```
Generated by [gomarkdoc](<https://github.com/princjef/gomarkdoc>)
v1alpha5

View File

@@ -1,31 +0,0 @@
```mermaid
---
title: holos render component
---
sequenceDiagram
participant HRC as holos<br />render component
participant CUE as CUE<br />(embedded)
participant G as Generator<br />(e.g. Helm)
participant T as Transformer<br />(e.g. Kustomize)
participant V as Validator<br />(e.g. CUE)
participant M as Manifests
HRC ->>+ CUE: Get apiVersion
HRC ->>+ CUE: Get BuildPlan
loop For each Artifact in BuildPlan concurrently
loop For each Generator in Artifact concurrently
HRC ->>+ G: Generate Config
G ->>+ HRC: Config
end
loop For each Transformer in Artifact sequentially
HRC ->>+ T: Transform Config
T ->>+ HRC: Config
end
loop For each Validator in Artifact concurrently
HRC ->>+ V: Validate Config
V ->>+ HRC: Valid / Invalid
end
HRC ->>+ M: Write Artifact File
end
Note over M: Ready for deployment
```

View File

@@ -1,22 +0,0 @@
```mermaid
---
title: holos render platform
---
sequenceDiagram
participant HRP as holos<br />render platform
participant HRC as holos<br />render component
participant CUE as CUE<br />(embedded)
participant A as Artifacts
participant M as Manifests
HRP ->>+ CUE: Get apiVersion
HRP ->>+ CUE: Get Platform Definition
loop For each Component in Platform concurrently
HRP ->>+ HRC: Execute
HRC ->>+ CUE: Get apiVersion
HRC ->>+ CUE: Get BuildPlan
HRC ->>+ A: Build Artifacts
A ->>+ HRC: Manifests
HRC ->>+ M: Write Manifest Files
end
Note over M: Ready for deployment
```

View File

@@ -1,32 +0,0 @@
```mermaid
---
title: Rendering Overview
---
graph LR
Platform[<a href="/docs/api/author/v1alpha4/#Platform">Platform</a>]
Component[<a href="/docs/api/author/v1alpha4/#ComponentConfig">Components</a>]
Helm[<a href="/docs/api/author/v1alpha4/#Helm">Helm</a>]
Kustomize[<a href="/docs/api/author/v1alpha4/#Kustomize">Kustomize</a>]
Kubernetes[<a href="/docs/api/author/v1alpha4/#Kubernetes">Kubernetes</a>]
BuildPlan[<a href="/docs/api/core/v1alpha4/#buildplan">BuildPlan</a>]
ResourcesArtifact[<a href="/docs/api/core/v1alpha4/#artifact">Resources<br/>Artifact</a>]
GitOpsArtifact[<a href="/docs/api/core/v1alpha4/#artifact">GitOps<br/>Artifact</a>]
Generators[<a href="/docs/api/core/v1alpha4/#generators">Generators</a>]
Transformers[<a href="/docs/api/core/v1alpha4/#transformer">Transformers</a>]
Validators[Validators<br/>TBD]
Files[Manifest<br/>Files]
Platform --> Component
Component --> Helm --> BuildPlan
Component --> Kubernetes --> BuildPlan
Component --> Kustomize --> BuildPlan
BuildPlan --> ResourcesArtifact --> Generators
BuildPlan --> GitOpsArtifact --> Generators
Generators --> Transformers --> Validators --> Files
```

View File

@@ -0,0 +1 @@
# Multi Cluster

View File

View File

View File

@@ -0,0 +1,272 @@
---
description: Build a local Cluster to use with these guides.
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import Admonition from '@theme/Admonition';
# Local Cluster
In this guide we'll set up a local k3d cluster to apply and explore the
configuration described in our other guides. After completing this guide you'll
have a standard Kubernetes API server with proper DNS and TLS certificates.
You'll be able to easily reset the cluster to a known good state to iterate on
your own Platform.
## Reset the Cluster
If you've already followed this guide, reset the cluster by running the
following commands. Skip this section if you're creating a cluster for the
first time.
First, delete the cluster.
<Tabs groupId="k3d-cluster-delete">
<TabItem value="command" label="Command">
```bash
k3d cluster delete workload
```
</TabItem>
<TabItem value="output" label="Output">
```txt showLineNumbers
INFO[0000] Deleting cluster 'workload'
INFO[0000] Deleting cluster network 'k3d-workload'
INFO[0000] Deleting 1 attached volumes...
INFO[0000] Removing cluster details from default kubeconfig...
INFO[0000] Removing standalone kubeconfig file (if there is one)...
INFO[0000] Successfully deleted cluster workload!
```
</TabItem>
</Tabs>
Then create the cluster again.
<Tabs groupId="k3d-cluster-create">
<TabItem value="command" label="Command">
```bash
k3d cluster create workload \
--registry-use k3d-registry.holos.localhost:5100 \
--port "443:443@loadbalancer" \
--k3s-arg "--disable=traefik@server:0"
```
</TabItem>
<TabItem value="output" label="Output">
```txt showLineNumbers
INFO[0000] portmapping '443:443' targets the loadbalancer: defaulting to [servers:*:proxy agents:*:proxy]
INFO[0000] Prep: Network
INFO[0000] Created network 'k3d-workload'
INFO[0000] Created image volume k3d-workload-images
INFO[0000] Starting new tools node...
INFO[0000] Starting node 'k3d-workload-tools'
INFO[0001] Creating node 'k3d-workload-server-0'
INFO[0001] Creating LoadBalancer 'k3d-workload-serverlb'
INFO[0001] Using the k3d-tools node to gather environment information
INFO[0001] HostIP: using network gateway 172.17.0.1 address
INFO[0001] Starting cluster 'workload'
INFO[0001] Starting servers...
INFO[0001] Starting node 'k3d-workload-server-0'
INFO[0003] All agents already running.
INFO[0003] Starting helpers...
INFO[0003] Starting node 'k3d-workload-serverlb'
INFO[0009] Injecting records for hostAliases (incl. host.k3d.internal) and for 3 network members into CoreDNS configmap...
INFO[0012] Cluster 'workload' created successfully!
INFO[0012] You can now use it like this:
kubectl cluster-info
```
</TabItem>
</Tabs>
Finally, add your trusted certificate authority.
<Tabs groupId="apply-local-ca">
<TabItem value="command" label="Command">
```bash
kubectl apply --server-side=true -f "$(mkcert -CAROOT)/namespace.yaml"
kubectl apply --server-side=true -n cert-manager -f "$(mkcert -CAROOT)/local-ca.yaml"
```
</TabItem>
<TabItem value="output" label="Output">
```txt showLineNumbers
namespace/cert-manager serverside-applied
secret/local-ca serverside-applied
```
</TabItem>
</Tabs>
You're back to the same state as the first time you completed this guide.
## What you'll need {#requirements}
You'll need the following tools installed to complete this guide.
1. [holos](/docs/install) - to build the platform.
2. [helm](https://helm.sh/docs/intro/install/) - to render Holos components that wrap upstream Helm charts.
3. [k3d](https://k3d.io/#installation) - to provide a k8s api server.
4. [OrbStack](https://docs.orbstack.dev/install) or [Docker](https://docs.docker.com/get-docker/) - to use k3d.
5. [kubectl](https://kubernetes.io/docs/tasks/tools/) - to interact with the k8s api server.
6. [mkcert](https://github.com/FiloSottile/mkcert?tab=readme-ov-file#installation) - to make trusted TLS certificates.
7. [jq](https://jqlang.github.io/jq/download/) - to fiddle with JSON output.
## Configure DNS {#configure-dns}
Configure your machine to resolve `*.holos.localhost` to your loopback
interface. This is necessary for requests to reach the workload cluster. Save
this script to a file and execute it.
```bash showLineNumbers
#! /bin/bash
#
set -euo pipefail
tmpdir="$(mktemp -d)"
finish() {
[[ -d "$tmpdir" ]] && rm -rf "$tmpdir"
}
trap finish EXIT
cd "$tmpdir"
brew install dnsmasq
cat <<EOF >"$(brew --prefix)/etc/dnsmasq.d/holos.localhost.conf"
# Refer to https://holos.run/docs/tutorial/local/k3d/
address=/holos.localhost/127.0.0.1
EOF
if [[ -r /Library/LaunchDaemons/homebrew.mxcl.dnsmasq.plist ]]; then
echo "dnsmasq already configured"
else
sudo cp "$(brew list dnsmasq | grep 'dnsmasq.plist$')" \
/Library/LaunchDaemons/homebrew.mxcl.dnsmasq.plist
sudo launchctl unload /Library/LaunchDaemons/homebrew.mxcl.dnsmasq.plist
sudo launchctl load /Library/LaunchDaemons/homebrew.mxcl.dnsmasq.plist
dscacheutil -flushcache
echo "dnsmasq configured"
fi
sudo mkdir -p /etc/resolver
sudo tee /etc/resolver/holos.localhost <<EOF
domain holos.localhost
nameserver 127.0.0.1
EOF
sudo killall -HUP mDNSResponder
echo "all done."
```
## Create the Cluster {#create-the-cluster}
The Workload Cluster is where your applications and services will be deployed.
In production this is usually an EKS, GKE, or AKS cluster.
:::tip
Holos supports all compliant Kubernetes clusters. Holos was developed and tested
on GKE, EKS, Talos, k3s, and Kubeadm clusters.
:::
Create a local registry to speed up image builds and pulls.
```bash
k3d registry create registry.holos.localhost --port 5100
```
Create the workload cluster configured to use the local registry.
```bash
k3d cluster create workload \
--registry-use k3d-registry.holos.localhost:5100 \
--port "443:443@loadbalancer" \
--k3s-arg "--disable=traefik@server:0"
```
Traefik is disabled because Istio provides the same functionality.
## Setup Root CA {#setup-root-ca}
Platforms most often use cert-manager to issue tls certificates. The browser
and tools we're using need to trust these certificates to work together.
Generate a local, trusted root certificate authority with the following script.
Admin access is necessary for `mkcert` to manage the certificate into your trust
stores.
```bash
sudo -v
```
Manage the local CA and copy the CA key to the workload cluster so that cert
manager can manage trusted certificates.
Save this script to a file and execute it to configure a trusted certificate
authority.
```bash showLineNumbers
#! /bin/bash
#
set -euo pipefail
mkcert --install
tmpdir="$(mktemp -d)"
finish() {
[[ -d "$tmpdir" ]] && rm -rf "$tmpdir"
}
trap finish EXIT
cd "$tmpdir"
# Create the local CA Secret with ca.crt, tls.crt, tls.key
mkdir local-ca
cd local-ca
CAROOT="$(mkcert -CAROOT)"
cp -p "${CAROOT}/rootCA.pem" ca.crt
cp -p "${CAROOT}/rootCA.pem" tls.crt
cp -p "${CAROOT}/rootCA-key.pem" tls.key
kubectl create secret generic --from-file=. --dry-run=client -o yaml local-ca > ../local-ca.yaml
echo 'type: kubernetes.io/tls' >> ../local-ca.yaml
cd ..
cat <<EOF > namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
labels:
kubernetes.io/metadata.name: cert-manager
name: cert-manager
spec:
finalizers:
- kubernetes
EOF
kubectl apply --server-side=true -f namespace.yaml
kubectl apply -n cert-manager --server-side=true -f local-ca.yaml
# Save the Secret to easily reset the cluster later.
install -m 0644 namespace.yaml "${CAROOT}/namespace.yaml"
install -m 0600 local-ca.yaml "${CAROOT}/local-ca.yaml"
```
:::warning
Take care to run the local-ca script each time you create the workload cluster
so that Certificates are issued correctly.
:::
## Clean Up {#clean-up}
If you'd like to clean up the resources you created in this guide, remove them
with:
```bash
k3d cluster delete workload
```
## Next Steps
Now that you have a real cluster, continue your Holos journey by following the
[Tutorial](../tutorial/1-overview.mdx) and setting up your own Platform.

View File

@@ -1,12 +0,0 @@
---
description: Re-use components by passing in parameters.
slug: component-parameters
sidebar_position: 400
---
# Component Parameters
Key points:
1. Components can be reused.
2. The Platform spec can pass user defined parameters to a component.

View File

@@ -1,12 +0,0 @@
---
description: Organize clusters into fleets.
slug: fleets
sidebar_position: 300
---
# Fleets
Key points:
1. Workload fleet.
2. Management fleet.

View File

@@ -1,15 +0,0 @@
---
description: Management Cluster
slug: management-cluster
sidebar_position: 200
---
# Management Cluster
Key points:
1. Namespaces
2. Certificates
3. Secrets
4. CronJobs
5. GKE autopilot

View File

@@ -1,12 +0,0 @@
---
description: Secrets Management
slug: secrets-management
sidebar_position: 150
---
# Secrets Management
Key points:
1. Namespaces
2. ExternalSecrets

View File

@@ -1,12 +0,0 @@
---
description: Workload Cluster
slug: workload-cluster
sidebar_position: 250
---
# Workload Cluster
Key points:
1. Namespaces
2. ExternalSecrets

View File

@@ -0,0 +1,94 @@
# Overview
Holos is an open-source tool that simplifies software integration for platform
teams. While most Kubernetes tools focus on application management, Holos takes
a holistic infrastructure-as-code (IaC) approach, targeting the integration
layer where applications and organizational data converge. By providing
well-defined, typed structures for consistent validation, Holos reduces errors,
streamlines integration, and creates clear pathways for teams to easily
integrate their services.
# How Holos works
Holos implements the [rendered manifests pattern][rmp], generating fully
rendered Kubernetes manifests from [CUE language][CUE] abstractions called
Components. These Components can model [Helm charts][Helm], [Kustomize
bases][Kustomize], or native Kubernetes resources. A Holos Platform consists of
one or more Components and is applied to one or more Kubernetes clusters.
```mermaid
graph BT
Platform[Platform]
Cluster[Cluster]
Component[Component]
Helm[Helm]
Kustomize[Kustomize]
CUE[CUE]
Platform --> Cluster
Component --> Platform
Helm --> Component
Kustomize --> Component
CUE --> Component
```
# Holos' role in your workflow
Holos generates, but does not apply, rendered manifests, allowing teams to use
`git diff` to clearly see changes and assess the impact on clusters and services
before deploying. Instead, Holos manages the GitOps configuration for tools like
ArgoCD and Flux CD, enabling continuous deployment of resource changes with the
full visibility and control that these tools provide. As a result, expect Holos
to sit at the very end of a CI pipeline.
# Advantages of using Holos
### Safety
* CUE constraints on Holos Components surface validation errors early in the
development process, reducing the number of failed deployments and the time
spent troubleshooting them.
* Holos natively provides a "blast radius" to code
changes by identifying the rendered manifests across your fleet of Kubernetes
clusters that will be affected by the change.
* CUE's unification strategy allows multiples teams to contribute to the desired
state of a service:
* The Platform team provides definitions for shared resources.
* Engineering teams populate definitions with service-specific data.
* The Security team provides concrete values that cannot be changed to harden the company's security posture.
### Flexibility
* CUE is adept at modeling variation and organizational complexity at scale,
while Holos enables seamless integration of CUE data with native Kubernetes
tools such as [Helm][Helm] and [Kustomize][Kustomize].
* Holos is extensible, allowing Holos Components to be modeled from any tool that
generates (e.g. `helm`) or transforms (e.g. `kustomize`) manifest data.
### Consistency
* Holos manages the execution context for Helm and Kustomize, ensuring that
rendered manifests are consistently and reliably reproducible, no matter where
Holos is run.
* CUE constraints ensure that data abstractions include the required information
in the expected format, triggering early failures if any required data is
missing.
# When not to use Holos
Holos excels at configuration management and is most effective when integrated
into a broader Kubernetes deployment strategy. If you need a single tool to
manage the entire lifecycle of a Kubernetes cluster, Holos may not fully meet
your needs on its own.
# Next Steps
Now that you have a base understanding of how Holos works, continue
to the [Setup page](./2-setup.mdx) that guides you through installing Holos and
initializing your first Platform.
[rmp]: https://akuity.io/blog/the-rendered-manifests-pattern
[Helm]: https://helm.sh/
[Kustomize]: https://kustomize.io/
[CUE]: https://cuelang.org/

View File

@@ -0,0 +1,85 @@
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
# Setup
This tutorial will guide you through the installation of Holos and its
dependencies, as well as the initialization of a minimal Platform that you can
extend to meet your specific needs.
# Prerequisites
Holos integrates with the following tools that should be installed to enable
their functionality.
* [Helm][helm] to fetch and render Helm chart Components
* [Kubectl][kubectl] to [kustomize][kustomize] components.
# Install Holos
Holos is distributed as a single file executable that can be installed in a couple of ways.
### Releases
Download `holos` from the [releases](https://github.com/holos-run/holos/releases) page and place the executable into your shell path.
### Go install
Alternatively, install directly into your go bin path using:
```shell
go install github.com/holos-run/holos/cmd/holos@latest
```
# Do I need a Kubernetes cluster?
Holos only generates rendered Kubernetes manifests, so you don't need a
Kubernetes cluster to start using the tool or understand its workflow. However,
you will need a cluster eventually to apply the rendered manifests and verify
that they achieve the desired end state.
We recommend using [k3d](https://k3d.io/) to set up a minimal Kubernetes cluster with
[Orbstack](https://docs.orbstack.dev/install) or
[Docker](https://docs.docker.com/get-started/get-docker/). To simplify this,
we've created [the Local Cluster guide](../topics/4-local-cluster.mdx) which
automates the process, ensuring proper DNS and TLS certificates, and includes a
script to reset the cluster to a known good state.
When you're ready to experiment with a live Kubernetes cluster, refer to [the
Local Cluster guide](../topics/4-local-cluster.mdx).
# Initialize a new Platform
Use Holos to generate a minimal platform using the recommended directory structure
by creating an empty directory and then using the `holos generate platform` subcommand:
<Tabs groupId="tutorial-setup-generate-platform">
<TabItem value="command" label="Command">
```bash
mkdir holos_platform
cd holos_platform
holos generate platform v1alpha4
```
</TabItem>
<TabItem value="output" label="Output">
```txt
no output
```
</TabItem>
</Tabs>
Holos creates a `platform` directory with a `platform.gen.cue` file that serves
as the foundation for your newly initialized Holos Platform. For each Holos
Component you model, you'll add a new CUE file to the platform directory,
mapping it to the location of the Component's CUE code, and extending the
capabilities of your platform.
# Next Steps
Now that you've got Holos and its prerequisites installed, continue on to the
[Hello Holos page](./3-hello-holos.mdx) where we will guide you through the
process of creating your first Component and modeling a Helm chart.
[helm]: https://github.com/helm/helm/releases
[kubectl]: https://kubernetes.io/docs/tasks/tools/
[kustomize]: https://kustomize.io/

View File

@@ -0,0 +1,74 @@
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
# Hello Holos
One of the first exercises you perform when learning a new programming language
is to print out the "Hello World!" greeting. For Holos, our "Hello Holos"
exercise involves modeling the [podinfo Helm chart][podinfo] that produces a
similar greeting message from a Kubernetes Pod.
By the end of this tutorial you will gain the understanding of how to model
an individual Holos Component using a Helm chart as its source.
# The code
### Podinfo Helm Chart
Let's start by creating a directory for `podinfo`, touching an empty CUE file,
and then populating it with the CUE code below:
<Tabs groupId="tutorial-hello-podinfo-helm-cue-code">
<TabItem value="projects/tutorial/components/podinfo/podinfo.cue" label="Podinfo Helm Chart">
```bash
mkdir -p projects/tutorial/components/podinfo
touch projects/tutorial/components/podinfo/podinfo.cue
```
```cue showLineNumbers
package holos
// Produce a helm chart build plan.
_HelmChart.BuildPlan
_HelmChart: #Helm & {
Name: "podinfo"
Chart: {
version: "6.6.2"
repository: {
name: "podinfo"
url: "https://stefanprodan.github.io/podinfo"
}
}
}
```
</TabItem>
</Tabs>
### Register the podinfo component
Now let's register the code modeling the `podinfo` Helm chart as a Holos
Component and assign it to a cluser. For this we will need a create new file in
the `platform` directory with the following contents:
<Tabs groupId="tutorial-hello-register-podinfo-component">
<TabItem value="platform/podinfo.cue" label="Register Podinfo">
```bash
touch plaform/podinfo.cue
```
```cue showLineNumbers
_Platform: Components: podinfo: {
name: "podinfo"
component: "projects/tutorial/components/podinfo"
cluster: "local"
}
```
</TabItem>
</Tabs>
[podinfo]: https://github.com/stefanprodan/podinfo

View File

@@ -0,0 +1 @@
# Helm

View File

@@ -0,0 +1 @@
# Kustomize

View File

@@ -0,0 +1 @@
# CUE

View File

@@ -0,0 +1 @@
# Mix Ins

View File

@@ -0,0 +1 @@
# Core API

View File

@@ -0,0 +1 @@
# Author API

View File

@@ -1,15 +0,0 @@
---
slug: cue-generator
title: CUE Generator
description: Render component manifests directly from CUE.
sidebar_position: 50
---
# CUE
Key points to cover:
1. Resources are validated against `#Resources` defined at the root.
2. Custom Resource Definitions need to be imported with timoni.
3. One component can have multiple generators, e.g. Helm and CUE together. This
is how resources are mixed in to helm charts.

View File

@@ -1,13 +0,0 @@
---
slug: Custom Schemas
title: Custom Schemas
description: Define your own custom data structures.
sidebar_position: 70
---
# Custom Schemas
- Work through defining a `#Cluster` schema and a `Clusters` struct.
- Direct the reader to [topics] for more recipes.
[topics]: ../topics.mdx

View File

@@ -1,14 +0,0 @@
---
slug: file-generator
title: File Generator
description: Provide plain files to the Kustomize transformer.
sidebar_position: 51
---
# File Generator
Key points to cover:
1. The file generator loads plain files.
2. Can be used to pass-through a plain file.
3. Can be used as an input to a transformer.

View File

@@ -1,313 +0,0 @@
---
slug: hello-holos
title: Hello Holos
description: Configure a simple Hello World service with Holos.
sidebar_position: 30
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import RenderingOverview from '../diagrams/rendering-overview.mdx';
import PlatformSequence from '../diagrams/render-platform-sequence.mdx';
import ComponentSequence from '../diagrams/render-component-sequence.mdx';
# Hello Holos
One of the first exercises you perform when learning a new programming language
is to print out the "Hello World!" greeting. For Holos, our "Hello Holos"
exercise involves modeling the [podinfo Helm chart][podinfo] that produces a
similar greeting message from a Kubernetes Pod.
By the end of this tutorial you will gain the understanding of how to model
an individual Holos Component using a Helm chart as its source.
## The Code
### Generate a Platform
Use `holos` to generate a minimal platform directory structure. First, create
and cd into a blank directory. Then use the `holos generate platform` command to
generate a minimal platform.
```shell
mkdir holos-tutorial
cd holos-tutorial
holos generate platform v1alpha5
```
Holos creates a `platform` directory containing a `platform.gen.cue` file. This
file is the entry point for your new platform configuration. You'll integrate
components into the platform using the CUE code in this `platform` directory.
### Create a Component
Start by creating a directory for the `podinfo` component. Create an empty file
and then add the following CUE configuration to it.
<Tabs groupId="tutorial-hello-podinfo-helm-cue-code">
<TabItem value="components/podinfo/podinfo.cue" label="Podinfo Helm Chart">
```bash
mkdir -p components/podinfo
touch components/podinfo/podinfo.cue
```
```cue showLineNumbers
package holos
// Produce a helm chart build plan.
holos: HelmChart.BuildPlan
HelmChart: #Helm & {
Name: "podinfo"
Chart: {
version: "6.6.2"
repository: {
name: "podinfo"
url: "https://stefanprodan.github.io/podinfo"
}
}
}
```
</TabItem>
</Tabs>
### Integrate the Component
Integrate the `podinfo` component by creating a new file in the `platform`
directory with the following CUE code:
<Tabs groupId="tutorial-hello-register-podinfo-component">
<TabItem value="platform/podinfo.cue" label="Register Podinfo">
```bash
touch platform/podinfo.cue
```
```cue showLineNumbers
package holos
Platform: Components: podinfo: {
name: "podinfo"
path: "components/podinfo"
}
```
</TabItem>
</Tabs>
## Render the Manifests
Render a manifest for `podinfo` using the `holos render platform ./platform`
command.
<Tabs groupId="tutorial-hello-render-manifests">
<TabItem value="command" label="Command">
```bash
holos render platform ./platform
```
</TabItem>
<TabItem value="output" label="Output">
```
cached podinfo 6.6.2
rendered podinfo in 1.938665041s
rendered platform in 1.938759417s
```
</TabItem>
<TabItem value="manifest" label="Manifest Data">
```txt
deploy/components/podinfo/podinfo.gen.yaml
```
```yaml showLineNumbers
apiVersion: v1
kind: Service
metadata:
labels:
app.kubernetes.io/managed-by: Helm
app.kubernetes.io/name: podinfo
app.kubernetes.io/version: 6.6.2
helm.sh/chart: podinfo-6.6.2
name: podinfo
spec:
ports:
- name: http
port: 9898
protocol: TCP
targetPort: http
- name: grpc
port: 9999
protocol: TCP
targetPort: grpc
selector:
app.kubernetes.io/name: podinfo
type: ClusterIP
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app.kubernetes.io/managed-by: Helm
app.kubernetes.io/name: podinfo
app.kubernetes.io/version: 6.6.2
helm.sh/chart: podinfo-6.6.2
name: podinfo
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: podinfo
strategy:
rollingUpdate:
maxUnavailable: 1
type: RollingUpdate
template:
metadata:
annotations:
prometheus.io/port: "9898"
prometheus.io/scrape: "true"
labels:
app.kubernetes.io/name: podinfo
spec:
containers:
- command:
- ./podinfo
- --port=9898
- --cert-path=/data/cert
- --port-metrics=9797
- --grpc-port=9999
- --grpc-service-name=podinfo
- --level=info
- --random-delay=false
- --random-error=false
env:
- name: PODINFO_UI_COLOR
value: '#34577c'
image: ghcr.io/stefanprodan/podinfo:6.6.2
imagePullPolicy: IfNotPresent
livenessProbe:
exec:
command:
- podcli
- check
- http
- localhost:9898/healthz
failureThreshold: 3
initialDelaySeconds: 1
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 5
name: podinfo
ports:
- containerPort: 9898
name: http
protocol: TCP
- containerPort: 9797
name: http-metrics
protocol: TCP
- containerPort: 9999
name: grpc
protocol: TCP
readinessProbe:
exec:
command:
- podcli
- check
- http
- localhost:9898/readyz
failureThreshold: 3
initialDelaySeconds: 1
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 5
resources:
limits: null
requests:
cpu: 1m
memory: 16Mi
volumeMounts:
- mountPath: /data
name: data
terminationGracePeriodSeconds: 30
volumes:
- emptyDir: {}
name: data
```
</TabItem>
</Tabs>
Holos rendered the `deploy/components/podinfo/podinfo.gen.yaml` file by
executing `helm template` after caching `podinfo` locally.
## Breaking it down
Heres a quick review of the files we created and their purpose:
| File | Purpose |
| - | - |
| `components/podinfo/podinfo.cue` | Configures the `podinfo` Helm chart as a holos component. |
| `platform/podinfo.cue` | Adds the `podinfo` Helm chart to the list of Components that will be rendered by Holos. |
We run `holos render platform` against the `platform` directory because that
directory exports a [Platform] resource to `holos`.
<Tabs groupId="67C1EE71-3EA8-4568-9F6D-0072BA09FF12">
<TabItem value="overview" label="Rendering Overview">
Take a look at the other tabs for more detailed sequence diagrams.
<RenderingOverview />
</TabItem>
<TabItem value="platform" label="Platform Sequence">
<PlatformSequence />
</TabItem>
<TabItem value="component" label="Component Sequence">
<ComponentSequence />
</TabItem>
</Tabs>
Components are the building blocks for a Platform, and without them `holos
render platform` does nothing. The `platform/podinfo.cue` file registers the
`podinfo` Component with the Platform, and provides the path to the directory
where the Component's CUE files are located.
:::important
Components can be parameterized.
:::
The Platform spec can re-use the same component path providing it varying input
parameters. This is covered in the [Component Parameters] topic.
:::tip
Holos makes it easy to re-use a Helm chart with multiple customers and
environments. This is not well supported by Helm alone.
:::
The `components/podinfo/podinfo.cue` file unifies the `#Helm`
definition, indicating that we're configuring a Helm chart, along with the
`podinfo` chart's version and repository information. If we wanted to customize
the `podinfo` chart and change any of the chart's values, we would make those
changes here. For example, we could change the message being displayed by
passing the `ui.message` value to the chart:
```cue
HelmChart: #Helm & {
Name: "podinfo"
Chart: {
version: "6.6.2"
repository: {
name: "podinfo"
url: "https://stefanprodan.github.io/podinfo"
}
}
Values: {
ui: message: "Hello Holos from Podinfo!"
}
}
```
Holos repeats this process for every Component added to the Platform, and since `podinfo`
is the only Component, we're done!
## Next Steps
We've shown how to add a single Helm chart to the Platform, but what if you have
more than one Helm chart and they all need to access the same data? Check
out the [Helm Generator] tutorial to learn more.
[podinfo]: https://github.com/stefanprodan/podinfo
[Helm Generator]: ./helm-generator.mdx
[Platform]: ../api/author.md#Platform
[Component Parameters]: ../topics/component-parameters.mdx

View File

@@ -1,13 +0,0 @@
---
slug: helm-generator
title: Helm Generator
description: Generate manifests from existing Helm Charts
sidebar_position: 40
---
# Helm
Key points to cover:
1. Holos makes it easy to define data once and pass it to multiple charts.
2. Work through the Prometheus and Blackbox example from the v1alpha4 Helm Guide.

View File

@@ -1,10 +0,0 @@
---
slug: kustomize-transformer
title: Kustomize Transformer
description: Holos makes it easy to Kustomize a Helm chart.
sidebar_position: 60
---
# Helm Kustomization
Introduce Kustomization transformer.

View File

@@ -1,124 +0,0 @@
---
slug: overview
title: Overview
description: Learn how Holos integrates software into a holistic platform.
sidebar_position: 10
---
import RenderingOverview from '../diagrams/rendering-overview.mdx';
import RenderPlatformDiagram from '../diagrams/render-platform-sequence.mdx';
import RenderComponentDiagram from '../diagrams/render-component-sequence.mdx';
# Tutorial
Holos is a configuration management tool for Kubernetes resources. It provides
the building blocks needed for implementing the [rendered manifests pattern].
It gives the flexibility to manage a wide range of configurations, from large
software delivery platforms spanning multiple clusters and regions to generating
a single resource on your local device.
{/* truncate */}
At a high level, Holos provides a few major components:
- A Platform schema for specifying how components integrate together into a platform.
- Component building blocks for Helm, Kustomize, and Kubernetes to unify configuration with CUE.
- A BuildPlan orchestrating generators, transformers, and validators to produce manifest files.
<RenderingOverview />
## Holos' role in your organization
Platform engineers run the `holos render platform` command locally and in CI to
produce Kubernetes manifests which are committed to version control. GitOps
tools like ArgoCD or Flux deploy the manifests produced by Holos.
Other tools focus on individual applications. Holos focuses holistically on the
integration of applications into a whole platform. In this way a single one
line configuration change, like changing the domain name, is clearly visible
across the whole platform.
Rendering a platform may produce a single ConfigMap resource or produce millions
of lines of fully rendered manifests configuring multiple environments,
clusters, and regions.
<RenderPlatformDiagram />
## Advantages of Holos
This section outlines some advantages of Holos.
### Safe
Holos uses [CUE] to provide strong typing and constraints to configuration data.
Additionally, holos adds strong validation to verify the output produced by Helm
and other tools.
### Consistent
Holos offers a consistent way to incorporate a wide variety of tools into a well
defined data pipeline. Configuration produced from CUE, Helm, and Kustomize are
all handled with the same consistent process.
### Easy
Holos makes it easy to define configuration data once, then safely use the data
in multiple Helm charts, Kustomize bases, or any other supported component kind.
### Flexible
Holos is designed to be flexible. Holos offers flexible building blocks for
data generation, transformation, validation, and integration. Find the perfect
fit for your team by assembling these building blocks to your unique needs.
Holos does not have an opinion on many common aspects of platform configuration.
For example, environments and clusters are explicitly kept out of the core and
instead are provided as flexible, user-customizable [topics] organized as a
recipes.
<RenderComponentDiagram />
## When not to use Holos
Holos is useful for many organizations but there are situations where Holos is
not a good fit.
### Implicit GitOps
Holos takes an explicit view of GitOps, meaning the fully rendered manifests are
complete. These manifests are stored in version control without an implicit
dependency on external systems. If your team uses an implicit approach, meaning
some configuration comes from elsewhere, then Holos may not be a good fit.
For example, if container image tags are managed by a system outside of version
control then holos may not be a good fit.
Holos is a good fit if you're open to integrate external systems with Holos such
that the external data becomes an input to produce the fully rendered manifests.
### CUE is a Non Starter
Holos heavily uses CUE. For example, when using Holos with Helm and Kustomize,
we no longer need to write YAML files and text templates. All configuration is
expressed in CUE and `holos` handles the YAML on our behalf.
CUE is a relatively simple domain specific language, but it is very different
from most other languages because of it's roots in logic programming languages.
Holos is not a good fit if your team prefers to write code in YAML templates or
turing complete general purpose languages.
For more info see [Why CUE for Configuration].
## Getting Help
If you get stuck, you can get help on [Discord] or [GitHub discussions]. Don't
worry about asking "beginner" questions, configuration is often complex even for
the most experienced among us. We all start somewhere and are happy to help.
[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]: ../topics.mdx

View File

@@ -1,141 +0,0 @@
---
slug: setup
title: Setup
description: Install Holos and build the initial structure.
sidebar_position: 20
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import RenderPlatformDiagram from '../diagrams/render-platform-sequence.mdx';
# Setup
This tutorial will guide you through the installation of Holos and its
dependencies, as well as the initialization of a minimal Platform that you can
extend to meet your specific needs.
## Prerequisites
Holos integrates with the following tools that should be installed to enable
their functionality.
* [Helm] to fetch and render Helm chart Components [Kubectl] to [kustomize]
* components.
## Install Holos
Holos is distributed as a single file executable that can be installed in a couple of ways.
### Releases
Download `holos` from the [releases] page and place the executable into your
shell path.
### Go install
Alternatively, install directly into your go bin path using:
```shell
go install github.com/holos-run/holos/cmd/holos@latest
```
## Generate a new Platform
Use `holos` to generate a minimal platform using the recommended directory
structure. First, create and cd into a blank directory. Then use the `holos
generate platform` command to generate a minimal platform.
```shell
mkdir holos-tutorial
cd holos-tutorial
holos generate platform v1alpha5
```
Holos creates a `platform` directory containing a `platform.gen.cue` file
serving as the entry point for your new platform configuration. You'll register
software components with the platform using the CUE code in this `platform`
directory.
## Render the Platform
We usually make configuration changes with holos in the following loop:
1. Edit some CUE code.
2. Render the whole platform.
3. Review the fully rendered manifests.
4. Repeat
Rendering the platform to plain manifest files allows us to see the changes
clearly.
<Tabs groupId="66379FE3-206D-4DF1-BAF0-29C9A6C17D00">
<TabItem value="command" label="Command">
```bash
holos render platform ./platform
```
</TabItem>
<TabItem value="output" label="Output">
```txt
rendered platform in 8.292µs
```
</TabItem>
<TabItem value="diagram" label="Diagram">
<RenderPlatformDiagram />
</TabItem>
</Tabs>
In this initial state `holos` doesn't do anything because no components are
registered with the platform. You can inspect the Platform definition to get
this insight.
<Tabs groupId="218658D2-1305-46D3-8F55-8DA8EB45F114">
<TabItem value="command" label="Command">
```bash
holos cue export --expression holos --out=yaml ./platform
```
</TabItem>
<TabItem value="output" label="Output">
```yaml
kind: Platform
apiVersion: v1alpha5
metadata:
name: default
spec:
components: []
```
Note the `spec.components` field is empty.
</TabItem>
</Tabs>
## Next Steps
You've got the structure of your platform configuration in place. Continue on to
[Hello Holos] where you'll learn how easy it is to manage a Helm chart with
holos.
## Do I need Kubernetes?
No. Holos generates fully rendered Kubernetes manifests but does not apply
them. You don't need a Kubernetes cluster to start using Holos and reviewing
the files it creates.
You will need a cluster to apply the rendered manifests and interact with the
resources and services Holos configures. We recommend using our [Local Cluster]
guide to set up a minimal [k3d] Kubernetes cluster with [OrbStack] or [Docker].
You might like our [Local Cluster] guide because it:
1. Includes a script to quickly reset the cluster to a known good state.
2. Configures proper DNS and TLS certificates.
[Helm]: https://github.com/helm/helm/releases
[Kubectl]: https://kubernetes.io/docs/tasks/tools/
[kustomize]: https://kustomize.io/
[Local Cluster]: ../topics/local-cluster.mdx
[Hello Holos]: ./hello-holos.mdx
[releases]: https://github.com/holos-run/holos/releases
[k3d]: https://k3d.io/
[OrbStack]: https://docs.orbstack.dev/install
[Docker]: https://docs.docker.com/get-started/get-docker/

View File

@@ -35,8 +35,5 @@ gomarkdoc --output "${tmpdir}/doc.md" "./${package}"
# Fix heading anchors by making them explicit
# Refer to https://docusaurus.io/docs/markdown-features/toc#heading-ids
sed -E 's/## type ([A-Za-z0-9_]+)/## type \1 {#\1}/' "${tmpdir}/doc.md" > "${tmpdir}/with-header.md"
# Remove the annoying package h1 header, docusaurus will use the title front
# matter for the page h1 header.
grep -v "^# $(basename "${schema}")" "${tmpdir}/with-header.md" > "${tmpdir}/fixed.md"
sed -E 's/## type ([A-Za-z0-9_]+)/## type \1 {#\1}/' "${tmpdir}/doc.md" > "${tmpdir}/fixed.md"
cat "./${package%%/}/header.yaml" "${tmpdir}/fixed.md" > "${base}.md"

View File

@@ -1,17 +1,7 @@
// Package holos defines types for the rest of the system.
package holos
import (
"bytes"
"context"
"encoding/json"
"fmt"
"log/slog"
"strings"
"cuelang.org/go/cue"
"github.com/holos-run/holos/internal/errors"
)
import "context"
// A PathCueMod is a string representing the absolute filesystem path of a cue
// module. It is given a unique type so the API is clear.
@@ -50,76 +40,3 @@ type ArtifactMap interface {
Set(path string, data []byte) error
Save(dir, path string) error
}
// Discriminator is useful to discriminate by type meta, the kind and api
// version of something.
type Discriminator interface {
Discriminate(ctx context.Context) (TypeMeta, error)
}
type Unifier interface {
Unify(ctx context.Context) (BuildData, error)
}
// BuildData represents the data necessary to produce a build plan. It is a
// convenience wrapper to store relevant fields to inform the user.
type BuildData struct {
Value cue.Value
ModuleRoot string
InstancePath InstancePath
Dir string
}
func (bd *BuildData) TypeMeta() (tm TypeMeta, err error) {
v, err := bd.value()
if err != nil {
return tm, errors.Wrap(err)
}
kind := v.LookupPath(cue.ParsePath("kind"))
if err := kind.Err(); err != nil {
return tm, errors.Wrap(err)
}
if tm.Kind, err = kind.String(); err != nil {
return tm, errors.Wrap(err)
}
version := v.LookupPath(cue.ParsePath("apiVersion"))
if err := version.Err(); err != nil {
return tm, errors.Wrap(err)
}
if tm.APIVersion, err = version.String(); err != nil {
return tm, errors.Wrap(err)
}
return
}
func (bd *BuildData) Decoder() (*json.Decoder, error) {
v, err := bd.value()
if err != nil {
return nil, errors.Wrap(err)
}
jsonBytes, err := v.MarshalJSON()
if err != nil {
return nil, errors.Wrap(err)
}
decoder := json.NewDecoder(bytes.NewReader(jsonBytes))
decoder.DisallowUnknownFields()
return decoder, nil
}
func (bd *BuildData) value() (v cue.Value, err error) {
v = bd.Value.LookupPath(cue.ParsePath("holos"))
if err := v.Err(); err != nil {
if strings.HasPrefix(err.Error(), "field not found") {
slog.Warn(fmt.Sprintf("%s: deprecated usage: nest output under holos: %s", err, bd.Dir), "err", err)
v = bd.Value
return v, nil
}
err = errors.Wrap(err)
return v, err
}
return
}

View File

@@ -48,6 +48,28 @@ type config struct {
tags []string
}
// BuildData represents the data necessary to produce a build plan. It is a
// convenience wrapper to store relevant fields to inform the user.
type BuildData struct {
Value cue.Value
ModuleRoot string
InstancePath holos.InstancePath
Dir string
}
func (bd *BuildData) TypeMeta() (tm holos.TypeMeta, err error) {
jsonBytes, err := bd.Value.MarshalJSON()
if err != nil {
err = errors.Format("could not marshal json %s: %w", bd.Dir, err)
return
}
err = json.NewDecoder(bytes.NewReader(jsonBytes)).Decode(&tm)
if err != nil {
err = errors.Format("could not decode type meta %s: %w", bd.Dir, err)
}
return
}
type Builder struct {
cfg config
ctx *cue.Context
@@ -120,59 +142,11 @@ func (b *Builder) Cluster() string {
return b.cfg.cluster
}
func (b *Builder) Discriminate(ctx context.Context) (tm holos.TypeMeta, err error) {
cueModDir, err := b.findCueMod()
if err != nil {
err = errors.Wrap(err)
return
}
cueConfig := load.Config{
Dir: string(cueModDir),
ModuleRoot: string(cueModDir),
}
bd := &holos.BuildData{ModuleRoot: string(cueModDir)}
if len(b.cfg.args) > 1 {
return tm, errors.Wrap(errors.New("cannot provide more than one argument"))
}
// Make args relative to the module directory
args := make([]string, 0, len(b.cfg.args)+2)
for _, path := range b.cfg.args {
target, err := filepath.Abs(path)
if err != nil {
return tm, errors.Wrap(fmt.Errorf("could not find absolute path: %w", err))
}
relPath, err := filepath.Rel(bd.ModuleRoot, target)
if err != nil {
return tm, errors.Wrap(fmt.Errorf("invalid argument, must be relative to cue.mod: %w", err))
}
bd.InstancePath = holos.InstancePath(target)
bd.Dir = relPath
relPath = "./" + relPath
args = append(args, relPath)
}
instances := load.Instances(args, &cueConfig)
values, err := b.ctx.BuildInstances(instances)
if err != nil {
return tm, errors.Wrap(err)
}
bd.Value = values[0]
tm, err = bd.TypeMeta()
return
}
// Unify returns a cue.Value representing the kind of build holos is meant to
// execute. This function unifies a cue package entrypoint with
// platform.config.json and user data json files located recursively within the
// userdata directory at the cue module root.
//
// Deprecated: use Discriminate instead.
func (b *Builder) Unify(ctx context.Context, cfg *client.Config) (bd holos.BuildData, err error) {
func (b *Builder) Unify(ctx context.Context, cfg *client.Config) (bd BuildData, err error) {
// Ensure the value is from the same runtime, otherwise cue panics.
bd.Value = b.ctx.CompileString("")
@@ -302,7 +276,7 @@ func (b *Builder) Run(ctx context.Context, cfg *client.Config) (results []*rende
return b.build(ctx, bd)
}
func (b *Builder) build(ctx context.Context, bd holos.BuildData) (results []*render.Result, err error) {
func (b *Builder) build(ctx context.Context, bd BuildData) (results []*render.Result, err error) {
log := logger.FromContext(ctx).With("dir", bd.InstancePath)
value := bd.Value
@@ -456,7 +430,7 @@ func (b *Builder) Platform(ctx context.Context, cfg *client.Config) (*core_v1alp
return b.runPlatform(ctx, bd)
}
func (b *Builder) runPlatform(ctx context.Context, bd holos.BuildData) (*core_v1alpha2.Platform, error) {
func (b *Builder) runPlatform(ctx context.Context, bd BuildData) (*core_v1alpha2.Platform, error) {
path := holos.InstancePath(bd.Dir)
log := logger.FromContext(ctx).With("dir", path)

View File

@@ -1,600 +0,0 @@
package v1alpha5
import (
"bytes"
"context"
"fmt"
"io"
"log/slog"
"os"
"path/filepath"
"strings"
"syscall"
"time"
"cuelang.org/go/cue"
"cuelang.org/go/cue/cuecontext"
"cuelang.org/go/cue/load"
"github.com/holos-run/holos"
h "github.com/holos-run/holos"
core "github.com/holos-run/holos/api/core/v1alpha5"
"github.com/holos-run/holos/internal/errors"
"github.com/holos-run/holos/internal/logger"
"github.com/holos-run/holos/internal/util"
"golang.org/x/sync/errgroup"
"gopkg.in/yaml.v3"
)
func Unify(cueCtx *cue.Context, path string, tags []string) (bd holos.BuildData, err error) {
if bd.ModuleRoot, bd.Dir, err = util.FindRootLeaf(path); err != nil {
return bd, errors.Wrap(err)
}
cueConfig := load.Config{
Dir: bd.ModuleRoot,
ModuleRoot: bd.ModuleRoot,
Tags: tags,
}
args := []string{bd.Dir}
instances := load.Instances(args, &cueConfig)
v := cueCtx.BuildInstance(instances[0])
if err = v.Err(); err != nil {
return bd, errors.Wrap(err)
}
bd.Value = v
return
}
func LoadPlatform(path string, tags []string) (*Platform, error) {
bd, err := Unify(cuecontext.New(), path, tags)
if err != nil {
return nil, errors.Wrap(err)
}
decoder, err := bd.Decoder()
if err != nil {
return nil, errors.Wrap(err)
}
var platform Platform
if err := decoder.Decode(&platform.Platform); err != nil {
return nil, errors.Wrap(err)
}
return &platform, nil
}
// Platform represents a platform builder.
type Platform struct {
Platform core.Platform
Concurrency int
Stderr io.Writer
}
// Build builds a Platform by concurrently building a BuildPlan for each
// platform component. No artifact files are written directly, only indirectly
// by rendering each component.
func (p *Platform) Build(ctx context.Context, _ h.ArtifactMap) error {
parentStart := time.Now()
components := p.Platform.Spec.Components
total := len(components)
g, ctx := errgroup.WithContext(ctx)
// Limit the number of concurrent goroutines due to CUE memory usage concerns
// while rendering components. One more for the producer.
g.SetLimit(p.Concurrency + 1)
// Spawn a producer because g.Go() blocks when the group limit is reached.
g.Go(func() error {
for idx := range components {
select {
case <-ctx.Done():
return ctx.Err()
default:
// Capture idx to avoid issues with closure. Fixed in Go 1.22.
idx := idx
component := &components[idx]
// Worker go routine. Blocks if limit has been reached.
g.Go(func() error {
select {
case <-ctx.Done():
return ctx.Err()
default:
start := time.Now()
log := logger.FromContext(ctx).With(
"name", component.Name,
"path", component.Path,
"num", idx+1,
"total", total,
)
log.DebugContext(ctx, "render component")
tags := make([]string, 0, 2+len(component.Parameters))
tags = append(tags, "holos_component_name="+component.Name)
tags = append(tags, "holos_component_path="+component.Path)
for key, value := range component.Parameters {
tags = append(tags, fmt.Sprintf("%s=%s", key, value))
}
// Execute a sub-process to limit CUE memory usage.
args := make([]string, 0, 10)
args = append(args,
"render",
"component",
)
for _, tag := range tags {
args = append(args, "--inject", tag)
}
if component.WriteTo != "" {
args = append(args, "--write-to", component.WriteTo)
}
args = append(args, component.Path)
result, err := util.RunCmd(ctx, "holos", args...)
// I've lost an hour+ digging into why I couldn't see log output
// from sub-processes. Make sure to surface at least stderr from
// sub-processes.
_, _ = io.Copy(p.Stderr, result.Stderr)
if err != nil {
return errors.Wrap(fmt.Errorf("could not render component: %w", err))
}
duration := time.Since(start)
msg := fmt.Sprintf(
"rendered %s in %s",
component.Name,
duration,
)
log.InfoContext(ctx, msg, "duration", duration)
return nil
}
})
}
}
return nil
})
// Wait for completion and return the first error (if any)
if err := g.Wait(); err != nil {
return err
}
duration := time.Since(parentStart)
msg := fmt.Sprintf("rendered platform in %s", duration)
logger.FromContext(ctx).InfoContext(ctx, msg, "duration", duration, "version", p.Platform.APIVersion)
return nil
}
// BuildPlan represents a component builder.
type BuildPlan struct {
BuildPlan core.BuildPlan
Concurrency int
Stderr io.Writer
// WriteTo --write-to=deploy flag
WriteTo string
// Path represents the path to the component
Path h.InstancePath
}
// Build builds a BuildPlan into Artifact files.
func (b *BuildPlan) Build(ctx context.Context, am h.ArtifactMap) error {
name := b.BuildPlan.Metadata.Name
path := b.BuildPlan.Source.Component.Path
log := logger.FromContext(ctx).With("name", name, "path", path)
msg := fmt.Sprintf("could not build %s", name)
if b.BuildPlan.Spec.Disabled {
log.WarnContext(ctx, fmt.Sprintf("%s: disabled", msg))
return nil
}
g, ctx := errgroup.WithContext(ctx)
// One more for the producer
g.SetLimit(b.Concurrency + 1)
// Producer.
g.Go(func() error {
for _, a := range b.BuildPlan.Spec.Artifacts {
msg := fmt.Sprintf("%s artifact %s", msg, a.Artifact)
log := log.With("artifact", a.Artifact)
if a.Skip {
log.WarnContext(ctx, fmt.Sprintf("%s: skipped field is true", msg))
continue
}
select {
case <-ctx.Done():
return ctx.Err()
default:
// https://golang.org/doc/faq#closures_and_goroutines
a := a
// Worker. Blocks if limit has been reached.
g.Go(func() error {
for _, gen := range a.Generators {
switch gen.Kind {
case "Resources":
if err := b.resources(log, gen, am); err != nil {
return errors.Format("could not generate resources: %w", err)
}
case "Helm":
if err := b.helm(ctx, log, gen, am); err != nil {
return errors.Format("could not generate helm: %w", err)
}
case "File":
if err := b.file(log, gen, am); err != nil {
return errors.Format("could not generate file: %w", err)
}
default:
return errors.Format("%s: unsupported kind %s", msg, gen.Kind)
}
}
for _, t := range a.Transformers {
switch t.Kind {
case "Kustomize":
if err := b.kustomize(ctx, log, t, am); err != nil {
return errors.Wrap(err)
}
case "Join":
s := make([][]byte, 0, len(t.Inputs))
for _, input := range t.Inputs {
if data, ok := am.Get(string(input)); ok {
s = append(s, data)
} else {
return errors.Format("%s: missing %s", msg, input)
}
}
data := bytes.Join(s, []byte(t.Join.Separator))
if err := am.Set(string(t.Output), data); err != nil {
return errors.Format("%s: %w", msg, err)
}
log.Debug("set artifact: " + string(t.Output))
default:
return errors.Format("%s: unsupported kind %s", msg, t.Kind)
}
}
// Write the final artifact
if err := am.Save(b.WriteTo, string(a.Artifact)); err != nil {
return errors.Format("%s: %w", msg, err)
}
log.DebugContext(ctx, "wrote "+filepath.Join(b.WriteTo, string(a.Artifact)))
return nil
})
}
}
return nil
})
// Wait for completion and return the first error (if any)
return g.Wait()
}
func (b *BuildPlan) file(
log *slog.Logger,
g core.Generator,
am h.ArtifactMap,
) error {
data, err := os.ReadFile(filepath.Join(string(b.Path), string(g.File.Source)))
if err != nil {
return errors.Wrap(err)
}
if err := am.Set(string(g.Output), data); err != nil {
return errors.Wrap(err)
}
log.Debug("set artifact: " + string(g.Output))
return nil
}
func (b *BuildPlan) helm(
ctx context.Context,
log *slog.Logger,
g core.Generator,
am h.ArtifactMap,
) error {
chartName := g.Helm.Chart.Name
log = log.With("chart", chartName)
// Unnecessary? cargo cult copied from internal/cli/render/render.go
if chartName == "" {
return errors.New("missing chart name")
}
// Cache the chart by version to pull new versions. (#273)
cacheDir := filepath.Join(string(b.Path), "vendor", g.Helm.Chart.Version)
cachePath := filepath.Join(cacheDir, filepath.Base(chartName))
if _, err := os.Stat(cachePath); os.IsNotExist(err) {
timeout, cancel := context.WithTimeout(ctx, 5*time.Minute)
defer cancel()
err := onceWithLock(log, timeout, cachePath, func() error {
return b.cacheChart(ctx, log, cacheDir, g.Helm.Chart)
})
if err != nil {
return errors.Format("could not cache chart: %w", err)
}
}
// Write values file
tempDir, err := os.MkdirTemp("", "holos.helm")
if err != nil {
return errors.Format("could not make temp dir: %w", err)
}
defer util.Remove(ctx, tempDir)
data, err := yaml.Marshal(g.Helm.Values)
if err != nil {
return errors.Format("could not marshal values: %w", err)
}
valuesPath := filepath.Join(tempDir, "values.yaml")
if err := os.WriteFile(valuesPath, data, 0666); err != nil {
return errors.Wrap(fmt.Errorf("could not write values: %w", err))
}
log.DebugContext(ctx, "wrote"+valuesPath)
// Run charts
args := []string{"template"}
if !g.Helm.EnableHooks {
args = append(args, "--no-hooks")
}
args = append(args,
"--include-crds",
"--values", valuesPath,
"--namespace", g.Helm.Namespace,
"--kubeconfig", "/dev/null",
"--version", g.Helm.Chart.Version,
g.Helm.Chart.Release,
cachePath,
)
helmOut, err := util.RunCmd(ctx, "helm", args...)
if err != nil {
stderr := helmOut.Stderr.String()
lines := strings.Split(stderr, "\n")
for _, line := range lines {
if strings.HasPrefix(line, "Error:") {
err = fmt.Errorf("%s: %w", line, err)
}
}
return errors.Format("could not run helm template: %w", err)
}
// Set the artifact
if err := am.Set(string(g.Output), helmOut.Stdout.Bytes()); err != nil {
return errors.Format("could not store helm output: %w", err)
}
log.Debug("set artifact: " + string(g.Output))
return nil
}
func (b *BuildPlan) resources(
log *slog.Logger,
g core.Generator,
am h.ArtifactMap,
) error {
var size int
for _, m := range g.Resources {
size += len(m)
}
list := make([]core.Resource, 0, size)
for _, m := range g.Resources {
for _, r := range m {
list = append(list, r)
}
}
msg := fmt.Sprintf(
"could not generate %s for %s path %s",
g.Output,
b.BuildPlan.Metadata.Name,
b.BuildPlan.Source.Component.Path,
)
buf, err := marshal(list)
if err != nil {
return errors.Format("%s: %w", msg, err)
}
if err := am.Set(string(g.Output), buf.Bytes()); err != nil {
return errors.Format("%s: %w", msg, err)
}
log.Debug("set artifact " + string(g.Output))
return nil
}
func (b *BuildPlan) kustomize(
ctx context.Context,
log *slog.Logger,
t core.Transformer,
am h.ArtifactMap,
) error {
tempDir, err := os.MkdirTemp("", "holos.kustomize")
if err != nil {
return errors.Wrap(err)
}
defer util.Remove(ctx, tempDir)
msg := fmt.Sprintf(
"could not transform %s for %s path %s",
t.Output,
b.BuildPlan.Metadata.Name,
b.BuildPlan.Source.Component.Path,
)
// Write the kustomization
data, err := yaml.Marshal(t.Kustomize.Kustomization)
if err != nil {
return errors.Format("%s: %w", msg, err)
}
path := filepath.Join(tempDir, "kustomization.yaml")
if err := os.WriteFile(path, data, 0666); err != nil {
return errors.Format("%s: %w", msg, err)
}
log.DebugContext(ctx, "wrote "+path)
// Write the inputs
for _, input := range t.Inputs {
path := string(input)
if err := am.Save(tempDir, path); err != nil {
return errors.Format("%s: %w", msg, err)
}
log.DebugContext(ctx, "wrote "+filepath.Join(tempDir, path))
}
// Execute kustomize
r, err := util.RunCmd(ctx, "kubectl", "kustomize", tempDir)
if err != nil {
kErr := r.Stderr.String()
err = errors.Format("%s: could not run kustomize: %w", msg, err)
log.ErrorContext(ctx, fmt.Sprintf("%s: stderr:\n%s", err.Error(), kErr), "err", err, "stderr", kErr)
return err
}
// Store the artifact
if err := am.Set(string(t.Output), r.Stdout.Bytes()); err != nil {
return errors.Format("%s: %w", msg, err)
}
log.Debug("set artifact " + string(t.Output))
return nil
}
func marshal(list []core.Resource) (buf bytes.Buffer, err error) {
encoder := yaml.NewEncoder(&buf)
defer encoder.Close()
for _, item := range list {
if err = encoder.Encode(item); err != nil {
err = errors.Wrap(err)
return
}
}
return
}
// cacheChart stores a cached copy of Chart in the chart subdirectory of path.
//
// We assume the only method responsible for writing to chartDir is cacheChart
// itself. cacheChart runs concurrently when rendering a platform.
//
// We rely on the atomicity of moving temporary directories into place on the
// same filesystem via os.Rename. If a syscall.EEXIST error occurs during
// renaming, it indicates that the cached chart already exists, which is
// expected when this function is called concurrently.
//
// TODO(jeff): Break the dependency on v1alpha5, make it work across versions as
// a utility function.
func (b *BuildPlan) cacheChart(
ctx context.Context,
log *slog.Logger,
cacheDir string,
chart core.Chart,
) error {
// Add repositories
repo := chart.Repository
if repo.URL == "" {
// repo update not needed for oci charts so this is debug instead of warn.
log.DebugContext(ctx, "skipped helm repo add and update: repo url is empty")
} else {
if r, err := util.RunCmd(ctx, "helm", "repo", "add", repo.Name, repo.URL); err != nil {
_, _ = io.Copy(b.Stderr, r.Stderr)
return errors.Format("could not run helm repo add: %w", err)
}
if r, err := util.RunCmd(ctx, "helm", "repo", "update", repo.Name); err != nil {
_, _ = io.Copy(b.Stderr, r.Stderr)
return errors.Format("could not run helm repo update: %w", err)
}
}
cacheTemp, err := os.MkdirTemp(cacheDir, chart.Name)
if err != nil {
return errors.Wrap(err)
}
defer util.Remove(ctx, cacheTemp)
cn := chart.Name
if chart.Repository.Name != "" {
cn = fmt.Sprintf("%s/%s", chart.Repository.Name, chart.Name)
}
helmOut, err := util.RunCmd(ctx, "helm", "pull", "--destination", cacheTemp, "--untar=true", "--version", chart.Version, cn)
if err != nil {
return errors.Wrap(fmt.Errorf("could not run helm pull: %w", err))
}
log.Debug("helm pull", "stdout", helmOut.Stdout, "stderr", helmOut.Stderr)
items, err := os.ReadDir(cacheTemp)
if err != nil {
return errors.Wrap(fmt.Errorf("could not read directory: %w", err))
}
if len(items) != 1 {
return errors.Format("want: exactly one item, have: %+v", items)
}
item := items[0]
src := filepath.Join(cacheTemp, item.Name())
dst := filepath.Join(cacheDir, chart.Name)
if err := os.Rename(src, dst); err != nil {
var linkErr *os.LinkError
if errors.As(err, &linkErr) && errors.Is(linkErr.Err, syscall.EEXIST) {
log.DebugContext(ctx, "cache already exists", "chart", chart.Name, "chart_version", chart.Version, "path", dst)
} else {
return errors.Wrap(fmt.Errorf("could not rename: %w", err))
}
} else {
log.DebugContext(ctx, fmt.Sprintf("renamed %s to %s", src, dst), "src", src, "dst", dst)
}
log.InfoContext(ctx,
fmt.Sprintf("cached %s %s", chart.Name, chart.Version),
"chart", chart.Name,
"chart_version", chart.Version,
"path", dst,
)
return nil
}
// onceWithLock obtains a filesystem lock with mkdir, then executes fn. If the
// lock is already locked, onceWithLock waits for it to be released then returns
// without calling fn.
func onceWithLock(log *slog.Logger, ctx context.Context, path string, fn func() error) error {
if err := os.MkdirAll(filepath.Dir(path), 0777); err != nil {
return errors.Wrap(err)
}
// Obtain a lock with a timeout.
lockDir := path + ".lock"
log = log.With("lock", lockDir)
err := os.Mkdir(lockDir, 0777)
if err == nil {
defer os.RemoveAll(lockDir)
log.DebugContext(ctx, fmt.Sprintf("acquired %s", lockDir))
if err := fn(); err != nil {
return errors.Wrap(err)
}
log.DebugContext(ctx, fmt.Sprintf("released %s", lockDir))
return nil
}
// Wait until the lock is released then return.
if os.IsExist(err) {
log.DebugContext(ctx, fmt.Sprintf("blocked %s", lockDir))
stillBlocked := time.After(5 * time.Second)
deadLocked := time.After(10 * time.Second)
for {
select {
case <-stillBlocked:
log.WarnContext(ctx, fmt.Sprintf("waiting for %s to be released", lockDir))
case <-deadLocked:
log.WarnContext(ctx, fmt.Sprintf("still waiting for %s to be released (dead lock?)", lockDir))
case <-time.After(100 * time.Millisecond):
if _, err := os.Stat(lockDir); os.IsNotExist(err) {
log.DebugContext(ctx, fmt.Sprintf("unblocked %s", lockDir))
return nil
}
case <-ctx.Done():
return errors.Wrap(ctx.Err())
}
}
}
// Unexpected error
return errors.Wrap(err)
}

View File

@@ -3,7 +3,6 @@ package generate
import (
"fmt"
"log/slog"
"os"
"path/filepath"
"strings"
@@ -28,29 +27,14 @@ func New(cfg *holos.Config, feature holos.Flagger) *cobra.Command {
}
func NewPlatform(cfg *holos.Config) *cobra.Command {
var force bool
cmd := command.New("platform [flags] PLATFORM")
cmd.Short = "generate a platform from an embedded schematic"
cmd.Long = fmt.Sprintf("Embedded platforms available to generate:\n\n %s", strings.Join(generate.Platforms(), "\n "))
cmd.Example = " holos generate platform k3d"
cmd.Args = cobra.ExactArgs(1)
cmd.Flags().BoolVarP(&force, "force", "", force, "force generation")
cmd.RunE = func(cmd *cobra.Command, args []string) error {
ctx := cmd.Root().Context()
if !force {
files, err := os.ReadDir(".")
if err != nil {
return errors.Wrap(err)
}
if len(files) > 0 {
return errors.Format("could not generate: directory not empty and --force=false")
}
}
for _, name := range args {
if err := generate.GeneratePlatform(ctx, name); err != nil {
return errors.Wrap(err)

View File

@@ -1,3 +1,3 @@
Enable completion with:
source <(holos completion ${SHELL##*/})
source <(holos-server completion ${SHELL##*/})

View File

@@ -9,12 +9,10 @@ import (
"runtime"
"strings"
"cuelang.org/go/cue/cuecontext"
h "github.com/holos-run/holos"
"github.com/holos-run/holos/internal/artifact"
"github.com/holos-run/holos/internal/builder"
"github.com/holos-run/holos/internal/builder/v1alpha4"
"github.com/holos-run/holos/internal/builder/v1alpha5"
"github.com/holos-run/holos/internal/cli/command"
"github.com/holos-run/holos/internal/client"
"github.com/holos-run/holos/internal/errors"
@@ -57,59 +55,30 @@ func NewComponent(cfg *holos.Config) *cobra.Command {
cmd.RunE = func(cmd *cobra.Command, args []string) error {
ctx := cmd.Root().Context()
log := logger.FromContext(ctx)
build := builder.New(builder.Entrypoints(args))
tm, err := build.Discriminate(ctx)
if err != nil {
return errors.Wrap(err)
}
if tm.Kind != "BuildPlan" {
return errors.Format("invalid kind: want: BuildPlan have: %s", tm.Kind)
}
log.DebugContext(ctx, fmt.Sprintf("discriminated %s %s", tm.APIVersion, tm.Kind))
path := args[0]
switch tm.APIVersion {
case "v1alpha5":
builder := v1alpha5.BuildPlan{
Concurrency: concurrency,
Stderr: cmd.ErrOrStderr(),
WriteTo: cfg.WriteTo(),
Path: h.InstancePath(path),
}
bd, err := v1alpha5.Unify(cuecontext.New(), path, tagMap.Tags())
if err != nil {
return errors.Wrap(err)
}
decoder, err := bd.Decoder()
if err != nil {
return errors.Wrap(err)
}
if err := decoder.Decode(&builder.BuildPlan); err != nil {
return errors.Format("could not decode build plan %s: %w", bd.Dir, err)
}
// Process the BuildPlan.
return render.Component(ctx, &builder, artifact.New())
}
// This is the old way of doing it prior to v1alpha5 and should be removed
// before v1.
build = builder.New(
build := builder.New(
builder.Entrypoints(args),
builder.Cluster(cfg.ClusterName()),
builder.Tags(tagMap.Tags()),
)
log := logger.FromContext(ctx)
log.DebugContext(ctx, "cue: building component instance")
//nolint:staticcheck
bd, err := build.Unify(ctx, config)
if err != nil {
return errors.Wrap(err)
}
typeMeta, err := bd.TypeMeta()
if err != nil {
return errors.Wrap(err)
}
if typeMeta.Kind != "BuildPlan" {
return errors.Format("invalid kind: want: BuildPlan have: %s", typeMeta.Kind)
}
log.DebugContext(ctx, "discriminated "+typeMeta.APIVersion+" "+typeMeta.Kind)
jsonBytes, err := bd.Value.MarshalJSON()
if err != nil {
return errors.Format("could not marshal json %s: %w", bd.Dir, err)
@@ -117,7 +86,9 @@ func NewComponent(cfg *holos.Config) *cobra.Command {
decoder := json.NewDecoder(bytes.NewReader(jsonBytes))
decoder.DisallowUnknownFields()
switch tm.APIVersion {
art := artifact.New()
switch version := typeMeta.APIVersion; version {
case "v1alpha4":
builder := v1alpha4.BuildPlan{
WriteTo: cfg.WriteTo(),
@@ -128,7 +99,7 @@ func NewComponent(cfg *holos.Config) *cobra.Command {
if err := decoder.Decode(&builder.BuildPlan); err != nil {
return errors.Format("could not decode build plan %s: %w", bd.Dir, err)
}
return render.Component(ctx, &builder, artifact.New())
return render.Component(ctx, &builder, art)
// Legacy method.
case "v1alpha3", "v1alpha2", "v1alpha1":
//nolint:staticcheck
@@ -168,7 +139,7 @@ func NewComponent(cfg *holos.Config) *cobra.Command {
}
default:
return errors.Format("component version not supported: %s", tm.APIVersion)
return errors.Format("component version not supported: %s", version)
}
return nil
@@ -191,12 +162,16 @@ func NewPlatform(cfg *holos.Config) *cobra.Command {
cmd.RunE = func(cmd *cobra.Command, args []string) error {
ctx := cmd.Root().Context()
build := builder.New(builder.Entrypoints(args))
log := logger.FromContext(ctx)
log.DebugContext(ctx, "cue: discriminating platform instance")
build := builder.New(builder.Entrypoints(args))
log.DebugContext(ctx, "cue: building platform instance")
bd, err := build.Unify(ctx, config)
if err != nil {
return errors.Wrap(err)
}
tm, err := build.Discriminate(ctx)
tm, err := bd.TypeMeta()
if err != nil {
return errors.Wrap(err)
}
@@ -204,28 +179,8 @@ func NewPlatform(cfg *holos.Config) *cobra.Command {
if tm.Kind != "Platform" {
return errors.Format("invalid kind: want: Platform have: %s", tm.Kind)
}
log.DebugContext(ctx, fmt.Sprintf("discriminated %s %s", tm.APIVersion, tm.Kind))
switch version := tm.APIVersion; version {
case "v1alpha5":
builder, err := v1alpha5.LoadPlatform(args[0], nil)
if err != nil {
return errors.Wrap(err)
}
builder.Concurrency = concurrency
builder.Stderr = cmd.ErrOrStderr()
return render.Platform(ctx, builder)
}
// Prior to v1alpha5 we fully unified and injected tags, which was a bad
// idea because it assumed certain tags would always be passed, like
// cluster, which we made optional in v1alpha5.
log.DebugContext(ctx, "cue: building platform instance")
//nolint:staticcheck
bd, err := build.Unify(ctx, config)
if err != nil {
return errors.Wrap(err)
}
log.DebugContext(ctx, "discriminated "+tm.APIVersion+" "+tm.Kind)
jsonBytes, err := bd.Value.MarshalJSON()
if err != nil {

View File

@@ -4,7 +4,6 @@ import (
_ "embed"
"fmt"
"log/slog"
"os"
"github.com/spf13/cobra"
@@ -30,8 +29,6 @@ import (
"github.com/holos-run/holos/internal/cli/render"
"github.com/holos-run/holos/internal/cli/token"
"github.com/holos-run/holos/internal/cli/txtar"
cue "cuelang.org/go/cmd/cue/cmd"
)
//go:embed help.txt
@@ -94,9 +91,6 @@ func New(cfg *holos.Config, feature holos.Flagger) *cobra.Command {
// Server
rootCmd.AddCommand(server.New(cfg, feature))
// CUE
rootCmd.AddCommand(newCueCmd())
return rootCmd
}
@@ -112,9 +106,3 @@ func newOrgCmd(feature holos.Flagger) (cmd *cobra.Command) {
}
return cmd
}
func newCueCmd() (cmd *cobra.Command) {
cueCmd, _ := cue.New(os.Args[2:])
cmd = cueCmd.Command
return
}

View File

@@ -92,8 +92,7 @@ func cmdHolos(ts *testscript.TestScript, neg bool, args []string) {
}
}
// Disabled because these are flakey tests. Need to fix them up.
func XTestSecrets(t *testing.T) {
func TestSecrets(t *testing.T) {
// Add TestWork: true to the Params to keep the $WORK directory around.
testscript.Run(t, testscript.Params{
Dir: "testdata",

View File

@@ -1,5 +1,9 @@
# Create the secret.
exec holos create secret k3-talos --namespace=jeff --from-file $WORK/secrets.yaml
holos create secret k3-talos --namespace=jeff --from-file $WORK/secrets.yaml
stderr 'created: k3-talos-..........'
# Want no warnings.
! stderr 'WRN'
-- secrets.yaml --
content: hello

View File

@@ -1,154 +0,0 @@
// Code generated by cue get go. DO NOT EDIT.
//cue:generate cue get go github.com/holos-run/holos/api/author/v1alpha5
// Package author contains a standard set of schemas for component authors to
// generate common [core] BuildPlans.
//
// Holos values stability, flexibility, and composition. This package
// intentionally defines only the minimal necessary set of structures.
// Component authors are encouraged to define their own structures building on
// our example [topics].
//
// The Holos Maintainers may add definitions to this package if the community
// identifies nearly all users must define the exact same structure. Otherwise,
// definitions should be added as a customizable example in [topics].
//
// For example, structures representing a cluster and environment almost always
// need to be defined. Their definition varies from one organization to the
// next. Therefore, customizable definitions for a cluster and environment are
// best maintained in [topics], not standardized in this package.
//
// [core]: https://holos.run/docs/api/core/
// [topics]: https://holos.run/docs/topics/
package author
import "github.com/holos-run/holos/api/core/v1alpha5:core"
// Platform assembles a core Platform in the Resource field for the holos render
// platform command. Use the Components field to register components with the
// platform.
#Platform: {
Name: string
Components: {[string]: core.#Component} @go(,map[NameLabel]core.Component)
Resource: core.#Platform
}
// ComponentConfig represents the configuration common to all kinds of
// components for use with the holos render component command. All component
// kinds may be transformed with [kustomize] configured with the
// [KustomizeConfig] field.
//
// - [Helm] charts.
// - [Kubernetes] resources generated from CUE.
// - [Kustomize] bases.
//
// [kustomize]: https://kubectl.docs.kubernetes.io/references/kustomize/kustomization/
#ComponentConfig: {
// Name represents the BuildPlan metadata.name field. Used to construct the
// fully rendered manifest file path.
Name: string
// Path represents the path to the component producing the BuildPlan.
Path: string
// Parameters are useful to reuse a component with various parameters.
// Injected as CUE @tag variables. Parameters with a "holos_" prefix are
// reserved for use by the Holos Authors.
Parameters: {[string]: string} @go(,map[string]string)
// OutputBaseDir represents the output base directory used when assembling
// artifacts. Useful to organize components by clusters or other parameters.
// For example, holos writes resource manifests to
// {WriteTo}/{OutputBaseDir}/components/{Name}/{Name}.gen.yaml
OutputBaseDir: string & (string | *"")
// Resources represents kubernetes resources mixed into the rendered manifest.
Resources: core.#Resources
// KustomizeConfig represents the configuration kustomize.
KustomizeConfig: #KustomizeConfig
// Artifacts represents additional artifacts to mix in. Useful for adding
// GitOps resources. Each Artifact is unified without modification into the
// BuildPlan.
Artifacts: {[string]: core.#Artifact} @go(,map[NameLabel]core.Artifact)
}
// Helm assembles a BuildPlan rendering a helm chart. Useful to mix in
// additional resources from CUE and transform the helm output with kustomize.
#Helm: {
#ComponentConfig
// Chart represents a Helm chart.
Chart: core.#Chart
// Values represents data to marshal into a values.yaml for helm.
Values: core.#Values
// EnableHooks enables helm hooks when executing the `helm template` command.
EnableHooks: bool & (true | *false)
// Namespace sets the helm chart namespace flag if provided.
Namespace?: string
// BuildPlan represents the derived BuildPlan produced for the holos render
// component command.
BuildPlan: core.#BuildPlan
}
// Kubernetes assembles a BuildPlan containing inline resources exported from
// CUE.
#Kubernetes: {
#ComponentConfig
// BuildPlan represents the derived BuildPlan produced for the holos render
// component command.
BuildPlan: core.#BuildPlan
}
// Kustomize assembles a BuildPlan rendering manifests from a [kustomize]
// kustomization.
//
// [kustomize]: https://kubectl.docs.kubernetes.io/references/kustomize/kustomization/
#Kustomize: {
#ComponentConfig
// BuildPlan represents the derived BuildPlan produced for the holos render
// component command.
BuildPlan: core.#BuildPlan
}
// KustomizeConfig represents the configuration for [kustomize] post processing.
// Use the Files field to mix in plain manifest files located in the component
// directory. Use the Resources field to mix in manifests from network urls.
//
// [kustomize]: https://kubectl.docs.kubernetes.io/references/kustomize/kustomization/
#KustomizeConfig: {
// Kustomization represents the kustomization used to transform resources.
// Note the resources field is internally managed from the Files and Resources fields.
Kustomization?: {...} @go(,map[string]any)
// Files represents files to copy from the component directory for kustomization.
Files: {[string]: Source: string} & {[NAME=_]: Source: NAME} @go(,map[string]struct{Source string})
// Resources represents additional entries to included in the resources list.
Resources: {[string]: Source: string} & {[NAME=_]: Source: NAME} @go(,map[string]struct{Source string})
// CommonLabels represents common labels added without including selectors.
CommonLabels: {[string]: string} @go(,map[string]string)
}
// NameLabel represents the common use case of converting a struct to a list
// where the name field of each value unifies with the field name of the outer
// struct.
//
// For example:
//
// S: [NameLabel=string]: name: NameLabel
// S: jeff: _
// S: gary: _
// S: nate: _
// L: [for x in S {x}]
// // L is [{name: "jeff"}, {name: "gary"}, {name: "nate"}]
#NameLabel: string

View File

@@ -1,302 +0,0 @@
// Code generated by cue get go. DO NOT EDIT.
//cue:generate cue get go github.com/holos-run/holos/api/core/v1alpha5
// Package core contains schemas for a [Platform] and [BuildPlan]. Holos takes
// a [Platform] as input, then iterates over each [Component] to produce a
// [BuildPlan]. Holos processes the [BuildPlan] to produce fully rendered
// manifests, each an [Artifact].
package core
// BuildPlan represents an implementation of the [rendered manifest pattern].
// Holos processes a BuildPlan to produce one or more [Artifact] output files.
// BuildPlan artifact files usually contain Kubernetes manifests, but they may
// have any content.
//
// A BuildPlan usually produces two artifacts. One artifact contains a manifest
// of resources. A second artifact contains a GitOps resource to manage the
// first, usually an ArgoCD Application resource.
//
// Holos uses CUE to construct a BuildPlan. A future enhancement will support
// user defined executables providing a BuildPlan to Holos in the style of an
// [external credential provider].
//
// [rendered manifest pattern]: https://akuity.io/blog/the-rendered-manifests-pattern
// [external credential provider]: https://github.com/kubernetes/enhancements/blob/313ad8b59c80819659e1fbf0f165230f633f2b22/keps/sig-auth/541-external-credential-providers/README.md
#BuildPlan: {
// Kind represents the type of the resource.
kind: string & "BuildPlan" @go(Kind)
// APIVersion represents the versioned schema of the resource.
apiVersion: string & (string | *"v1alpha5") @go(APIVersion)
// Metadata represents data about the resource such as the Name.
metadata: #Metadata @go(Metadata)
// Spec specifies the desired state of the resource.
spec: #BuildPlanSpec @go(Spec)
// Source reflects the origin of the BuildPlan.
source?: #BuildPlanSource @go(Source)
}
// BuildPlanSpec represents the specification of the [BuildPlan].
#BuildPlanSpec: {
// Artifacts represents the artifacts for holos to build.
artifacts: [...#Artifact] @go(Artifacts,[]Artifact)
// Disabled causes the holos cli to disregard the build plan.
disabled?: bool @go(Disabled)
}
// BuildPlanSource reflects the origin of a [BuildPlan]. Useful to save a build
// plan to a file, then re-generate it without needing to process a [Platform]
// component collection.
#BuildPlanSource: {
// Component reflects the component that produced the build plan.
component?: #Component @go(Component)
}
// Artifact represents one fully rendered manifest produced by a [Transformer]
// sequence, which transforms a [Generator] collection. A [BuildPlan] produces
// an [Artifact] collection.
//
// Each Artifact produces one manifest file artifact. Generator Output values
// are used as Transformer Inputs. The Output field of the final [Transformer]
// should have the same value as the Artifact field.
//
// When there is more than one [Generator] there must be at least one
// [Transformer] to combine outputs into one Artifact. If there is a single
// Generator, it may directly produce the Artifact output.
//
// An Artifact is processed concurrently with other artifacts in the same
// [BuildPlan]. An Artifact should not use an output from another Artifact as
// an input. Each [Generator] may also run concurrently. Each [Transformer] is
// executed sequentially starting after all generators have completed.
//
// Output fields are write-once. It is an error for multiple Generators or
// Transformers to produce the same Output value within the context of a
// [BuildPlan].
#Artifact: {
artifact?: #FilePath @go(Artifact)
generators?: [...#Generator] @go(Generators,[]Generator)
transformers?: [...#Transformer] @go(Transformers,[]Transformer)
skip?: bool @go(Skip)
}
// Generator generates Kubernetes resources. [Helm] and [Resources] are the
// most commonly used, often paired together to mix-in resources to an
// unmodified Helm chart. A simple [File] generator is also available for use
// with the [Kustomize] transformer.
//
// Each Generator in an [Artifact] must have a distinct Output value for a
// [Transformer] to reference.
//
// 1. [Resources] - Generates resources from CUE code.
// 2. [Helm] - Generates rendered yaml from a [Chart].
// 3. [File] - Generates data by reading a file from the component directory.
#Generator: {
// Kind represents the kind of generator. Must be Resources, Helm, or File.
kind: string & ("Resources" | "Helm" | "File") @go(Kind)
// Output represents a file for a Transformer or Artifact to consume.
output: #FilePath @go(Output)
// Resources generator. Ignored unless kind is Resources. Resources are
// stored as a two level struct. The top level key is the Kind of resource,
// e.g. Namespace or Deployment. The second level key is an arbitrary
// InternalLabel. The third level is a map[string]any representing the
// Resource.
resources?: #Resources @go(Resources)
// Helm generator. Ignored unless kind is Helm.
helm?: #Helm @go(Helm)
// File generator. Ignored unless kind is File.
file?: #File @go(File)
}
// Resource represents one kubernetes api object.
#Resource: {...}
// Resources represents Kubernetes resources. Most commonly used to mix
// resources into the [BuildPlan] generated from CUE, but may be generated from
// elsewhere.
#Resources: {[string]: [string]: #Resource}
// File represents a simple single file copy [Generator]. Useful with a
// [Kustomize] [Transformer] to process plain manifest files stored in the
// component directory. Multiple File generators may be used to transform
// multiple resources.
#File: {
// Source represents a file sub-path relative to the component path.
source: #FilePath @go(Source)
}
// Helm represents a [Chart] manifest [Generator].
#Helm: {
// Chart represents a helm chart to manage.
chart: #Chart @go(Chart)
// Values represents values for holos to marshal into values.yaml when
// rendering the chart.
values: #Values @go(Values)
// EnableHooks enables helm hooks when executing the `helm template` command.
enableHooks?: bool @go(EnableHooks)
// Namespace represents the helm namespace flag
namespace?: string @go(Namespace)
}
// Values represents [Helm] Chart values generated from CUE.
#Values: {...}
// Chart represents a [Helm] Chart.
#Chart: {
// Name represents the chart name.
name: string @go(Name)
// Version represents the chart version.
version: string @go(Version)
// Release represents the chart release when executing helm template.
release: string @go(Release)
// Repository represents the repository to fetch the chart from.
repository?: #Repository @go(Repository)
}
// Repository represents a [Helm] [Chart] repository.
#Repository: {
name: string @go(Name)
url: string @go(URL)
}
// Transformer combines multiple inputs from prior [Generator] or [Transformer]
// outputs into one output. [Kustomize] is the most commonly used transformer.
// A simple [Join] is also supported for use with plain manifest files.
//
// 1. [Kustomize] - Patch and transform the output from prior generators or
// transformers. See [Introduction to Kustomize].
// 2. [Join] - Concatenate multiple prior outputs into one output.
//
// [Introduction to Kustomize]: https://kubectl.docs.kubernetes.io/guides/config_management/introduction/
#Transformer: {
// Kind represents the kind of transformer. Must be Kustomize, or Join.
kind: string & ("Kustomize" | "Join") @go(Kind)
// Inputs represents the files to transform. The Output of prior Generators
// and Transformers.
inputs: [...#FilePath] @go(Inputs,[]FilePath)
// Output represents a file for a subsequent Transformer or Artifact to
// consume.
output: #FilePath @go(Output)
// Kustomize transformer. Ignored unless kind is Kustomize.
kustomize?: #Kustomize @go(Kustomize)
// Join transformer. Ignored unless kind is Join.
join?: #Join @go(Join)
}
// Join represents a [Transformer] using [bytes.Join] to concatenate multiple
// inputs into one output with a separator. Useful for combining output from
// [Helm] and [Resources] together into one [Artifact] when [Kustomize] is
// otherwise unnecessary.
//
// [bytes.Join]: https://pkg.go.dev/bytes#Join
#Join: {
separator: string & (string | *"---\n") @go(Separator)
}
// Kustomize represents a kustomization [Transformer].
#Kustomize: {
// Kustomization represents the decoded kustomization.yaml file
kustomization: #Kustomization @go(Kustomization)
// Files holds file contents for kustomize, e.g. patch files.
files?: #FileContentMap @go(Files)
}
// Kustomization represents a kustomization.yaml file for use with the
// [Kustomize] [Transformer]. Untyped to avoid tightly coupling holos to
// kubectl versions which was a problem for the Flux maintainers. Type checking
// is expected to happen in CUE against the kubectl version the user prefers.
#Kustomization: {...}
// FileContent represents file contents.
#FileContent: string
// FileContentMap represents a mapping of file paths to file contents.
#FileContentMap: {[string]: #FileContent}
// FilePath represents a file path.
#FilePath: string
// InternalLabel is an arbitrary unique identifier internal to holos itself.
// The holos cli is expected to never write a InternalLabel value to rendered
// output files, therefore use a InternalLabel when the identifier must be
// unique and internal. Defined as a type for clarity and type checking.
#InternalLabel: string
// Kind is a discriminator. Defined as a type for clarity and type checking.
#Kind: string
// Metadata represents data about the resource such as the Name.
#Metadata: {
// Name represents the resource name.
name: string @go(Name)
}
// Platform represents a platform to manage. A Platform specifies a [Component]
// collection and integrates the components together into a holistic platform.
// Holos iterates over the [Component] collection producing a [BuildPlan] for
// each, which holos then executes to render manifests.
//
// Inspect a Platform resource holos would process by executing:
//
// cue export --out yaml ./platform
#Platform: {
// Kind is a string value representing the resource.
kind: string & "Platform" @go(Kind)
// APIVersion represents the versioned schema of this resource.
apiVersion: string & (string | *"v1alpha5") @go(APIVersion)
// Metadata represents data about the resource such as the Name.
metadata: #Metadata @go(Metadata)
// Spec represents the platform specification.
spec: #PlatformSpec @go(Spec)
}
// PlatformSpec represents the platform specification.
#PlatformSpec: {
// Components represents a collection of holos components to manage.
components: [...#Component] @go(Components,[]Component)
}
// Component represents the complete context necessary to produce a [BuildPlan]
// from a path containing parameterized CUE configuration.
#Component: {
// Name represents the name of the component. Injected as the tag variable
// "holos_component_name".
name: string @go(Name)
// Path represents the path of the component relative to the platform root.
// Injected as the tag variable "holos_component_path".
path: string @go(Path)
// 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)
// Parameters represent user defined input variables to produce various
// [BuildPlan] resources from one component path. Injected as CUE @tag
// variables. Parameters with a "holos_" prefix are reserved for use by the
// Holos Authors. Multiple environments are a prime example of an input
// parameter that should always be user defined, never defined by Holos.
parameters?: {[string]: string} @go(Parameters,map[string]string)
}

View File

@@ -1,177 +0,0 @@
package author
import (
ks "sigs.k8s.io/kustomize/api/types"
core "github.com/holos-run/holos/api/core/v1alpha5:core"
)
#Platform: {
Name: string | *"no-platform-name"
Components: _
Resource: core.#Platform & {
metadata: name: Name
spec: components: [for x in Components {x}]
}
}
#KustomizeConfig: {
CommonLabels: _
Kustomization: ks.#Kustomization & {
apiVersion: "kustomize.config.k8s.io/v1beta1"
kind: "Kustomization"
_labels: commonLabels: {
includeSelectors: false
pairs: CommonLabels
}
labels: [for x in _labels {x}]
}
}
// Kustomize and Kubernetes are identical.
// https://holos.run/docs/next/api/author/#Kustomize
#Kustomize: #Kubernetes
// https://holos.run/docs/next/api/author/#Kubernetes
#Kubernetes: {
Name: _
Path: _
Parameters: _
Resources: _
OutputBaseDir: _
KustomizeConfig: _
Artifacts: {
HolosComponent: {
_path: string
if OutputBaseDir == "" {
_path: "components/\(Name)"
}
if OutputBaseDir != "" {
_path: "\(OutputBaseDir)/components/\(Name)"
}
artifact: "\(_path)/\(Name).gen.yaml"
let ResourcesOutput = "resources.gen.yaml"
generators: [
{
kind: "Resources"
output: ResourcesOutput
resources: Resources
},
for x in KustomizeConfig.Files {
kind: "File"
output: x.Source
file: source: x.Source
},
]
transformers: [
core.#Transformer & {
kind: "Kustomize"
inputs: [for x in generators {x.output}]
output: artifact
kustomize: kustomization: KustomizeConfig.Kustomization & {
resources: [
ResourcesOutput,
for x in KustomizeConfig.Files {x.Source},
for x in KustomizeConfig.Resources {x.Source},
]
}
},
]
}
}
BuildPlan: {
metadata: name: Name
spec: artifacts: [for x in Artifacts {x}]
source: component: {
name: Name
path: Path
parameters: Parameters
}
}
}
// https://holos.run/docs/next/api/author/#Helm
#Helm: {
Name: _
Path: _
Parameters: _
Resources: _
OutputBaseDir: _
KustomizeConfig: _
Chart: {
name: string | *Name
release: string | *name
}
Values: _
EnableHooks: _
Namespace?: _
Artifacts: {
HolosComponent: {
_path: string
if OutputBaseDir == "" {
_path: "components/\(Name)"
}
if OutputBaseDir != "" {
_path: "\(OutputBaseDir)/components/\(Name)"
}
artifact: "\(_path)/\(Name).gen.yaml"
let HelmOutput = "helm.gen.yaml"
let ResourcesOutput = "resources.gen.yaml"
generators: [
{
kind: "Helm"
output: HelmOutput
helm: core.#Helm & {
chart: Chart
values: Values
enableHooks: EnableHooks
if Namespace != _|_ {
namespace: Namespace
}
}
},
{
kind: "Resources"
output: ResourcesOutput
resources: Resources
},
for x in KustomizeConfig.Files {
kind: "File"
output: x.Source
file: source: x.Source
},
]
transformers: [
core.#Transformer & {
kind: "Kustomize"
inputs: [for x in generators {x.output}]
output: artifact
kustomize: kustomization: KustomizeConfig.Kustomization & {
resources: [
HelmOutput,
ResourcesOutput,
for x in KustomizeConfig.Files {x.Source},
for x in KustomizeConfig.Resources {x.Source},
]
}
},
]
}
}
BuildPlan: {
metadata: name: Name
spec: artifacts: [for x in Artifacts {x}]
source: component: {
name: Name
path: Path
parameters: Parameters
}
}
}

View File

@@ -1,13 +0,0 @@
package v1alpha5
#Transformer: {
kind: _
if kind == "Kustomize" {
kustomize: _
}
if kind == "Join" {
join: _
}
}

View File

@@ -1,9 +0,0 @@
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
vendor/
node_modules/

View File

@@ -1,10 +0,0 @@
package holos
import "github.com/holos-run/holos/api/author/v1alpha5:author"
Platform: author.#Platform & {
Name: "default"
}
// Render a Platform resource for holos to process
holos: Platform.Resource

View File

@@ -1,49 +0,0 @@
package holos
import (
corev1 "k8s.io/api/core/v1"
appsv1 "k8s.io/api/apps/v1"
rbacv1 "k8s.io/api/rbac/v1"
batchv1 "k8s.io/api/batch/v1"
ci "cert-manager.io/clusterissuer/v1"
rgv1 "gateway.networking.k8s.io/referencegrant/v1beta1"
certv1 "cert-manager.io/certificate/v1"
hrv1 "gateway.networking.k8s.io/httproute/v1"
gwv1 "gateway.networking.k8s.io/gateway/v1"
ap "argoproj.io/appproject/v1alpha1"
es "external-secrets.io/externalsecret/v1beta1"
ss "external-secrets.io/secretstore/v1beta1"
)
#Resources: {
[Kind=string]: [InternalLabel=string]: {
kind: Kind
metadata: name: string | *InternalLabel
}
AppProject?: [_]: ap.#AppProject
Certificate?: [_]: certv1.#Certificate
ClusterIssuer?: [_]: ci.#ClusterIssuer
ClusterRole?: [_]: rbacv1.#ClusterRole
ClusterRoleBinding?: [_]: rbacv1.#ClusterRoleBinding
ConfigMap?: [_]: corev1.#ConfigMap
CronJob?: [_]: batchv1.#CronJob
Deployment?: [_]: appsv1.#Deployment
ExternalSecret?: [_]: es.#ExternalSecret
HTTPRoute?: [_]: hrv1.#HTTPRoute
Job?: [_]: batchv1.#Job
Namespace?: [_]: corev1.#Namespace
ReferenceGrant?: [_]: rgv1.#ReferenceGrant
Role?: [_]: rbacv1.#Role
RoleBinding?: [_]: rbacv1.#RoleBinding
Secret?: [_]: corev1.#Secret
SecretStore?: [_]: ss.#SecretStore
Service?: [_]: corev1.#Service
ServiceAccount?: [_]: corev1.#ServiceAccount
StatefulSet?: [_]: appsv1.#StatefulSet
Gateway?: [_]: gwv1.#Gateway & {
spec: gatewayClassName: string | *"istio"
}
}

View File

@@ -1,27 +0,0 @@
package holos
import "github.com/holos-run/holos/api/author/v1alpha5:author"
#ComponentConfig: author.#ComponentConfig & {
Name: _Tags.component.name
Path: _Tags.component.path
Resources: #Resources
}
// https://holos.run/docs/api/author/v1alpha5/#Kubernetes
#Kubernetes: close({
#ComponentConfig
author.#Kubernetes
})
// https://holos.run/docs/api/author/v1alpha5/#Kustomize
#Kustomize: close({
#ComponentConfig
author.#Kustomize
})
// https://holos.run/docs/api/author/v1alpha5/#Helm
#Helm: close({
#ComponentConfig
author.#Helm
})

View File

@@ -1,12 +0,0 @@
package holos
import "github.com/holos-run/holos/api/core/v1alpha5:core"
// Note: tags should have a reasonable default value for cue export.
_Tags: {
// Standardized parameters
component: core.#Component & {
name: string | *"no-name" @tag(holos_component_name, type=string)
path: string | *"no-path" @tag(holos_component_path, type=string)
}
}

View File

@@ -42,7 +42,7 @@ type Flagger interface {
type EnvFlagger struct{}
// Flag returns true if feature name is enabled.
func (e *EnvFlagger) Flag(name feature) bool {
return os.Getenv(fmt.Sprintf("HOLOS_FEATURE_%s", name)) != ""
envVar := "HOLOS_FEATURE_" + strings.ToUpper(string(name))
return os.Getenv(envVar) != ""
}

View File

@@ -1,12 +1,5 @@
package util
import (
"os"
"path/filepath"
"github.com/holos-run/holos/internal/errors"
)
// EnsureNewline adds a trailing newline if not already there.
func EnsureNewline(b []byte) []byte {
if len(b) > 0 && b[len(b)-1] != '\n' {
@@ -14,44 +7,3 @@ func EnsureNewline(b []byte) []byte {
}
return b
}
// FindCueMod returns the root module location containing the cue.mod.
func FindCueMod(target string) (dir string, err error) {
dir, err = filepath.Abs(target)
if err != nil {
err = errors.Wrap(err)
return
}
for {
if _, err = os.Stat(filepath.Join(dir, "cue.mod")); err == nil {
break
} else if !os.IsNotExist(err) {
return "", errors.Wrap(err)
}
parent := filepath.Dir(dir)
if parent == dir {
return "", errors.Format("no cue.mod from root to leaf: %v", target)
}
dir = parent
}
return
}
func FindRootLeaf(target string) (root string, leaf string, err error) {
if root, err = FindCueMod(target); err != nil {
return "", "", errors.Wrap(err)
}
absPath, err := filepath.Abs(target)
if err != nil {
return "", "", errors.Wrap(err)
}
if leaf, err = filepath.Rel(root, absPath); err != nil {
return "", "", errors.Wrap(err)
}
// Needed for CUE to load the path properly.
leaf = "." + string(os.PathSeparator) + leaf
return
}

View File

@@ -1 +1 @@
98
97

View File

@@ -1 +1 @@
0
3