Compare commits

...

15 Commits

Author SHA1 Message Date
Jeff McCune
43c8702398 (#66) Configure an ExtAuthzProxy provider for each project stage
This patch configures an istio envoyExtAuthzHttp provider for each stage
in each project.  An example provider for the dev stage of the holos
project is `authproxy-dev-holos`
2024-03-30 11:28:23 -07:00
Jeff McCune
ce94776dbb (#66) Add ZITADEL project and client ids for iam project
core1 and core2 don't render without these resource identifiers in
place.
2024-03-30 09:18:54 -07:00
Jeff McCune
78ab6cd848 (#66) Match /holos/oidc for all hosts in the project stage
This has the same effect and makes the VirtualService much more
manageable, particularly when calling `kubectl get vs -A`.
2024-03-29 22:50:17 -07:00
Jeff McCune
0a7001f868 (#66) Configure the primary domain for zitadel
This bypasses the account selection screen and automatically redirects
back to the application without user interaction.
2024-03-29 22:44:52 -07:00
Jeff McCune
2db7be671b (#66) Route prefix /holos/oidc to authproxy
This patch configures the service mesh to route all requests with a uri
path prefix of `/holos/oidc` to the auth proxy associated with the
project stage.

Consider a request to https://jeff.holos.dev.k2.ois.run/holos/oidc/sign_in

This request is usually routed to the backend app, but
VirtualService/authproxy in the dev-holos-system namespace matches the
request and routes it to the auth proxy instead.

The auth proxy matches the request Host: header against the whitelist
and cookiedomain setting, which matches the suffix
`.holos.dev.k2.ois.run`.  The auth proxy redirects to the oidc issuer
with a callback url of the request Host for a url of
`https://jeff.holos.dev.k2.ois.run/holos/oidc/callback`.

ZITADEL matches the callback against those registered with the app and
the app client id.  A code is then sent back to the auth proxy.

The auth proxy sets a cookie named `__Secure-authproxy-dev-holos` with a
domain of `.holos.dev.k2.ois.run` from the suffix match of the
`--cookiedomain` flag.

Because this all works using paths, the `auth` prefix domains have been
removed.  They're unnecessary, oauth2-proxy is available for any host
routed to the project stage at path prefix `/holos/oidc`.

Refer to https://oauth2-proxy.github.io/oauth2-proxy/features/endpoints/
for good endpoints for debuggin, replacing `/oauth2` with `/holos/oidc`
2024-03-29 21:56:46 -07:00
Jeff McCune
b51870f7bf (#66) Deploy oauth2-proxy and redis to stage namespaces
This patch deploys oauth2-proxy and redis to the system namespace of
each stage in each project.  The plan is to redirect unauthenticated
requests to the request host at the /holos/oidc/callback endpoint.

This patch removes the --redirect-uri flag, which makes the auth domain
prefix moot, so a future patch should remove those if they really are
unnecessary.

The reason to remove the --redirect-uri flag is to make sure we set the
cookie to a domain suffix of the request Host: header.
2024-03-29 20:56:26 -07:00
Jeff McCune
0227dfa7e5 (#66) Add Gateway entries for oauth2-proxy
This patch adds entries to the project stage Gateway for oauth2-proxy.
Three entries for each stage are added, one for the global endpoint plus
one for each cluster.
2024-03-29 15:30:02 -07:00
Jeff McCune
05b59d9af0 (#66) Refactor project hosts for auth proxy cookies
Without this patch the auth proxy cookie domain is difficult to manage.
This patch refactors the hosts managed for each environment in a project
to better align with security domains and auth proxy session cookies.

The convention is: `<env?>.<host>.<stage?>.<cluster?>.<domain>` where
`host` can be 0..N entries with a default value of `[projectName]`.

env may be omitted for prod or the dev env of the dev stage.  stage may
be omitted for prod.  cluster may be omitted for the global endpoint.

For a project named `holos`:

| Project | Stage | Env  | Cluster | Host                      |
| ------- | ----- | ---  | ------- | ------                    |
| holos   | dev   | jeff | k2      | jeff.holos.dev.k2.ois.run |
| holos   | dev   | jeff | global  | jeff.holos.dev.ois.run    |
| holos   | dev   | -    | k2      | holos.dev.k2.ois.run      |
| holos   | dev   | -    | global  | holos.dev.ois.run         |
| holos   | prod  | -    | k2      | holos.k2.ois.run          |
| holos   | prod  | -    | global  | holos.ois.run             |

Auth proxy:

| Project | Stage | Auth Proxy Host           | Auth Cookie Domain   |
| ------- | ----- | ------                    | ------------------   |
| holos   | dev   | auth.holos.dev.ois.run    | holos.dev.ois.run    |
| holos   | dev   | auth.holos.dev.k1.ois.run | holos.dev.k1.ois.run |
| holos   | dev   | auth.holos.dev.k2.ois.run | holos.dev.k2.ois.run |
| holos   | prod  | auth.holos.ois.run        | holos.ois.run        |
| holos   | prod  | auth.holos.k1.ois.run     | holos.k1.ois.run     |
| holos   | prod  | auth.holos.k2.ois.run     | holos.k2.ois.run     |
2024-03-29 15:30:01 -07:00
Jeff McCune
04f9f3b3a8 Merge pull request #79 from holos-run/nate/makefile_version
Show the holos version in 'make install|build'
2024-03-29 15:04:48 -07:00
Nate McCurdy
b58be8b38c Show the holos version in 'make install|build'
Prior to this, when running the 'install' or 'build' Makefile target,
the version of holos being built was not shown even though the 'build'
target attempted to show the version.

```
.PHONY: build
build: generate ## Build holos executable.
	@echo "building ${BIN_NAME} ${VERSION}"
```

For example:
```
> make install
go generate ./...
building holos
...
```

Holo's version is stored in pkg/version/embedded/{major,minor,patch},
not the `Version` const. So the fix is to change the value of `VERSION`
so that it comes from those embedded files.

Now the version of holos is shown:

```
> make install
go generate ./...
building holos 0.61.1
...
```

This also adds a new Makefile target called `show-version` which shows
the full version string (i.e. the value of `$VERSION`).
2024-03-29 15:01:33 -07:00
Jeff McCune
10493d754a (#66) Add httpbin to each project environment
The goal of this patch is to verify each project environment is wired up
to the ingress Gateway for the project stage.

This is a necessary step to eventually configure the VirtualService and
AuthorizationPolicy to only match on the `/dump/request` path of each
endpoint for troubleshooting.
2024-03-28 21:51:34 -07:00
Jeff McCune
cf28516b8b (#66) Project managed namespaces
This patch uses the existing #ManagedNamespaces definition to create and
manage namespaces on the provisioner and workload clusters so that
SecretStore and eso-creds-refresher resources are managed in the project
environment namespaces and the project stage system namespace.
2024-03-28 15:09:57 -07:00
Jeff McCune
d81e25c4e4 (#66) Project Certificates
Provisioner cluster:

This patch creates a Certificate resource in the provisioner for each
host associated with the project.  By default, one host is created for
each stage with the short hostname set to the project name.

A namespace is also created for each project for eso creds refresher to
manage service accounts for SecretStore resources in the workload
clusters.

Workload cluster:

For each env, plus one system namespace per stage:

 - Namespace per env
 - SecretStore per env
 - ExternalSecret per host in the env

Common names for the holos project, prod stage:

- holos.k1.ois.run
- holos.k2.ois.run
- holos.ois.run

Common names for the holos project, dev stage:

- holos.dev.k1.ois.run
- holos.dev.k2.ois.run
- holos.dev.ois.run
- holos.gary.k1.ois.run
- holos.gary.k2.ois.run
- holos.gary.ois.run
- holos.jeff.k1.ois.run
- holos.jeff.k2.ois.run
- holos.jeff.ois.run
- holos.nate.k1.ois.run
- holos.nate.k2.ois.run
- holos.nate.ois.run

Usage:

    holos render --cluster-name=provisioner \
      ~/workspace/holos-run/holos/docs/examples/platforms/reference/clusters/provisioner/projects/...
    holos render --cluster-name=k1 \
      ~/workspace/holos-run/holos/docs/examples/platforms/reference/clusters/workload/projects/...
    holos render --cluster-name=k2 \
      ~/workspace/holos-run/holos/docs/examples/platforms/reference/clusters/workload/projects/...
2024-03-27 20:54:51 -07:00
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
31 changed files with 861 additions and 122 deletions

1
.gitignore vendored
View File

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

View File

@@ -4,7 +4,7 @@ PROJ=holos
ORG_PATH=github.com/holos-run
REPO_PATH=$(ORG_PATH)/$(PROJ)
VERSION := $(shell grep "const Version " pkg/version/version.go | sed -E 's/.*"(.+)"$$/\1/')
VERSION := $(shell cat pkg/version/embedded/major pkg/version/embedded/minor pkg/version/embedded/patch | xargs printf "%s.%s.%s")
BIN_NAME := holos
DOCKER_REPO=quay.io/openinfrastructure/holos
@@ -39,6 +39,10 @@ bumpmajor: ## Bump the major version.
scripts/bump minor 0
scripts/bump patch 0
.PHONY: show-version
show-version: ## Print the full version.
@echo $(VERSION)
.PHONY: tidy
tidy: ## Tidy go module.
go mod tidy

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

@@ -306,19 +306,10 @@ import "strings"
// "value"` for prefix-based match - `regex: "value"` for RE2
// style regex-based match
// (https://github.com/google/re2/wiki/Syntax).
uri?: ({} | {
exact: _
} | {
prefix: _
} | {
regex: _
}) & {
uri?: {
exact?: string
prefix?: string
// RE2 style regex-based match
// (https://github.com/google/re2/wiki/Syntax).
regex?: string
regex?: string
}
// withoutHeader has the same syntax with the header, but has

View File

@@ -7,14 +7,18 @@ import "encoding/yaml"
// apiObjects holds each the api objects produced by cue.
apiObjects: {
[Kind=_]: {
[Name=_]: {
[string]: {
kind: Kind
...
}
}
Namespace?: [Name=_]: #Namespace & {metadata: name: Name}
ExternalSecret?: [Name=_]: #ExternalSecret & {_name: Name}
VirtualService?: [Name=_]: #VirtualService & {metadata: name: Name}
Issuer?: [Name=_]: #Issuer & {metadata: name: Name}
Gateway?: [Name=_]: #Gateway & {metadata: name: Name}
ConfigMap?: [Name=_]: #ConfigMap & {metadata: name: Name}
Deployment?: [_]: #Deployment
}
// apiObjectMap holds the marshalled representation of apiObjects

View File

@@ -0,0 +1,52 @@
package holos
// #MeshConfig provides the istio meshconfig in the config key given projects.
#MeshConfig: {
projects: #Projects
// clusterName is the value of the --cluster-name flag, the cluster currently being manged / rendered.
clusterName: string | *#ClusterName
extensionProviderMap: [Name=_]: {
name: Name
}
config: {
accessLogEncoding: string | *"JSON"
accessLogFile: string | *"/dev/stdout"
defaultConfig: {
discoveryAddress: string | *"istiod.istio-system.svc:15012"
tracing: zipkin: address: string | *"zipkin.istio-system:9411"
}
defaultProviders: metrics: [...string] | *["prometheus"]
enablePrometheusMerge: false | *true
rootNamespace: string | *"istio-system"
trustDomain: string | *"cluster.local"
extensionProviders: [for x in extensionProviderMap {x}]
}
}
// #ExtAuthzProxy defines the provider configuration for an istio external authorization auth proxy.
#ExtAuthzProxy: {
name: string
envoyExtAuthzHttp: {
headersToDownstreamOnDeny: [
"content-type",
"set-cookie",
]
headersToUpstreamOnAllow: [
"authorization",
"path",
"x-auth-request-user",
"x-auth-request-email",
"x-auth-request-access-token",
]
includeAdditionalHeadersInCheck: "X-Auth-Request-Redirect": "%REQ(x-forwarded-proto)%://%REQ(:authority)%%REQ(:path)%%REQ(:query)%"
includeRequestHeadersInCheck: [
"authorization",
"cookie",
"x-forwarded-for",
]
port: 4180
service: string
}
}

View File

@@ -1,7 +1,5 @@
package holos
import "list"
spec: components: KubernetesObjectsList: [
#KubernetesObjects & {
metadata: name: "prod-secrets-namespaces"
@@ -9,7 +7,7 @@ spec: components: KubernetesObjectsList: [
apiObjects: {
// #ManagedNamespaces is the set of all namespaces across all clusters in the platform.
for k, ns in #ManagedNamespaces {
if list.Contains(ns.clusterNames, #ClusterName) {
if ns.clusters[#ClusterName] != _|_ {
Namespace: "\(k)": #Namespace & ns.namespace
}
}

View File

@@ -50,8 +50,8 @@ let OBJECTS = #APIObjects & {
seccompProfile: type: "RuntimeDefault"
allowPrivilegeEscalation: false
runAsNonRoot: true
runAsUser: 1337
runAsGroup: 1337
runAsUser: 8192
runAsGroup: 8192
capabilities: drop: ["ALL"]
}}]
}

View File

@@ -2,73 +2,4 @@ package holos
// Istio meshconfig
// TODO: Generate per-project extauthz providers.
_MeshConfig: {
accessLogEncoding: "JSON"
accessLogFile: "/dev/stdout"
defaultConfig: {
discoveryAddress: "istiod.istio-system.svc:15012"
tracing: zipkin: address: "zipkin.istio-system:9411"
}
defaultProviders: metrics: ["prometheus"]
enablePrometheusMerge: true
// For PROXY PROTOCOL at the ingress gateway.
gatewayTopology: {
numTrustedProxies: 2
}
rootNamespace: "istio-system"
trustDomain: "cluster.local"
extensionProviders: [{
name: "cluster-trace"
zipkin: {
maxTagLength: 56
port: 9411
service: "zipkin.istio-system.svc"
}
}, {
name: "cluster-gatekeeper"
envoyExtAuthzHttp: {
headersToDownstreamOnDeny: [
"content-type",
"set-cookie",
]
headersToUpstreamOnAllow: [
"authorization",
"path",
"x-auth-request-user",
"x-auth-request-email",
"x-auth-request-access-token",
]
includeAdditionalHeadersInCheck: "X-Auth-Request-Redirect": "%REQ(x-forwarded-proto)%://%REQ(:authority)%%REQ(:path)%%REQ(:query)%"
includeRequestHeadersInCheck: [
"authorization",
"cookie",
"x-forwarded-for",
]
port: 4180
service: "oauth2-proxy.istio-ingress.svc.cluster.local"
}
}, {
name: "core-authorizer"
envoyExtAuthzHttp: {
headersToDownstreamOnDeny: [
"content-type",
"set-cookie",
]
headersToUpstreamOnAllow: [
"authorization",
"path",
"x-auth-request-user",
"x-auth-request-email",
"x-auth-request-access-token",
]
includeAdditionalHeadersInCheck: "X-Auth-Request-Redirect": "%REQ(x-forwarded-proto)%://%REQ(:authority)%%REQ(:path)%%REQ(:query)%"
includeRequestHeadersInCheck: [
"authorization",
"cookie",
"x-forwarded-for",
]
port: 4180
service: "oauth2-proxy.prod-core-system.svc.cluster.local"
}
}]
}
_MeshConfig: (#MeshConfig & {projects: _Projects}).config

View File

@@ -0,0 +1,41 @@
package holos
#Project: authProxyOrgDomain: "openinfrastructure.co"
_Projects: #Projects & {
holos: {
resourceId: 260446255245690199
clusters: {
k1: _
k2: _
}
stages: dev: authProxyClientID: "260505543108527218@holos"
stages: prod: authProxyClientID: "260506079325128023@holos"
environments: {
prod: stage: "prod"
dev: stage: "dev"
jeff: stage: dev.stage
gary: stage: dev.stage
nate: stage: dev.stage
}
}
iam: {
resourceId: 260582480954787159
clusters: {
core1: _
core2: _
}
stages: dev: authProxyClientID: "260582521186616432@iam"
stages: prod: authProxyClientID: "260582633862399090@iam"
}
}
// Manage namespaces for platform project environments.
for project in _Projects {
for ns in project.managedNamespaces {
if ns.clusters[#ClusterName] != _|_ {
#ManagedNamespaces: (ns.namespace.metadata.name): ns
}
}
}

View File

@@ -1,3 +0,0 @@
package holos
#InputKeys: project: "projects"

View File

@@ -0,0 +1,5 @@
package holos
for Project in _Projects {
spec: components: resources: (#ProjectTemplate & {project: Project}).provisioner.resources
}

View File

@@ -0,0 +1,5 @@
package holos
for Project in _Projects {
spec: components: resources: (#ProjectTemplate & {project: Project}).workload.resources
}

View File

@@ -0,0 +1,35 @@
package holos
#MeshConfig: {
projects: _
clusterName: _
extensionProviderMap: {
"cluster-trace": {
zipkin: {
maxTagLength: 56
port: 9411
service: "zipkin.istio-system.svc"
}
}
}
config: {
// For PROXY PROTOCOL at the ingress gateway.
gatewayTopology: {
numTrustedProxies: 2
}
}
// Configure an ExtAuthzHttp provider for each stage's authproxy
for Project in projects {
if Project.clusters[clusterName] != _|_ {
for Stage in Project.stages {
let Name = "authproxy-\(Stage.slug)"
extensionProviderMap: (Name): #ExtAuthzProxy & {
envoyExtAuthzHttp: service: "authproxy.\(Stage.namespace).svc.cluster.local"
}
}
}
}
}

View File

@@ -0,0 +1,384 @@
package holos
// Platform level definition of a project.
#Project: {
name: string
// All projects have at least a prod environment and stage.
stages: prod: stageSegments: []
environments: prod: stage: "prod"
environments: prod: envSegments: []
stages: dev: _
environments: dev: stage: "dev"
environments: dev: envSegments: []
// Ensure at least the project name is a short hostname. Additional may be added.
hosts: (name): _
// environments share the stage segments of their stage.
environments: [_]: {
stage: string
stageSegments: stages[stage].stageSegments
}
}
#ProjectTemplate: {
project: #Project
let Project = project
// GatewayServers maps Gateway spec.servers #GatewayServer values indexed by stage then name.
let GatewayServers = {
// Initialize all stages, even if they have no environments.
for stage in project.stages {
(stage.name): {}
}
// For each stage, construct entries for the Gateway spec.servers.hosts field.
for env in project.environments {
(env.stage): {
let Env = env
let Stage = project.stages[env.stage]
for host in (#EnvHosts & {project: Project, env: Env}).hosts {
(host.name): #GatewayServer & {
hosts: [
"\(env.namespace)/\(host.name)",
// Allow the authproxy VirtualService to match the project.authProxyPrefix path.
"\(Stage.namespace)/\(host.name)",
]
port: host.port
tls: credentialName: host.name
tls: mode: "SIMPLE"
}
}
}
}
}
workload: resources: {
// Provide resources only if the project is managed on --cluster-name
if project.clusters[#ClusterName] != _|_ {
for stage in project.stages {
let Stage = stage
// Istio Gateway
"\(stage.slug)-gateway": #KubernetesObjects & {
apiObjectMap: (#APIObjects & {
apiObjects: Gateway: (stage.slug): #Gateway & {
spec: servers: [for server in GatewayServers[stage.name] {server}]
}
for host in GatewayServers[stage.name] {
apiObjects: ExternalSecret: (host.tls.credentialName): metadata: namespace: "istio-ingress"
}
}).apiObjectMap
}
// Manage auth-proxy in each stage
"\(stage.slug)-authproxy": #KubernetesObjects & {
apiObjectMap: (#APIObjects & {
apiObjects: (AUTHPROXY & {stage: Stage, project: Project, servers: GatewayServers[stage.name]}).apiObjects
}).apiObjectMap
}
// Manage httpbin in each environment
for Env in project.environments if Env.stage == stage.name {
"\(Env.slug)-httpbin": #KubernetesObjects & {
apiObjectMap: (#APIObjects & {
let Project = project
apiObjects: (HTTPBIN & {env: Env, project: Project}).apiObjects
}).apiObjectMap
}
}
}
}
}
provisioner: resources: {
for stage in project.stages {
"\(stage.slug)-certs": #KubernetesObjects & {
apiObjectMap: (#APIObjects & {
for host in GatewayServers[stage.name] {
let CN = host.tls.credentialName
apiObjects: Certificate: (CN): #Certificate & {
metadata: name: CN
metadata: namespace: "istio-ingress"
spec: {
commonName: CN
dnsNames: [CN]
secretName: CN
issuerRef: {
kind: "ClusterIssuer"
name: "letsencrypt"
}
}
}
}
}).apiObjectMap
}
}
}
}
let HTTPBIN = {
name: string | *"httpbin"
project: #Project
env: #Environment
let Name = name
let Metadata = {
name: Name
namespace: env.namespace
labels: app: name
}
apiObjects: {
Deployment: (Name): #Deployment & {
metadata: Metadata
spec: selector: matchLabels: Metadata.labels
spec: template: {
metadata: labels: Metadata.labels & #IstioSidecar
spec: securityContext: seccompProfile: type: "RuntimeDefault"
spec: containers: [{
name: Name
image: "quay.io/holos/mccutchen/go-httpbin"
ports: [{containerPort: 8080}]
securityContext: {
seccompProfile: type: "RuntimeDefault"
allowPrivilegeEscalation: false
runAsNonRoot: true
runAsUser: 8192
runAsGroup: 8192
capabilities: drop: ["ALL"]
}}]
}
}
Service: (Name): #Service & {
metadata: Metadata
spec: selector: Metadata.labels
spec: ports: [
{port: 80, targetPort: 8080, protocol: "TCP", name: "http"},
]
}
VirtualService: (Name): #VirtualService & {
metadata: Metadata
let Project = project
let Env = env
spec: hosts: [for host in (#EnvHosts & {project: Project, env: Env}).hosts {host.name}]
spec: gateways: ["istio-ingress/\(env.stageSlug)"]
spec: http: [{route: [{destination: host: Name}]}]
}
}
}
// AUTHPROXY configures one oauth2-proxy deployment for each host in each stage of a project. Multiple deployments per stage are used to narrow down the cookie domain.
let AUTHPROXY = {
name: string | *"authproxy"
project: #Project
stage: #Stage
servers: {}
let Name = name
let Project = project
let Stage = stage
let Metadata = {
name: Name
namespace: stage.namespace
labels: {
"app.kubernetes.io/name": name
"app.kubernetes.io/instance": stage.name
"app.kubernetes.io/part-of": stage.project
}
}
let RedisMetadata = {
name: Name + "-redis"
namespace: stage.namespace
labels: {
"app.kubernetes.io/name": name
"app.kubernetes.io/instance": stage.name
"app.kubernetes.io/part-of": stage.project
}
}
apiObjects: {
// oauth2-proxy
ExternalSecret: (Name): metadata: Metadata
Deployment: (Name): #Deployment & {
metadata: Metadata
// project.dev.example.com, project.dev.k1.example.com, project.dev.k2.example.com
let StageDomains = {
for host in (#StageDomains & {project: Project, stage: Stage}).hosts {
(host.name): host
}
}
spec: {
replicas: 1
selector: matchLabels: Metadata.labels
template: {
metadata: labels: Metadata.labels
metadata: labels: #IstioSidecar
spec: securityContext: seccompProfile: type: "RuntimeDefault"
spec: containers: [{
image: "quay.io/oauth2-proxy/oauth2-proxy:v7.4.0"
imagePullPolicy: "IfNotPresent"
name: "oauth2-proxy"
args: [
// callback url is proxy prefix + /callback
"--proxy-prefix=" + project.authProxyPrefix,
"--email-domain=*",
"--session-store-type=redis",
"--redis-connection-url=redis://\(RedisMetadata.name):6379",
"--cookie-refresh=12h",
"--cookie-expire=2160h",
"--cookie-secure=true",
"--cookie-name=__Secure-\(Name)-\(stage.slug)",
"--cookie-samesite=lax",
for domain in StageDomains {"--cookie-domain=.\(domain.name)"},
for domain in StageDomains {"--whitelist-domain=.\(domain.name)"},
"--cookie-csrf-per-request=true",
"--cookie-csrf-expire=120s",
"--set-authorization-header=false",
"--set-xauthrequest=true",
"--pass-access-token=true",
"--pass-authorization-header=true",
"--upstream=static://200",
"--reverse-proxy",
"--real-client-ip-header=X-Forwarded-For",
"--skip-provider-button=true",
"--auth-logging",
"--provider=oidc",
"--scope=openid profile email groups offline_access urn:zitadel:iam:org:domain:primary:\(project.authProxyOrgDomain)",
"--client-id=" + stage.authProxyClientID,
"--client-secret-file=/dev/null",
"--oidc-issuer-url=\(project.authProxyIssuer)",
"--code-challenge-method=S256",
"--http-address=0.0.0.0:4180",
// "--allowed-group=\(project.resourceId):\(stage.name)-access",
]
env: [{
name: "OAUTH2_PROXY_COOKIE_SECRET"
// echo '{"cookiesecret":"'$(LC_ALL=C tr -dc "[:alpha:]" </dev/random | tr '[:upper:]' '[:lower:]' | head -c 32)'"}' | holos create secret -n dev-holos-system --append-hash=false --data-stdin authproxy
valueFrom: secretKeyRef: {
key: "cookiesecret"
name: Name
}
}]
ports: [{
containerPort: 4180
protocol: "TCP"
}]
securityContext: {
seccompProfile: type: "RuntimeDefault"
allowPrivilegeEscalation: false
runAsNonRoot: true
runAsUser: 8192
runAsGroup: 8192
capabilities: drop: ["ALL"]
}
}]
}
}
}
Service: (Name): #Service & {
metadata: Metadata
spec: selector: Metadata.labels
spec: ports: [
{port: 4180, targetPort: 4180, protocol: "TCP", name: "http"},
]
}
VirtualService: (Name): #VirtualService & {
metadata: Metadata
spec: hosts: ["*"]
spec: gateways: ["istio-ingress/\(stage.slug)"]
spec: http: [{
match: [{uri: prefix: project.authProxyPrefix}]
route: [{
destination: host: Name
destination: port: number: 4180
}]
}]
}
// redis
ConfigMap: (RedisMetadata.name): {
metadata: RedisMetadata
data: "redis.conf": """
maxmemory 128mb
maxmemory-policy allkeys-lru
"""
}
Deployment: (RedisMetadata.name): {
metadata: RedisMetadata
spec: {
selector: matchLabels: RedisMetadata.labels
template: {
metadata: labels: RedisMetadata.labels
metadata: labels: #IstioSidecar
spec: securityContext: seccompProfile: type: "RuntimeDefault"
spec: {
containers: [{
command: [
"redis-server",
"/redis-master/redis.conf",
]
env: [{
name: "MASTER"
value: "true"
}]
image: "quay.io/holos/redis:7.2.4"
livenessProbe: {
initialDelaySeconds: 15
tcpSocket: port: "redis"
}
name: "redis"
ports: [{
containerPort: 6379
name: "redis"
}]
readinessProbe: {
exec: command: [
"redis-cli",
"ping",
]
initialDelaySeconds: 5
}
resources: limits: cpu: "0.5"
securityContext: {
seccompProfile: type: "RuntimeDefault"
allowPrivilegeEscalation: false
capabilities: drop: ["ALL"]
}
volumeMounts: [{
mountPath: "/redis-master-data"
name: "data"
}, {
mountPath: "/redis-master"
name: "config"
}]
}]
volumes: [{
emptyDir: {}
name: "data"
}, {
configMap: name: RedisMetadata.name
name: "config"
}]
}
}
}
}
Service: (RedisMetadata.name): #Service & {
metadata: RedisMetadata
spec: selector: RedisMetadata.labels
spec: type: "ClusterIP"
spec: ports: [{
name: "redis"
port: 6379
protocol: "TCP"
targetPort: 6379
}]
}
}
}

View File

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

View File

@@ -1,11 +1,195 @@
package holos
import h "github.com/holos-run/holos/api/v1alpha1"
import "strings"
// #Projects is a map of all the projects in the platform.
#Projects: [Name=_]: #Project & {name: Name}
#Project: {
name: string
// resourceId is the zitadel project Resource ID
resourceId: number
let ProjectName = name
description: string
environments: [Name=string]: #Environment & {
name: Name
project: ProjectName
}
stages: [Name=string]: #Stage & {
name: Name
project: ProjectName
}
domain: string | *#Platform.org.domain
// authProxyPrefix is the path routed to the ext auth proxy.
authProxyPrefix: string | *"/holos/oidc"
// authProxyOrgDomain is the primary org domain for zitadel.
authProxyOrgDomain: string | *#Platform.org.domain
// authProxyIssuer is the issuer url
authProxyIssuer: string | *"https://login.\(#Platform.org.domain)"
// hosts are short hostnames to configure for the project.
// Each value is routed to every environment in the project as a dns prefix.
hosts: [Name=string]: #Host & {name: Name}
// clusters are the cluster names the project is configured on.
clusters: [Name=string]: #Cluster & {name: Name}
clusterNames: [for c in clusters {c.name}]
// managedNamespaces ensures project namespaces have SecretStores that can sync ExternalSecrets from the provisioner cluster.
managedNamespaces: {
// Define the shape of a managed namespace.
[Name=_]: #ManagedNamespace & {
namespace: metadata: name: Name
clusterNames: ["provisioner", for c in clusters {c.name}]
}
// Manage a system namespace for each stage in the project.
for stage in stages {
for ns in stage.namespaces {
(ns.name): _
}
}
// Manage a namespace for each environment in the project.
for env in environments {
(env.namespace): _
}
}
// features is YAGNI maybe?
features: [Name=string]: #Feature & {name: Name}
}
#Projects: {
[Name=_]: #Project & {
name: Name
// #Cluster defines a cluster
#Cluster: name: string
// #Host defines a short hostname
#Host: name: string
#Environment: {
// name uniquely identifies the environment within the scope of the project.
name: string
project: string
stage: string | "dev" | "prod"
slug: "\(name)-\(project)"
namespace: "\(name)-\(project)"
stageSlug: "\(stage)-\(project)"
// envSegments are the env portion of the dns segments
envSegments: [...string] | *[name]
// stageSegments are the stage portion of the dns segments
stageSegments: [...string] | *[stage]
// #host provides a hostname
// Refer to: https://github.com/holos-run/holos/issues/66#issuecomment-2027562626
#host: {
name: string
cluster?: string
clusterSegments: [...string]
if cluster != _|_ {
clusterSegments: [cluster]
}
let SEGMENTS = envSegments + [name] + stageSegments + clusterSegments + [#Platform.org.domain]
let NAMESEGMENTS = ["https"] + SEGMENTS
host: {
name: strings.Join(SEGMENTS, ".")
port: {
name: strings.Replace(strings.Join(NAMESEGMENTS, "-"), ".", "-", -1)
number: 443
protocol: "HTTPS"
}
}
}
}
#Stage: {
name: string
project: string
slug: "\(name)-\(project)"
// namespace is the system namespace for the project stage
namespace: "\(name)-\(project)-system"
// Manage a system namespace for each stage
namespaces: [Name=_]: name: Name
namespaces: (namespace): _
// stageSegments are the stage portion of the dns segments
stageSegments: [...string] | *[name]
// authProxyClientID is the ClientID registered with the oidc issuer.
authProxyClientID: string
}
#Feature: {
name: string
description: string
enabled: *true | false
}
#ProjectTemplate: {
project: #Project
// workload cluster resources
workload: resources: [Name=_]: h.#KubernetesObjects & {
metadata: name: Name
}
// provisioner cluster resources
provisioner: resources: [Name=_]: h.#KubernetesObjects & {
metadata: name: Name
}
}
// #EnvHosts provides hostnames given a project and environment.
// Refer to https://github.com/holos-run/holos/issues/66#issuecomment-2027562626
#EnvHosts: {
project: #Project & {name: env.project}
env: #Environment
hosts: {
for host in project.hosts {
// globally scoped hostname
let HOST = (env.#host & {name: host.name}).host
(HOST.name): HOST
// cluster scoped hostname
for Cluster in project.clusters {
let HOST = (env.#host & {name: host.name, cluster: Cluster.name}).host
(HOST.name): HOST
}
}
}
}
// #StageDomains provides hostnames given a project and stage. Primarily for the
// auth proxy.
// Refer to https://github.com/holos-run/holos/issues/66#issuecomment-2027562626
#StageDomains: {
// names are the leading prefix names to create hostnames for.
// this is a two level list to support strings.Join()
prefixes: [...[...string]] | *[[]]
stage: #Stage
project: #Project & {
name: stage.project
}
// blank segment for the global domain plus each cluster in the project.
let ClusterSegments = [[], for cluster in project.clusters {[cluster.name]}]
hosts: {
for prefix in prefixes {
for ClusterSegment in ClusterSegments {
let SEGMENTS = prefix + [project.name] + stage.stageSegments + ClusterSegment + [project.domain]
let NAMESEGMENTS = ["https"] + SEGMENTS
let HOSTNAME = strings.Join(SEGMENTS, ".")
(HOSTNAME): {
name: HOSTNAME
port: {
name: strings.Replace(strings.Join(NAMESEGMENTS, "-"), ".", "-", -1)
number: 443
protocol: "HTTPS"
}
}
}
}
}
}

View File

@@ -73,11 +73,15 @@ _apiVersion: "holos.run/v1alpha1"
#Job: #NamespaceObject & batchv1.#Job
#CronJob: #NamespaceObject & batchv1.#CronJob
#Deployment: #NamespaceObject & appsv1.#Deployment
#Gateway: #NamespaceObject & gw.#Gateway
#VirtualService: #NamespaceObject & vs.#VirtualService
#Certificate: #NamespaceObject & crt.#Certificate
#PostgresCluster: #NamespaceObject & pg.#PostgresCluster
#Gateway: #NamespaceObject & gw.#Gateway & {
metadata: namespace: string | *"istio-ingress"
spec: selector: istio: string | *"ingressgateway"
}
// #HTTP01Cert defines a http01 certificate.
#HTTP01Cert: {
_name: string
@@ -229,6 +233,9 @@ _apiVersion: "holos.run/v1alpha1"
}
// clusterNames represents the set of clusters the namespace is managed on. Usually all clusters.
clusterNames: [...string]
for cluster in clusterNames {
clusters: (cluster): name: cluster
}
}
// #ManagedNamepsaces is the union of all namespaces across all cluster types and optional services.
@@ -295,3 +302,77 @@ _apiVersion: "holos.run/v1alpha1"
// #IsPrimaryCluster is true if the cluster being rendered is the primary cluster
// Used by the iam project to determine where https://login.example.com is active.
#IsPrimaryCluster: bool & #ClusterName == #Platform.primaryCluster.name
// #GatewayServer defines the value of the istio Gateway.spec.servers field.
#GatewayServer: {
// The ip or the Unix domain socket to which the listener should
// be bound to.
bind?: string
defaultEndpoint?: string
// One or more hosts exposed by this gateway.
hosts: [...string]
// An optional name of the server, when set must be unique across
// all servers.
name?: string
// The Port on which the proxy should listen for incoming
// connections.
port: {
// Label assigned to the port.
name: string
// A valid non-negative integer port number.
number: int
// The protocol exposed on the port.
protocol: string
targetPort?: int
}
// Set of TLS related options that govern the server's behavior.
tls?: {
// REQUIRED if mode is `MUTUAL` or `OPTIONAL_MUTUAL`.
caCertificates?: string
// Optional: If specified, only support the specified cipher list.
cipherSuites?: [...string]
// For gateways running on Kubernetes, the name of the secret that
// holds the TLS certs including the CA certificates.
credentialName?: string
// If set to true, the load balancer will send a 301 redirect for
// all http connections, asking the clients to use HTTPS.
httpsRedirect?: bool
// Optional: Maximum TLS protocol version.
maxProtocolVersion?: "TLS_AUTO" | "TLSV1_0" | "TLSV1_1" | "TLSV1_2" | "TLSV1_3"
// Optional: Minimum TLS protocol version.
minProtocolVersion?: "TLS_AUTO" | "TLSV1_0" | "TLSV1_1" | "TLSV1_2" | "TLSV1_3"
// Optional: Indicates whether connections to this port should be
// secured using TLS.
mode?: "PASSTHROUGH" | "SIMPLE" | "MUTUAL" | "AUTO_PASSTHROUGH" | "ISTIO_MUTUAL" | "OPTIONAL_MUTUAL"
// REQUIRED if mode is `SIMPLE` or `MUTUAL`.
privateKey?: string
// REQUIRED if mode is `SIMPLE` or `MUTUAL`.
serverCertificate?: string
// A list of alternate names to verify the subject identity in the
// certificate presented by the client.
subjectAltNames?: [...string]
// An optional list of hex-encoded SHA-256 hashes of the
// authorized client certificates.
verifyCertificateHash?: [...string]
// An optional list of base64-encoded SHA-256 hashes of the SPKIs
// of authorized client certificates.
verifyCertificateSpki?: [...string]
}
}

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

View File

@@ -1 +1 @@
3
4