Compare commits

..

9 Commits

Author SHA1 Message Date
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
10 changed files with 548 additions and 162 deletions

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

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

@@ -13,12 +13,12 @@ import "encoding/yaml"
}
}
Namespace?: [Name=_]: #Namespace & {metadata: name: Name}
SecretStore?: [Name=_]: #SecretStore & {_namespace: Name}
ExternalSecret?: [Name=_]: #ExternalSecret & {_name: Name}
VirtualService?: [Name=_]: #VirtualService & {metadata: name: Name}
Issuer?: [Name=_]: #Issuer & {metadata: name: Name}
Gateway?: [Name=_]: #Gateway & {metadata: name: Name}
Certificate?: [Name=_]: #Certificate & {metadata: name: Name}
ConfigMap?: [Name=_]: #ConfigMap & {metadata: name: Name}
Deployment?: [_]: #Deployment
}
// apiObjectMap holds the marshalled representation of apiObjects

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

@@ -1,11 +1,16 @@
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"
@@ -22,3 +27,12 @@ _Projects: #Projects & {
}
}
}
// 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,115 +1,102 @@
package holos
import "strings"
// 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: dnsSegments: []
stages: prod: _
stages: dev: _
// Short hostnames to construct fqdns.
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
// ExtAuthzHosts maps host names to the backend environment namespace for ExtAuthz.
let ExtAuthzHosts = {
// 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): {
for host in project.hosts {
let NAME = "https-\(project.name)-\(env.name)-\(host.name)"
let SEGMENTS = [host.name] + env.dnsSegments + [#Platform.org.domain]
let HOST = strings.Join(SEGMENTS, ".")
(NAME): #GatewayServer & {
hosts: ["\(env.namespace)/\(HOST)"]
// name must be unique across all servers in all gateways
port: name: NAME
port: number: 443
port: protocol: "HTTPS"
// TODO: Manage a certificate with each host in the dns alt names.
tls: credentialName: HOST
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"
}
for cluster in project.clusters {
let NAME = "https-\(cluster.name)-\(project.name)-\(env.name)-\(host.name)"
let SEGMENTS = [host.name] + env.dnsSegments + [cluster.name, #Platform.org.domain]
let HOST = strings.Join(SEGMENTS, ".")
(NAME): #GatewayServer & {
hosts: ["\(env.namespace)/\(HOST)"]
// name must be unique across all servers in all gateways
port: name: NAME
port: number: 443
port: protocol: "HTTPS"
// TODO: Manage a certificate with each host in the dns alt names.
tls: credentialName: HOST
tls: mode: "SIMPLE"
}
}
}
}
}
}
workload: resources: {
for stage in project.stages {
// System namespace for each project stage
let SystemName = "\(stage.slug)-system"
(SystemName): #KubernetesObjects & {
apiObjectMap: (#APIObjects & {
apiObjects: Namespace: (SystemName): _
apiObjects: SecretStore: (SystemName): _
}).apiObjectMap
}
// Provide resources only if the project is managed on --cluster-name
if project.clusters[#ClusterName] != _|_ {
for stage in project.stages {
let Stage = stage
// Project namespace for each project environment
"\(stage.slug)-namespaces": #KubernetesObjects & {
apiObjectMap: (#APIObjects & {
for env in project.environments if env.stage == stage.name {
apiObjects: Namespace: (env.slug): _
apiObjects: SecretStore: (env.slug): _
}
}).apiObjectMap
}
// Istio Gateway
"\(stage.slug)-gateway": #KubernetesObjects & {
apiObjectMap: (#APIObjects & {
apiObjects: Gateway: (stage.slug): #Gateway & {
spec: servers: [for server in GatewayServers[stage.name] {server}]
}
// Istio Gateway
"\(stage.slug)-gateway": #KubernetesObjects & {
apiObjectMap: (#APIObjects & {
apiObjects: Gateway: (stage.slug): #Gateway & {
spec: servers: [for host in ExtAuthzHosts[stage.name] {host}]
}
for host in GatewayServers[stage.name] {
apiObjects: ExternalSecret: (host.tls.credentialName): metadata: namespace: "istio-ingress"
}
}).apiObjectMap
}
for host in ExtAuthzHosts[stage.name] {
apiObjects: ExternalSecret: (host.tls.credentialName): metadata: namespace: "istio-ingress"
// 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
}
}).apiObjectMap
}
}
}
}
provisioner: resources: {
for stage in project.stages {
"\(stage.slug)-namespaces": #KubernetesObjects & {
apiObjectMap: (#APIObjects & {
apiObjects: Namespace: "\(stage.slug)-system": _
for env in project.environments if env.stage == stage.name {
apiObjects: Namespace: (env.slug): _
}
}).apiObjectMap
}
"\(stage.slug)-certs": #KubernetesObjects & {
apiObjectMap: (#APIObjects & {
for host in ExtAuthzHosts[stage.name] {
for host in GatewayServers[stage.name] {
let CN = host.tls.credentialName
apiObjects: Certificate: (CN): #Certificate & {
metadata: name: CN
@@ -125,83 +112,273 @@ import "strings"
}
}
}
}).apiObjectMap
}
}
}
}
// #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
let HTTPBIN = {
name: string | *"httpbin"
project: #Project
env: #Environment
let Name = name
// 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
let Metadata = {
name: Name
namespace: env.namespace
labels: app: name
}
// Set of TLS related options that govern the server's behavior.
tls?: {
// REQUIRED if mode is `MUTUAL` or `OPTIONAL_MUTUAL`.
caCertificates?: string
apiObjects: {
Deployment: (Name): #Deployment & {
metadata: Metadata
// 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]
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: 80, targetPort: 4180, protocol: "TCP", name: "http"},
]
}
VirtualService: (Name): #VirtualService & {
metadata: Metadata
spec: hosts: [for host, v in servers {host}]
spec: gateways: ["istio-ingress/\(stage.slug)"]
spec: http: [{
match: [{uri: prefix: project.authProxyPrefix}]
route: [{
destination: host: Name
destination: port: number: 80
}]
}]
}
// 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

@@ -2,11 +2,15 @@ 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 & {
@@ -17,11 +21,42 @@ import h "github.com/holos-run/holos/api/v1alpha1"
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}
@@ -40,13 +75,48 @@ import h "github.com/holos-run/holos/api/v1alpha1"
stage: string | "dev" | "prod"
slug: "\(name)-\(project)"
namespace: "\(name)-\(project)"
dnsSegments: [...string] | *[name]
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: {
@@ -68,3 +138,58 @@ import h "github.com/holos-run/holos/api/v1alpha1"
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

@@ -233,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.
@@ -299,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

@@ -1 +1 @@
0
3