mirror of
https://github.com/holos-run/holos.git
synced 2026-03-19 00:37:45 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
670d716403 | ||
|
|
bba3895f35 |
3
docs/examples/platform.config.cue
Normal file
3
docs/examples/platform.config.cue
Normal file
@@ -0,0 +1,3 @@
|
||||
package holos
|
||||
|
||||
_platform_config: string @tag(platform_config, type=string)
|
||||
1
docs/examples/platform.config.json
Normal file
1
docs/examples/platform.config.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
15
internal/generate/components/helm/podinfo-oci/podinfo.cue
Normal file
15
internal/generate/components/helm/podinfo-oci/podinfo.cue
Normal 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: {}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
15
internal/generate/components/helm/podinfo/podinfo.cue
Normal file
15
internal/generate/components/helm/podinfo/podinfo.cue
Normal 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: {}
|
||||
}
|
||||
10
internal/generate/components/helm/podinfo/schematic.json
Normal file
10
internal/generate/components/helm/podinfo/schematic.json
Normal 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"
|
||||
}
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
33
internal/generate/platforms/bare/buildplan.cue
Normal file
33
internal/generate/platforms/bare/buildplan.cue
Normal 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]
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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."}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
0
|
||||
2
|
||||
|
||||
Reference in New Issue
Block a user