Compare commits

..

2 Commits

Author SHA1 Message Date
Jeff McCune
670d716403 (#175) Add podinfo oci example
This patch adds to more example helm chart based components.  podinfo
installs as a normal https repository based helm chart.  podinfo-oci
uses an oci image to manage the helm chart.

The way holos handls OCI images is subtle, so it's good to include an
example right out of the chute.  Github actions uses OCI images for
example.
2024-05-21 12:36:45 -07:00
Jeff McCune
bba3895f35 (#175) Add holos generate component helm command
This patch adds a schematic to generate a holos component that wraps a
helm chart.  The cert-manager chart is the current example.

Usage:

```bash
set -euo pipefail

rm -rf ~/holos/dev/bare
mkdir ~/holos/dev/bare
cd ~/holos/dev/bare

holos generate platform bare
holos pull platform config .
holos render platform ./platform/
(cd components && holos generate component helm cert-manager)
```

The chart builds:

```bash
holos build ./components/cert-manager | yq .
```

And renders:

```bash
holos render component ./components/cert-manager --cluster-name k2
find deploy -type f
```

```txt
9:41PM INF render.go:83 rendered cert-manager version=0.81.1 cluster=k2 status=ok action=rendered name=cert-manager
deploy/clusters/k2/holos/components/cert-manager-kustomization.gen.yaml
deploy/clusters/k2/components/cert-manager/cert-manager.gen.yaml
```
2024-05-21 11:05:53 -07:00
23 changed files with 310 additions and 29 deletions

View File

@@ -0,0 +1,3 @@
package holos
_platform_config: string @tag(platform_config, type=string)

View File

@@ -0,0 +1 @@
{}

View File

@@ -10,6 +10,7 @@ import (
"github.com/holos-run/holos/internal/client"
"github.com/holos-run/holos/internal/errors"
"github.com/holos-run/holos/internal/holos"
"github.com/holos-run/holos/internal/server/middleware/logger"
"github.com/spf13/cobra"
)
@@ -17,6 +18,7 @@ import (
func makeBuildRunFunc(cfg *client.Config) command.RunFunc {
return func(cmd *cobra.Command, args []string) error {
ctx := cmd.Root().Context()
logger.FromContext(ctx).DebugContext(ctx, "RunE", "args", args)
build := builder.New(builder.Entrypoints(args), builder.Cluster(cfg.Holos().ClusterName()))
results, err := build.Run(ctx, cfg)
if err != nil {

View File

@@ -2,6 +2,8 @@ package generate
import (
"fmt"
"log/slog"
"path/filepath"
"strings"
"github.com/holos-run/holos/internal/cli/command"
@@ -41,6 +43,7 @@ func NewPlatform(cfg *holos.Config) *cobra.Command {
return errors.Wrap(err)
}
}
return nil
}
@@ -53,17 +56,27 @@ func NewComponent() *cobra.Command {
cmd.Short = "generate a component from an embedded schematic"
cmd.AddCommand(NewCueComponent())
cmd.AddCommand(NewHelmComponent())
return cmd
}
func NewHelmComponent() *cobra.Command {
cmd := command.New("helm")
cmd.Short = "generate a helm component from a schematic"
for _, name := range generate.HelmComponents() {
cmd.AddCommand(makeHelmCommand(name))
}
return cmd
}
func NewCueComponent() *cobra.Command {
cmd := command.New("cue")
cmd.Short = "generate a cue component from an embedded schematic"
cmd.Short = "generate a cue component from a schematic"
components := generate.CueComponents()
cmd.Long = fmt.Sprintf("Embedded cue components available to generate:\n\n %s", strings.Join(components, "\n "))
for _, name := range components {
for _, name := range generate.CueComponents() {
cmd.AddCommand(makeCueCommand(name))
}
return cmd
@@ -87,3 +100,26 @@ func makeCueCommand(name string) *cobra.Command {
return cmd
}
func makeHelmCommand(name string) *cobra.Command {
cmd := command.New(name)
cfg, err := generate.NewSchematic(filepath.Join("components", "helm"), name)
if err != nil {
slog.Error("could not get schematic", "err", err)
return nil
}
cmd.Short = cfg.Short
cmd.Long = cfg.Long
cmd.Args = cobra.NoArgs
cmd.Flags().AddGoFlagSet(cfg.FlagSet())
cmd.RunE = func(cmd *cobra.Command, args []string) error {
ctx := cmd.Root().Context()
if err := generate.GenerateHelmComponent(ctx, name, cfg); err != nil {
return errors.Wrap(err)
}
return nil
}
return cmd
}

View File

@@ -4,6 +4,7 @@ import (
"bytes"
"context"
"embed"
"encoding/json"
"flag"
"io/fs"
"log/slog"
@@ -21,6 +22,65 @@ var components embed.FS
// componentsRoot is the root path to copy component cue code from.
const componentsRoot = "components"
func NewSchematic(root string, name string) (*Schematic, error) {
data, err := components.ReadFile(filepath.Join(root, name, "schematic.json"))
if err != nil {
return nil, errors.Wrap(err)
}
schematic := Schematic{Name: name}
if err := json.Unmarshal(data, &schematic); err != nil {
return nil, errors.Wrap(err)
}
return &schematic, nil
}
// Schematic represents the flags and command metadata stored in the
// schematic.yaml file along side each schematic.
type Schematic struct {
// Name represents the name of the resource the schematic generates.
Name string `json:"name,omitempty" yaml:"name,omitempty"`
Short string `json:"short,omitempty" yaml:"short,omitempty"`
Long string `json:"long,omitempty" yaml:"long,omitempty"`
Chart *string `json:"chart,omitempty" yaml:"chart,omitempty"`
Version *string `json:"version,omitempty" yaml:"version,omitempty"`
Namespace *string `json:"namespace,omitempty" yaml:"namespace,omitempty"`
RepoName *string `json:"reponame,omitempty" yaml:"reponame,omitempty"`
RepoURL *string `json:"repourl,omitempty" yaml:"repourl,omitempty"`
flagSet *flag.FlagSet
}
func (s *Schematic) FlagSet() *flag.FlagSet {
if s == nil {
return nil
}
if s.flagSet != nil {
return s.flagSet
}
fs := flag.NewFlagSet("", flag.ContinueOnError)
fs.StringVar(&s.Name, "name", s.Name, "component name")
if s.Chart != nil {
fs.StringVar(s.Chart, "chart", *s.Chart, "chart name")
}
if s.Version != nil {
fs.StringVar(s.Version, "component-version", *s.Version, "component version")
}
if s.Namespace != nil {
fs.StringVar(s.Namespace, "namespace", *s.Namespace, "namespace")
}
if s.RepoName != nil {
fs.StringVar(s.RepoName, "repo-name", *s.RepoName, "chart repository name")
}
if s.RepoURL != nil {
fs.StringVar(s.RepoURL, "repo-url", *s.RepoURL, "chart repository url")
}
s.flagSet = fs
return fs
}
// CueConfig represents the config values passed to cue go templates.
type CueConfig struct {
ComponentName string
@@ -40,6 +100,24 @@ func (c *CueConfig) FlagSet() *flag.FlagSet {
return fs
}
type HelmConfig struct {
ComponentName string
flagSet *flag.FlagSet
}
func (c *HelmConfig) FlagSet(name string) *flag.FlagSet {
if c == nil {
return nil
}
if c.flagSet != nil {
return c.flagSet
}
fs := flag.NewFlagSet("", flag.ContinueOnError)
fs.StringVar(&c.ComponentName, "name", name, "component name")
c.flagSet = fs
return fs
}
// CueComponents returns a slice of embedded component schematics or nil if there are none.
func CueComponents() []string {
entries, err := fs.ReadDir(components, filepath.Join(componentsRoot, "cue"))
@@ -53,8 +131,21 @@ func CueComponents() []string {
return dirs
}
// HelmComponents returns a slice of embedded component schematics or nil if there are none.
func HelmComponents() []string {
entries, err := fs.ReadDir(components, filepath.Join(componentsRoot, "helm"))
if err != nil {
return nil
}
dirs := make([]string, 0, len(entries))
for _, entry := range entries {
dirs = append(dirs, entry.Name())
}
return dirs
}
// makeRenderFunc makes a template rendering function for embedded files.
func makeRenderFunc(log *slog.Logger, path string, cfg *CueConfig) func([]byte) *bytes.Buffer {
func makeRenderFunc[T any](log *slog.Logger, path string, cfg T) func([]byte) *bytes.Buffer {
return func(content []byte) *bytes.Buffer {
tmpl, err := template.New(filepath.Base(path)).Parse(string(content))
if err != nil {
@@ -91,3 +182,23 @@ func GenerateCueComponent(ctx context.Context, name string, cfg *CueConfig) erro
log.InfoContext(ctx, "generated component")
return nil
}
// GenerateHelmComponent writes the cue code for a component to the local working
// directory.
func GenerateHelmComponent(ctx context.Context, name string, cfg *Schematic) error {
path := filepath.Join(componentsRoot, "helm", name)
dstPath := filepath.Join(getCwd(ctx), cfg.Name)
log := logger.FromContext(ctx).With("name", cfg.Name, "path", dstPath)
log.DebugContext(ctx, "mkdir")
if err := os.MkdirAll(dstPath, os.ModePerm); err != nil {
return errors.Wrap(err)
}
mapper := makeRenderFunc(log, path, cfg)
if err := copyEmbedFS(ctx, components, path, dstPath, mapper); err != nil {
return errors.Wrap(err)
}
log.InfoContext(ctx, "generated component")
return nil
}

View File

@@ -1,6 +1,7 @@
package holos
import v1 "github.com/holos-run/holos/api/v1alpha1"
import "encoding/yaml"
let ComponentName = "{{ .ComponentName }}"
@@ -16,7 +17,7 @@ v1.#BuildPlan & {
// OBJECTS represents the kubernetes api objects to manage.
let OBJECTS = v1.#APIObjects & {
// Add Kubernetes API Objects to manage here.
// Add Kubernetes API Objects to manage here.
apiObjects: ConfigMap: "\(ComponentName)": {
metadata: {
name: ComponentName

View File

@@ -0,0 +1,20 @@
package holos
// Produce a helm chart build plan.
(#Helm & Chart).Output
let Chart = {
Name: "{{ .Name }}"
Version: "{{ .Version }}"
Namespace: "{{ .Namespace }}"
Repo: name: "{{ .RepoName }}"
Repo: url: "{{ .RepoURL }}"
Values: {
installCRDs: true
startupapicheck: enabled: false
// Must not use kube-system on gke autopilot. GKE Warden blocks access.
global: leaderElection: namespace: Namespace
}
}

View File

@@ -0,0 +1,10 @@
{
"name": "cert-manager",
"short": "cloud native certificate management",
"long": "Automatically provision and manage TLS certificates in Kubernetes",
"chart": "cert-manager",
"version": "1.14.5",
"namespace": "cert-manager",
"reponame": "jetstack",
"repourl": "https://charts.jetstack.io"
}

View File

@@ -0,0 +1,15 @@
package holos
// Produce a helm chart build plan.
(#Helm & Chart).Output
let Chart = {
Name: "{{ .Name }}"
Version: "{{ .Version }}"
Namespace: "{{ .Namespace }}"
// OCI helm charts use the image url as the chart name
Chart: chart: name: "{{ .Chart }}"
Values: {}
}

View File

@@ -0,0 +1,8 @@
{
"name": "podinfo-oci",
"short": "oci helm chart example",
"long": "Podinfo is a tiny web application made with Go that showcases best practices of running microservices in Kubernetes.",
"chart": "oci://ghcr.io/stefanprodan/charts/podinfo",
"version": "6.6.2",
"namespace": "default"
}

View File

@@ -0,0 +1,15 @@
package holos
// Produce a helm chart build plan.
(#Helm & Chart).Output
let Chart = {
Name: "{{ .Name }}"
Version: "{{ .Version }}"
Namespace: "{{ .Namespace }}"
Repo: name: "{{ .RepoName }}"
Repo: url: "{{ .RepoURL }}"
Values: {}
}

View File

@@ -0,0 +1,10 @@
{
"name": "podinfo",
"short": "simple helm chart example",
"long": "Podinfo is a tiny web application made with Go that showcases best practices of running microservices in Kubernetes.",
"chart": "podinfo",
"reponame": "podinfo",
"repourl": "https://stefanprodan.github.io/podinfo",
"version": "6.6.2",
"namespace": "default"
}

View File

@@ -41,17 +41,25 @@ func copyEmbedFS(ctx context.Context, srcFS embed.FS, srcPath, dstPath string, m
return errors.Wrap(err)
}
log.DebugContext(ctx, "created", "directory", dstFullPath)
} else {
data, err := srcFS.ReadFile(path)
if err != nil {
return errors.Wrap(err)
}
buf := mapFunc(data)
if err := os.WriteFile(dstFullPath, buf.Bytes(), os.ModePerm); err != nil {
return errors.Wrap(err)
}
log.DebugContext(ctx, "wrote", "file", dstFullPath)
return nil
}
if filepath.Base(path) == "schematic.json" {
log.DebugContext(ctx, "skipped", "file", dstFullPath)
return nil
}
data, err := srcFS.ReadFile(path)
if err != nil {
return errors.Wrap(err)
}
buf := mapFunc(data)
if err := os.WriteFile(dstFullPath, buf.Bytes(), os.ModePerm); err != nil {
return errors.Wrap(err)
}
log.DebugContext(ctx, "wrote", "file", dstFullPath)
return nil
})
}

View File

@@ -0,0 +1,33 @@
package holos
import "encoding/yaml"
import v1 "github.com/holos-run/holos/api/v1alpha1"
// #Helm represents a holos build plan composed of one or more helm charts.
#Helm: {
Name: string
Version: string
Namespace: string
Repo: {
name: string | *""
url: string | *""
}
Values: {...}
Chart: v1.#HelmChart & {
metadata: name: string | *Name
namespace: string | *Namespace
chart: name: string | *Name
chart: version: string | *Version
chart: repository: Repo
// Render the values to yaml for holos to provide to helm.
valuesContent: yaml.Marshal(Values)
}
// output represents the build plan provided to the holos cli.
Output: v1.#BuildPlan & {
spec: components: helmChartList: [Chart]
}
}

View File

@@ -1,6 +1,7 @@
package holos
import "encoding/yaml"
import v1 "github.com/holos-run/holos/api/v1alpha1"
// Provide a BuildPlan to the holos cli to render k8s api objects.

View File

@@ -140,7 +140,7 @@ let FormBuilder = v1.#FormBuilder & {
required: true
}
validation: messages: {
pattern: "It must be \(props.minLength) to \(props.maxLength) lowercase letters, digits, or hyphens. It must start with a letter. Trailing hyphens are prohibited."
pattern: "It must be \(props.minLength) to \(props.maxLength) lowercase letters, digits, or hyphens. It must start with a letter. Trailing hyphens are prohibited."
minLength: "Must be at least \(props.minLength) characters."
maxLength: "Must be at most \(props.maxLength) characters."
}

View File

@@ -3,11 +3,12 @@ package holos
import "encoding/json"
import v1 "github.com/holos-run/holos/api/v1alpha1"
import dto "github.com/holos-run/holos/service/gen/holos/object/v1alpha1:object"
// _PlatformConfig represents all of the data passed from holos to cue.
// Intended to carry the platform model and project models.
_PlatformConfig: dto.#PlatformConfig & json.Unmarshal(_PlatformConfigJSON)
_PlatformConfig: dto.#PlatformConfig & json.Unmarshal(_PlatformConfigJSON)
_PlatformConfigJSON: string | *"{}" @tag(platform_config, type=string)
// _Platform provides a platform resource to the holos cli for rendering. The
@@ -16,7 +17,9 @@ _PlatformConfigJSON: string | *"{}" @tag(platform_config, type=string)
// resource itself is output once when rendering the entire platform, see the
// platform/ subdirectory.
_Platform: v1.#Platform & {
metadata: name: string | *"bare" @tag(platform_name, type=string)
metadata: {
name: string | *"bare" @tag(platform_name, type=string)
}
// spec is the platform specification
spec: {
@@ -28,7 +31,7 @@ _Platform: v1.#Platform & {
_components: {
for WorkloadCluster in _Clusters.Workload {
"\(WorkloadCluster)-configmap": {
path: "components/configmap"
path: "components/configmap"
cluster: WorkloadCluster
}
}

View File

@@ -4,3 +4,12 @@ package v1alpha1
apiVersion: #APIVersion
kind: #BuildPlanKind
}
#HelmChart: {
apiVersion: #APIVersion
kind: "HelmChart"
metadata: name: string
chart: name: string | *metadata.name
chart: release: string | *metadata.name
}

View File

@@ -1,8 +0,0 @@
package v1alpha1
// #BuildPlan is the API contract between CUE and the Holos cli.
// Holos requires CUE to evaluate and provide a valid #BuildPlan.
#BuildPlan: {
kind: #BuildPlanKind
apiVersion: #APIVersion
}

View File

@@ -1,6 +1,7 @@
package holos
import "encoding/yaml"
import v1 "github.com/holos-run/holos/api/v1alpha1"
let PLATFORM = {message: "TODO: Load the platform from the API."}

View File

@@ -1 +1 @@
0
2