Compare commits

..

2 Commits

Author SHA1 Message Date
Jeff McCune
c4612ff5d2 (#64) Manage one system namespace per project
This patch introduces a new BuildPlan spec.components.resources
collection, which is a map version of
spec.components.kubernetesObjectsList.  The map version is much easier
to work with and produce in CUE than the list version.

The list version should be deprecated and removed prior to public
release.

The projects holos instance renders multiple holos components, each
containing kubernetes api objects defined directly in CUE.

<project>-system is intended for the ext auth proxy providers for all
stages.

<project>-namespaces is intended to create a namespace for each
environment in the project.

The intent is to expand the platform level definition of a project to
include the per-stage auth proxy and per-env role bindings.  Secret
Store and ESO creds refresher resources will also be defined by the
platform level definition of a project.
2024-03-26 12:23:01 -07:00
Jeff McCune
d70acbb47e ignore .vscode 2024-03-22 21:22:06 -07:00
20 changed files with 145 additions and 31 deletions

1
.gitignore vendored
View File

@@ -5,3 +5,4 @@ coverage.out
dist/
*.hold/
/deploy/
.vscode/

View File

@@ -19,9 +19,10 @@ type BuildPlanSpec struct {
}
type BuildPlanComponents struct {
HelmChartList []HelmChart `json:"helmChartList,omitempty" yaml:"helmChartList,omitempty"`
KubernetesObjectsList []KubernetesObjects `json:"kubernetesObjectsList,omitempty" yaml:"kubernetesObjectsList,omitempty"`
KustomizeBuildList []KustomizeBuild `json:"kustomizeBuildList,omitempty" yaml:"kustomizeBuildList,omitempty"`
HelmChartList []HelmChart `json:"helmChartList,omitempty" yaml:"helmChartList,omitempty"`
KubernetesObjectsList []KubernetesObjects `json:"kubernetesObjectsList,omitempty" yaml:"kubernetesObjectsList,omitempty"`
KustomizeBuildList []KustomizeBuild `json:"kustomizeBuildList,omitempty" yaml:"kustomizeBuildList,omitempty"`
Resources map[string]KubernetesObjects `json:"resources,omitempty" yaml:"resources,omitempty"`
}
func (bp *BuildPlan) Validate() error {

View File

@@ -35,7 +35,7 @@ type Repository struct {
URL string `json:"url"`
}
func (hc *HelmChart) Render(ctx context.Context, path holos.PathComponent) (*Result, error) {
func (hc *HelmChart) Render(ctx context.Context, path holos.InstancePath) (*Result, error) {
result := Result{HolosComponent: hc.HolosComponent}
if err := hc.helm(ctx, &result, path); err != nil {
return nil, err
@@ -49,7 +49,7 @@ func (hc *HelmChart) Render(ctx context.Context, path holos.PathComponent) (*Res
// runHelm provides the values produced by CUE to helm template and returns
// the rendered kubernetes api objects in the result.
func (hc *HelmChart) helm(ctx context.Context, r *Result, path holos.PathComponent) error {
func (hc *HelmChart) helm(ctx context.Context, r *Result, path holos.InstancePath) error {
log := logger.FromContext(ctx).With("chart", hc.Chart.Name)
if hc.Chart.Name == "" {
log.WarnContext(ctx, "skipping helm: no chart name specified, use a different component type")
@@ -121,7 +121,7 @@ func (hc *HelmChart) helm(ctx context.Context, r *Result, path holos.PathCompone
}
// cacheChart stores a cached copy of Chart in the chart subdirectory of path.
func cacheChart(ctx context.Context, path holos.PathComponent, chartDir string, chart Chart) error {
func cacheChart(ctx context.Context, path holos.InstancePath, chartDir string, chart Chart) error {
log := logger.FromContext(ctx)
cacheTemp, err := os.MkdirTemp(string(path), chartDir)

View File

@@ -2,6 +2,7 @@ package v1alpha1
import (
"context"
"github.com/holos-run/holos"
)
@@ -13,7 +14,7 @@ type KubernetesObjects struct {
}
// Render produces kubernetes api objects from the APIObjectMap
func (o *KubernetesObjects) Render(ctx context.Context, path holos.PathComponent) (*Result, error) {
func (o *KubernetesObjects) Render(ctx context.Context, path holos.InstancePath) (*Result, error) {
result := Result{HolosComponent: o.HolosComponent}
result.addObjectMap(ctx, o.APIObjectMap)
return &result, nil

View File

@@ -2,6 +2,7 @@ package v1alpha1
import (
"context"
"github.com/holos-run/holos"
"github.com/holos-run/holos/pkg/logger"
"github.com/holos-run/holos/pkg/util"
@@ -29,7 +30,7 @@ type KustomizeBuild struct {
// Render produces a Result by executing kubectl kustomize on the holos
// component path. Useful for processing raw yaml files.
func (kb *KustomizeBuild) Render(ctx context.Context, path holos.PathComponent) (*Result, error) {
func (kb *KustomizeBuild) Render(ctx context.Context, path holos.InstancePath) (*Result, error) {
log := logger.FromContext(ctx)
result := Result{HolosComponent: kb.HolosComponent}
// Run kustomize.

View File

@@ -1,8 +1,14 @@
package v1alpha1
// Label is an arbitrary unique identifier. Defined as a type for clarity and type checking.
type Label string
// Kind is a kubernetes api object kind. Defined as a type for clarity and type checking.
type Kind string
// APIObjectMap is the shape of marshalled api objects returned from cue to the
// holos cli. A map is used to improve the clarity of error messages from cue.
type APIObjectMap map[string]map[string]string
type APIObjectMap map[Kind]map[Label]string
// FileContentMap is a map of file names to file contents.
type FileContentMap map[string]string

View File

@@ -2,12 +2,13 @@ package v1alpha1
import (
"context"
"github.com/holos-run/holos"
)
type Renderer interface {
GetKind() string
Render(ctx context.Context, path holos.PathComponent) (*Result, error)
Render(ctx context.Context, path holos.InstancePath) (*Result, error)
}
// Render produces a Result representing the kubernetes api objects to
@@ -16,6 +17,6 @@ type Renderer interface {
// conceptualized as a data pipeline, for example a component may render a
// result by first calling helm template, then passing the result through
// kustomize, then mixing in overlay api objects.
func Render(ctx context.Context, r Renderer, path holos.PathComponent) (*Result, error) {
func Render(ctx context.Context, r Renderer, path holos.InstancePath) (*Result, error) {
return r.Render(ctx, path)
}

View File

@@ -3,12 +3,13 @@ package v1alpha1
import (
"context"
"fmt"
"github.com/holos-run/holos/pkg/logger"
"github.com/holos-run/holos/pkg/util"
"github.com/holos-run/holos/pkg/wrapper"
"os"
"path/filepath"
"slices"
"github.com/holos-run/holos/pkg/logger"
"github.com/holos-run/holos/pkg/util"
"github.com/holos-run/holos/pkg/wrapper"
)
// Result is the build result for display or writing. Holos components Render the Result as a data pipeline.
@@ -40,7 +41,7 @@ func (r *Result) AccumulatedOutput() string {
func (r *Result) addObjectMap(ctx context.Context, objectMap APIObjectMap) {
log := logger.FromContext(ctx)
b := []byte(r.AccumulatedOutput())
kinds := make([]string, 0, len(objectMap))
kinds := make([]Kind, 0, len(objectMap))
// Sort the keys
for kind := range objectMap {
kinds = append(kinds, kind)
@@ -50,7 +51,7 @@ func (r *Result) addObjectMap(ctx context.Context, objectMap APIObjectMap) {
for _, kind := range kinds {
v := objectMap[kind]
// Sort the keys
names := make([]string, 0, len(v))
names := make([]Label, 0, len(v))
for name := range v {
names = append(names, name)
}

View File

@@ -19,7 +19,8 @@ package v1alpha1
}
#BuildPlanComponents: {
helmCharts?: [...#HelmChart] @go(HelmCharts,[]HelmChart)
kubernetesObjects?: [...#KubernetesObjects] @go(KubernetesObjects,[]KubernetesObjects)
kustomizeBuilds?: [...#KustomizeBuild] @go(KustomizeBuilds,[]KustomizeBuild)
helmChartList?: [...#HelmChart] @go(HelmChartList,[]HelmChart)
kubernetesObjectsList?: [...#KubernetesObjects] @go(KubernetesObjectsList,[]KubernetesObjects)
kustomizeBuildList?: [...#KustomizeBuild] @go(KustomizeBuildList,[]KustomizeBuild)
resources?: {[string]: #KubernetesObjects} @go(Resources,map[string]KubernetesObjects)
}

View File

@@ -11,5 +11,5 @@ package v1alpha1
// ChartDir is the directory name created in the holos component directory to cache a chart.
#ChartDir: "vendor"
// ResourcesFile is the file name used to store component output when post processing with kustomize.
// ResourcesFile is the file name used to store component output when post-processing with kustomize.
#ResourcesFile: "resources.yaml"

View File

@@ -4,6 +4,12 @@
package v1alpha1
// Label is an arbitrary unique identifier. Defined as a type for clarity and type checking.
#Label: string
// Kind is a kubernetes api object kind. Defined as a type for clarity and type checking.
#Kind: string
// APIObjectMap is the shape of marshalled api objects returned from cue to the
// holos cli. A map is used to improve the clarity of error messages from cue.
#APIObjectMap: {[string]: [string]: string}

View File

@@ -7,7 +7,7 @@ import "encoding/yaml"
// apiObjects holds each the api objects produced by cue.
apiObjects: {
[Kind=_]: {
[Name=_]: {
[string]: {
kind: Kind
...
}

View File

@@ -0,0 +1,43 @@
package holos
_Projects: #Projects & {
example: environments: {
dev: stage: "dev"
jeff: stage: "dev"
gary: stage: "dev"
nate: stage: "dev"
}
iam: _
}
// Platform level definition of a project.
#Project: {
// All projects have at least a prod environment and stage.
environments: prod: stage: "prod"
}
#ProjectTemplate: {
project: #Project
resources: {
// System namespace
let SystemName = "\(project.name)-system"
(SystemName): #KubernetesObjects & {
metadata: name: SystemName
apiObjectMap: (#APIObjects & {
apiObjects: Namespace: (SystemName): #Namespace & {metadata: name: SystemName}
}).apiObjectMap
}
// Project Namespaces
let NamespacesName = "\(project.name)-namespaces"
(NamespacesName): #KubernetesObjects & {
metadata: name: NamespacesName
apiObjectMap: (#APIObjects & {
for _, env in project.environments {
apiObjects: Namespace: (env.slug): #Namespace & {metadata: name: env.slug}
}
}).apiObjectMap
}
}
}

View File

@@ -1,3 +1,5 @@
package holos
#InputKeys: project: "projects"
for ProjectName, Project in _Projects {
spec: components: resources: (#ProjectTemplate & {project: Project}).resources
}

View File

@@ -0,0 +1,15 @@
package holos
#ProjectMap: {
holos: {
}
}
#Project: {
features: {
postgres16: #Feature & {
description: "PostgreSQL 16 with ha, backups, and multi-region availability."
}
}
}

View File

@@ -0,0 +1 @@
package holos

View File

@@ -1,11 +1,38 @@
package holos
import h "github.com/holos-run/holos/api/v1alpha1"
// #Projects is a map of all the projects in the platform.
#Projects: [Name=_]: #Project & {name: Name}
#Project: {
name: string
let ProjectName = name
description: string
environments: [Name=string]: #Environment & {
name: Name
project: ProjectName
}
features: [Name=string]: #Feature & {name: Name}
}
#Projects: {
[Name=_]: #Project & {
name: Name
#Environment: {
name: string
project: string
stage: string | "dev" | "prod"
slug: "\(name)-\(project)"
}
#Feature: {
name: string
description: string
enabled: *true | false
}
#ProjectTemplate: {
project: #Project
resources: [Name=_]: h.#KubernetesObjects & {
metadata: name: Name
}
}

View File

@@ -5,6 +5,6 @@ package holos
// It is given a unique type so the API is clear.
type PathCueMod string
// A PathComponent is a string representing the filesystem path of a holos component.
// A InstancePath is a string representing the filesystem path of a holos instance.
// It is given a unique type so the API is clear.
type PathComponent string
type InstancePath string

View File

@@ -155,22 +155,29 @@ func (b *Builder) Run(ctx context.Context) (results []*v1alpha1.Result, err erro
}
// TODO: concurrent renders
for _, component := range buildPlan.Spec.Components.Resources {
if result, err := component.Render(ctx, holos.InstancePath(instance.Dir)); err != nil {
return nil, wrapper.Wrap(fmt.Errorf("could not render: %w", err))
} else {
results = append(results, result)
}
}
for _, component := range buildPlan.Spec.Components.KubernetesObjectsList {
if result, err := component.Render(ctx, holos.PathComponent(instance.Dir)); err != nil {
if result, err := component.Render(ctx, holos.InstancePath(instance.Dir)); err != nil {
return nil, wrapper.Wrap(fmt.Errorf("could not render: %w", err))
} else {
results = append(results, result)
}
}
for _, component := range buildPlan.Spec.Components.HelmChartList {
if result, err := component.Render(ctx, holos.PathComponent(instance.Dir)); err != nil {
if result, err := component.Render(ctx, holos.InstancePath(instance.Dir)); err != nil {
return nil, wrapper.Wrap(fmt.Errorf("could not render: %w", err))
} else {
results = append(results, result)
}
}
for _, component := range buildPlan.Spec.Components.KustomizeBuildList {
if result, err := component.Render(ctx, holos.PathComponent(instance.Dir)); err != nil {
if result, err := component.Render(ctx, holos.InstancePath(instance.Dir)); err != nil {
return nil, wrapper.Wrap(fmt.Errorf("could not render: %w", err))
} else {
results = append(results, result)

View File

@@ -1 +1 @@
3
4