Compare commits

...

30 Commits

Author SHA1 Message Date
Jeff McCune
0d7bbbb659 (#48) Disable pg spec.dataSource for standby cluster
Problem:
The standby cluster on k2 fails to start.  A pgbackrest pod first
restores the database from S3, then the pgha nodes try to replay the WAL
as part of the standby initialization process.  This fails because the
PGDATA directory is not empty.

Solution:
Specify the spec.dataSource field only when the cluster is configured as
a primary cluster.

Result:
Non-primary clusters are standby, they skip the pgbackrest job to
restore from S3 and move straight to patroni replaying the WAL from S3
as part of the pgha pods.

One of the two pgha pods becomes the "standby leader" and restores the
WAL from S3.  The other is a cascading standby and then restores the
same WAL from the standby leader.

After 8 minutes both pods are ready.

```
❯ k get pods
NAME                               READY   STATUS    RESTARTS   AGE
zitadel-pgbouncer-d9f8cffc-j469g   2/2     Running   0          11m
zitadel-pgbouncer-d9f8cffc-xq29g   2/2     Running   0          11m
zitadel-pgha1-27w7-0               4/4     Running   0          11m
zitadel-pgha1-c5qj-0               4/4     Running   0          11m
zitadel-repo-host-0                2/2     Running   0          11m
```
2024-03-11 17:56:47 -07:00
Jeff McCune
3f3e36bbe9 (#48) Split workload into foundation and accounts
Problem:
The k3 and k4 clusters are getting the Zitadel components which are
really only intended for the core cluster pair.

Solution:
Split the workload subtree into two, named foundation and accounts.  The
core cluster pair gets foundation+accounts while the kX clusters get
just the foundation subtree.

Result:
prod-zitadel-iam is no longer managed on k3 and k4
2024-03-11 15:20:35 -07:00
Jeff McCune
9f41478d33 (#48) Restore from Monday morning after Gary and Nate registered
Set the restore point to time="2024-03-11T17:08:58Z" level=info
msg="crunchy-pgbackrest ends" which is just after Gary and Nate
registered and were granted the cluster-admin role.
2024-03-11 10:18:45 -07:00
Jeff McCune
b86fee04fc (#48) v0.55.4 to rebuild k3, k4, k5 2024-03-11 08:48:07 -07:00
Jeff McCune
c78da6949f Merge pull request #51 from holos-run/jeff/48-zitadel-backups
(#48) Custom PGO Certs for Zitadel
2024-03-10 23:08:29 -07:00
Jeff McCune
7b215bb8f1 (#48) Custom PGO Certs for Zitadel
The [Streaming Standby][standby] architecture requires custom tls certs
for two clusters in two regions to connect to each other.

This patch manages the custom certs following the configuration
described in the article [Using Cert Manager to Deploy TLS for Postgres
on Kubernetes][article].

NOTE: One thing not mentioned anywhere in the crunchy documentation is
how custom tls certs work with pgbouncer.  The pgbouncer service uses a
tls certificate issued by the pgo root cert, not by the custom
certificate authority.

For this reason, we use kustomize to patch the zitadel Deployment and
the zitadel-init and zitadel-setup Jobs.  The patch projects the ca
bundle from the `zitadel-pgbouncer` secret into the zitadel pods at
/pgbouncer/ca.crt

[standby]: https://access.crunchydata.com/documentation/postgres-operator/latest/architecture/disaster-recovery#streaming-standby-with-an-external-repo
[article]: https://www.crunchydata.com/blog/using-cert-manager-to-deploy-tls-for-postgres-on-kubernetes
2024-03-10 22:54:06 -07:00
Jeff McCune
78cec76a96 (#48) Restore ZITADEL from point in time full backup
A full backup was taken using:

```
kubectl annotate postgrescluster zitadel postgres-operator.crunchydata.com/pgbackrest-backup="$(date)"
```

And completed with:

```
❯ k logs -f zitadel-backup-5r6v-v5jnm
time="2024-03-10T21:52:15Z" level=info msg="crunchy-pgbackrest starts"
time="2024-03-10T21:52:15Z" level=info msg="debug flag set to false"
time="2024-03-10T21:52:15Z" level=info msg="backrest backup command requested"
time="2024-03-10T21:52:15Z" level=info msg="command to execute is [pgbackrest backup --stanza=db --repo=2 --type=full]"
time="2024-03-10T21:55:18Z" level=info msg="crunchy-pgbackrest ends"
```

This patch verifies the point in time backup is robust in the face of
the following operations:

1. pg cluster zitadel was deleted (whole namespace emptied)
2. pg cluster zitadel was re-created _without_ a `dataSource`
3. pgo initailized a new database and backed up the blank database to
   S3.
4. pg cluster zitadel was deleted again.
5. pg cluster zitadel was re-created with `dataSource` `options: ["--type=time", "--target=\"2024-03-10 21:56:00+00\""]` (Just after the full backup completed)
6. Restore completed successfully.
7. Applied the holos zitadel component.
8. Zitadel came up successfully and user login worked as expected.

- [x] Perform an in place [restore][restore] from [s3][bucket].
- [x] Set repo1-retention-full to clear warning

[restore]: https://access.crunchydata.com/documentation/postgres-operator/latest/tutorials/backups-disaster-recovery/disaster-recovery#restore-properties
[bucket]: https://access.crunchydata.com/documentation/postgres-operator/latest/tutorials/backups-disaster-recovery/disaster-recovery#cloud-based-data-source
2024-03-10 17:42:54 -07:00
Jeff McCune
0e98ad2ecb (#48) Zitadel Backups
This patch configures backups suitable to support the [Streaming Standby
with an External Repo][0] architecture.

- [x] PGO [Multiple Backup Repositories][1] to k8s pv and s3.
- [x] [Encryption][2] of backups to S3.
- [x] [Remove SUPERUSER][3] role from zitadel-admin pg user to work with pgbouncer.  Resolves zitadel-init job failure.
- [x] Take a [Manual Backup][5]

[0]: https://access.crunchydata.com/documentation/postgres-operator/latest/architecture/disaster-recovery#streaming-standby-with-an-external-repo
[1]: https://access.crunchydata.com/documentation/postgres-operator/latest/tutorials/backups-disaster-recovery/backups#set-up-multiple-backup-repositories
[2]: https://access.crunchydata.com/documentation/postgres-operator/latest/tutorials/backups-disaster-recovery/backups#encryption
[3]: https://github.com/CrunchyData/postgres-operator/issues/3095#issuecomment-1904712211
[4]: https://access.crunchydata.com/documentation/postgres-operator/latest/tutorials/backups-disaster-recovery/disaster-recovery#streaming-standby-with-an-external-repo
[5]: https://access.crunchydata.com/documentation/postgres-operator/latest/tutorials/backups-disaster-recovery/backup-management#taking-a-one-off-backup
2024-03-10 16:38:56 -07:00
Jeff McCune
30bb3f183a (#50) Describe type as strings to match others 2024-03-10 11:29:19 -07:00
Jeff McCune
1369338f3c (#50) Add -n shorthand for --namespace for secrets
It's annoying holos get secret -n foo doesn't work like kubectl get
secret -n foo works.

Closes: #50
2024-03-10 10:45:49 -07:00
Jeff McCune
ac03f64724 (#45) Configure ZITADEL to use pgbouncer 2024-03-09 09:44:33 -08:00
Jeff McCune
bea4468972 (#42) Remove cert manager db ca components
Simpler to let postgres manage the certs.  TLS is in verify-full mode
with the pgo configured certs.
2024-03-08 21:34:26 -08:00
Jeff McCune
224adffa15 (#42) Add holos components for zitadel with postgres
To establish the canonical https://login.ois.run identity issuer on the
core cluster pair.

Custom resources for PGO have been imported with:

    timoni mod vendor crds -f deploy/clusters/core2/components/prod-pgo-crds/prod-pgo-crds.gen.yaml

Note, the zitadel tls connection took some considerable effort to get
working.  We intentionally use pgo issued certs to reduce the toil of
managing certs issued by cert manager.

The default tls configuration of pgo is pretty good with verify full
enabled.
2024-03-08 21:29:25 -08:00
Jeff McCune
b4d34ffdbc (#42) Fix incorrect ceph pool for core2 cluster
The core2 cluster cannot provision pvcs because it's using the k8s-dev
pool when it has credentials valid only for the k8s-prod pool.

This patch adds an entry to the platform cluster map to configure the
pool for each cluster, with a default of k8s-dev.
2024-03-08 13:14:27 -08:00
Jeff McCune
a85db9cf5e (#42) Add KustomizeBuild holos component type to install pgo
PGO uses plain yaml and kustomize as the recommended installation
method.  Holos supports upstream by adding a new PlainFiles component
kind, which simply copies files into place and lets kustomize handle the
generation of the api objects.

Cue is responsible for very little in this kind of component, basically
allowing overlay resources if needed and deferring everything else to
the holos cli.

The holos cli in turn is responsible for executing kubectl kustomize
build on the input directory to produce the rendered output, then writes
the rendered output into place.
2024-03-08 11:27:42 -08:00
Jeff McCune
990c82432c (#40) Fix go releaser with standard arc runners
Standard arc runner image is missing gpg and git.
2024-03-07 22:59:15 -08:00
Jeff McCune
e3673b594c Merge pull request #41 from holos-run/jeff/40-actions-runners
(#40) Actions Runner Controller (Runner Scale Sets)
2024-03-07 22:43:16 -08:00
Jeff McCune
f8cf278a24 (#40) bump to v0.54.0 2024-03-07 22:37:51 -08:00
Jeff McCune
b0bc596a49 (#40) Update workflow to run on arc runner set
Matches the value of the github/arc/runner component helm release, which
is the installation name.
2024-03-07 22:37:51 -08:00
Jeff McCune
4501ceec05 (#40) Use baseline security context for GitHub arc
Without this patch the arc controller fails to create a listener.  The
template for the listener doesn't appear to be configurable from the
chart.

Could patch the listener pod template with kustomize, do this as a
follow up feature.

With this patch we get the expected two pods in the runner system
namespace:

```
❯ k get pods
NAME                                 READY   STATUS    RESTARTS   AGE
gha-rs-7db9c9f7-listener             1/1     Running   0          43s
gha-rs-controller-56bb9c77d9-6tjch   1/1     Running   0          8s
```
2024-03-07 22:37:50 -08:00
Jeff McCune
4183fdfd42 (#40) Note the helm release name is the installation name
Which is the value of the `runs-on` field in workflows.
2024-03-07 22:37:50 -08:00
Jeff McCune
2595793019 (#40) Do not force the namespace with kustomize
To avoid confining the custom resource definitions to a namespace.
2024-03-07 22:37:50 -08:00
Jeff McCune
aa3d1914b1 (#40) Manage the actions runner scale sets 2024-03-07 22:37:49 -08:00
Jeff McCune
679ddbb6bf (#40) Use Restricted pod security for arc runners
Might as well put the restriction in place before deploying the runners
to see what breaks.
2024-03-07 22:37:49 -08:00
Jeff McCune
b1d7d07a04 (#40) Add field for helm chart release name
The resource names for the arc controller are too long:

❯ k get pods -n arc-systems
NAME                                                              READY   STATUS    RESTARTS   AGE
gha-runner-scale-set-controller-gha-rs-controller-6bdf45bd6jx5n   1/1     Running   0          59m

Solve the problem by allowing components to set the release name to
`gha-rs-controller` which requires an additional field from the cue code
to differentiate from the chart name.
2024-03-07 20:40:31 -08:00
Jeff McCune
5f58263232 (#40) Create arc namespaces
Named after the upstream install guide, though arc-systems makes me
twitch for arc-system.
2024-03-07 20:37:35 -08:00
Jeff McCune
b6bdd072f7 (#40) Include crds when running helm template
Might need to make this a configurable option, but for now just always
do it.
2024-03-07 20:37:35 -08:00
Jeff McCune
509f2141ac (#40) Actions Runner Controller
This patch adds support for helm oci images which are used by the
gha-runner-scale-set-controller.

For example, arc is installed normally with:

```
NAMESPACE="arc-systems"
helm install arc \
    --namespace "${NAMESPACE}" \
    --create-namespace \
    oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set-controller
```

This patch caches the oci image in the same way as the repository based
method.

Refer to: https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners-with-actions-runner-controller/quickstart-for-actions-runner-controller
2024-03-07 20:37:35 -08:00
Jeff McCune
4c2bc34d58 (#32) SecretStore Component
Separate the SecretStore resources from the namespaces component because
it creates a deadlock.  The secretstore crds don't get applied until the
eso component is managed.

The namespaces component should have nothing but core api objects, no
custom resources.
2024-03-07 16:01:22 -08:00
Jeff McCune
d831070f53 Trim trailing newlines from files when creating secrets
Without this patch, the pattern of echoing data (without -n) or editing
files in a directory to represent the keys of a secret results in a
trailing newline in the kubernetes Secret.

This patch trims off the trailing newline by default, with the option to
preserve it with the --trim-trailing-newlines=false flag.
2024-03-06 11:21:32 -08:00
89 changed files with 30854 additions and 302 deletions

View File

@@ -15,7 +15,7 @@ permissions:
jobs:
golangci:
name: lint
runs-on: [self-hosted, k8s]
runs-on: gha-rs
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5

View File

@@ -2,17 +2,20 @@ name: Release
on:
push:
# Run only against tags
tags:
- '*'
branches:
- release
permissions:
contents: write
jobs:
goreleaser:
runs-on: [self-hosted, k8s]
runs-on: gha-rs
steps:
- name: Provide GPG and Git
run: sudo apt update && sudo apt -qq -y install gnupg git
- name: Checkout
uses: actions/checkout@v4
with:

View File

@@ -13,7 +13,7 @@ permissions:
jobs:
test:
runs-on: [self-hosted, k8s]
runs-on: gha-rs
steps:
- name: Checkout code
uses: actions/checkout@v4
@@ -23,10 +23,14 @@ jobs:
with:
go-version: stable
- name: Provide unzip for Helm
run: sudo apt update && sudo apt -qq -y install curl zip unzip tar bzip2
- name: Set up Helm
uses: azure/setup-helm@v4.1.0
with:
version: 'latest'
uses: azure/setup-helm@v4
- name: Set up Kubectl
uses: azure/setup-kubectl@v3
- name: Test
run: ./scripts/test

View File

@@ -16,6 +16,7 @@ namespace: "zitadel"
chart: {
name: "zitadel"
version: "7.9.0"
release: name
repository: {
name: "zitadel"
url: "https://charts.zitadel.com"

View File

@@ -0,0 +1,33 @@
# Kustomize is a supported holos component kind
exec holos render --cluster-name=mycluster . --log-level=debug
# Want generated output
cmp want.yaml deploy/clusters/mycluster/components/kstest/kstest.gen.yaml
-- cue.mod --
package holos
-- component.cue --
package holos
cluster: string @tag(cluster, string)
apiVersion: "holos.run/v1alpha1"
kind: "KustomizeBuild"
metadata: name: "kstest"
-- kustomization.yaml --
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: mynamespace
resources:
- serviceaccount.yaml
-- serviceaccount.yaml --
apiVersion: v1
kind: ServiceAccount
metadata:
name: test
-- want.yaml --
apiVersion: v1
kind: ServiceAccount
metadata:
name: test
namespace: mynamespace

View File

@@ -0,0 +1,975 @@
// Code generated by timoni. DO NOT EDIT.
//timoni:generate timoni vendor crd -f /home/jeff/workspace/holos-run/holos-infra/deploy/clusters/core2/components/prod-pgo-crds/prod-pgo-crds.gen.yaml
package v1beta1
import "strings"
// PGAdmin is the Schema for the pgadmins API
#PGAdmin: {
// APIVersion defines the versioned schema of this representation
// of an object. Servers should convert recognized schemas to the
// latest internal value, and may reject unrecognized values.
// More info:
// https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
apiVersion: "postgres-operator.crunchydata.com/v1beta1"
// Kind is a string value representing the REST resource this
// object represents. Servers may infer this from the endpoint
// the client submits requests to. Cannot be updated. In
// CamelCase. More info:
// https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
kind: "PGAdmin"
metadata!: {
name!: strings.MaxRunes(253) & strings.MinRunes(1) & {
string
}
namespace!: strings.MaxRunes(63) & strings.MinRunes(1) & {
string
}
labels?: {
[string]: string
}
annotations?: {
[string]: string
}
}
// PGAdminSpec defines the desired state of PGAdmin
spec!: #PGAdminSpec
}
// PGAdminSpec defines the desired state of PGAdmin
#PGAdminSpec: {
// Scheduling constraints of the PGAdmin pod. More info:
// https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node
affinity?: {
// Describes node affinity scheduling rules for the pod.
nodeAffinity?: {
// The scheduler will prefer to schedule pods to nodes that
// satisfy the affinity expressions specified by this field, but
// it may choose a node that violates one or more of the
// expressions. The node that is most preferred is the one with
// the greatest sum of weights, i.e. for each node that meets all
// of the scheduling requirements (resource request,
// requiredDuringScheduling affinity expressions, etc.), compute
// a sum by iterating through the elements of this field and
// adding "weight" to the sum if the node matches the
// corresponding matchExpressions; the node(s) with the highest
// sum are the most preferred.
preferredDuringSchedulingIgnoredDuringExecution?: [...{
// A node selector term, associated with the corresponding weight.
preference: {
// A list of node selector requirements by node's labels.
matchExpressions?: [...{
// The label key that the selector applies to.
key: string
// Represents a key's relationship to a set of values. Valid
// operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt.
operator: string
// An array of string values. If the operator is In or NotIn, the
// values array must be non-empty. If the operator is Exists or
// DoesNotExist, the values array must be empty. If the operator
// is Gt or Lt, the values array must have a single element,
// which will be interpreted as an integer. This array is
// replaced during a strategic merge patch.
values?: [...string]
}]
// A list of node selector requirements by node's fields.
matchFields?: [...{
// The label key that the selector applies to.
key: string
// Represents a key's relationship to a set of values. Valid
// operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt.
operator: string
// An array of string values. If the operator is In or NotIn, the
// values array must be non-empty. If the operator is Exists or
// DoesNotExist, the values array must be empty. If the operator
// is Gt or Lt, the values array must have a single element,
// which will be interpreted as an integer. This array is
// replaced during a strategic merge patch.
values?: [...string]
}]
}
// Weight associated with matching the corresponding
// nodeSelectorTerm, in the range 1-100.
weight: int
}]
requiredDuringSchedulingIgnoredDuringExecution?: {
// Required. A list of node selector terms. The terms are ORed.
nodeSelectorTerms: [...{
// A list of node selector requirements by node's labels.
matchExpressions?: [...{
// The label key that the selector applies to.
key: string
// Represents a key's relationship to a set of values. Valid
// operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt.
operator: string
// An array of string values. If the operator is In or NotIn, the
// values array must be non-empty. If the operator is Exists or
// DoesNotExist, the values array must be empty. If the operator
// is Gt or Lt, the values array must have a single element,
// which will be interpreted as an integer. This array is
// replaced during a strategic merge patch.
values?: [...string]
}]
// A list of node selector requirements by node's fields.
matchFields?: [...{
// The label key that the selector applies to.
key: string
// Represents a key's relationship to a set of values. Valid
// operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt.
operator: string
// An array of string values. If the operator is In or NotIn, the
// values array must be non-empty. If the operator is Exists or
// DoesNotExist, the values array must be empty. If the operator
// is Gt or Lt, the values array must have a single element,
// which will be interpreted as an integer. This array is
// replaced during a strategic merge patch.
values?: [...string]
}]
}]
}
}
// Describes pod affinity scheduling rules (e.g. co-locate this
// pod in the same node, zone, etc. as some other pod(s)).
podAffinity?: {
// The scheduler will prefer to schedule pods to nodes that
// satisfy the affinity expressions specified by this field, but
// it may choose a node that violates one or more of the
// expressions. The node that is most preferred is the one with
// the greatest sum of weights, i.e. for each node that meets all
// of the scheduling requirements (resource request,
// requiredDuringScheduling affinity expressions, etc.), compute
// a sum by iterating through the elements of this field and
// adding "weight" to the sum if the node has pods which matches
// the corresponding podAffinityTerm; the node(s) with the
// highest sum are the most preferred.
preferredDuringSchedulingIgnoredDuringExecution?: [...{
// Required. A pod affinity term, associated with the
// corresponding weight.
podAffinityTerm: {
// A label query over a set of resources, in this case pods.
labelSelector?: {
// matchExpressions is a list of label selector requirements. The
// requirements are ANDed.
matchExpressions?: [...{
// key is the label key that the selector applies to.
key: string
// operator represents a key's relationship to a set of values.
// Valid operators are In, NotIn, Exists and DoesNotExist.
operator: string
// values is an array of string values. If the operator is In or
// NotIn, the values array must be non-empty. If the operator is
// Exists or DoesNotExist, the values array must be empty. This
// array is replaced during a strategic merge patch.
values?: [...string]
}]
// matchLabels is a map of {key,value} pairs. A single {key,value}
// in the matchLabels map is equivalent to an element of
// matchExpressions, whose key field is "key", the operator is
// "In", and the values array contains only "value". The
// requirements are ANDed.
matchLabels?: {
[string]: string
}
}
// A label query over the set of namespaces that the term applies
// to. The term is applied to the union of the namespaces
// selected by this field and the ones listed in the namespaces
// field. null selector and null or empty namespaces list means
// "this pod's namespace". An empty selector ({}) matches all
// namespaces.
namespaceSelector?: {
// matchExpressions is a list of label selector requirements. The
// requirements are ANDed.
matchExpressions?: [...{
// key is the label key that the selector applies to.
key: string
// operator represents a key's relationship to a set of values.
// Valid operators are In, NotIn, Exists and DoesNotExist.
operator: string
// values is an array of string values. If the operator is In or
// NotIn, the values array must be non-empty. If the operator is
// Exists or DoesNotExist, the values array must be empty. This
// array is replaced during a strategic merge patch.
values?: [...string]
}]
// matchLabels is a map of {key,value} pairs. A single {key,value}
// in the matchLabels map is equivalent to an element of
// matchExpressions, whose key field is "key", the operator is
// "In", and the values array contains only "value". The
// requirements are ANDed.
matchLabels?: {
[string]: string
}
}
// namespaces specifies a static list of namespace names that the
// term applies to. The term is applied to the union of the
// namespaces listed in this field and the ones selected by
// namespaceSelector. null or empty namespaces list and null
// namespaceSelector means "this pod's namespace".
namespaces?: [...string]
// This pod should be co-located (affinity) or not co-located
// (anti-affinity) with the pods matching the labelSelector in
// the specified namespaces, where co-located is defined as
// running on a node whose value of the label with key
// topologyKey matches that of any node on which any of the
// selected pods is running. Empty topologyKey is not allowed.
topologyKey: string
}
// weight associated with matching the corresponding
// podAffinityTerm, in the range 1-100.
weight: int
}]
// If the affinity requirements specified by this field are not
// met at scheduling time, the pod will not be scheduled onto the
// node. If the affinity requirements specified by this field
// cease to be met at some point during pod execution (e.g. due
// to a pod label update), the system may or may not try to
// eventually evict the pod from its node. When there are
// multiple elements, the lists of nodes corresponding to each
// podAffinityTerm are intersected, i.e. all terms must be
// satisfied.
requiredDuringSchedulingIgnoredDuringExecution?: [...{
// A label query over a set of resources, in this case pods.
labelSelector?: {
// matchExpressions is a list of label selector requirements. The
// requirements are ANDed.
matchExpressions?: [...{
// key is the label key that the selector applies to.
key: string
// operator represents a key's relationship to a set of values.
// Valid operators are In, NotIn, Exists and DoesNotExist.
operator: string
// values is an array of string values. If the operator is In or
// NotIn, the values array must be non-empty. If the operator is
// Exists or DoesNotExist, the values array must be empty. This
// array is replaced during a strategic merge patch.
values?: [...string]
}]
// matchLabels is a map of {key,value} pairs. A single {key,value}
// in the matchLabels map is equivalent to an element of
// matchExpressions, whose key field is "key", the operator is
// "In", and the values array contains only "value". The
// requirements are ANDed.
matchLabels?: {
[string]: string
}
}
// A label query over the set of namespaces that the term applies
// to. The term is applied to the union of the namespaces
// selected by this field and the ones listed in the namespaces
// field. null selector and null or empty namespaces list means
// "this pod's namespace". An empty selector ({}) matches all
// namespaces.
namespaceSelector?: {
// matchExpressions is a list of label selector requirements. The
// requirements are ANDed.
matchExpressions?: [...{
// key is the label key that the selector applies to.
key: string
// operator represents a key's relationship to a set of values.
// Valid operators are In, NotIn, Exists and DoesNotExist.
operator: string
// values is an array of string values. If the operator is In or
// NotIn, the values array must be non-empty. If the operator is
// Exists or DoesNotExist, the values array must be empty. This
// array is replaced during a strategic merge patch.
values?: [...string]
}]
// matchLabels is a map of {key,value} pairs. A single {key,value}
// in the matchLabels map is equivalent to an element of
// matchExpressions, whose key field is "key", the operator is
// "In", and the values array contains only "value". The
// requirements are ANDed.
matchLabels?: {
[string]: string
}
}
// namespaces specifies a static list of namespace names that the
// term applies to. The term is applied to the union of the
// namespaces listed in this field and the ones selected by
// namespaceSelector. null or empty namespaces list and null
// namespaceSelector means "this pod's namespace".
namespaces?: [...string]
// This pod should be co-located (affinity) or not co-located
// (anti-affinity) with the pods matching the labelSelector in
// the specified namespaces, where co-located is defined as
// running on a node whose value of the label with key
// topologyKey matches that of any node on which any of the
// selected pods is running. Empty topologyKey is not allowed.
topologyKey: string
}]
}
// Describes pod anti-affinity scheduling rules (e.g. avoid
// putting this pod in the same node, zone, etc. as some other
// pod(s)).
podAntiAffinity?: {
// The scheduler will prefer to schedule pods to nodes that
// satisfy the anti-affinity expressions specified by this field,
// but it may choose a node that violates one or more of the
// expressions. The node that is most preferred is the one with
// the greatest sum of weights, i.e. for each node that meets all
// of the scheduling requirements (resource request,
// requiredDuringScheduling anti-affinity expressions, etc.),
// compute a sum by iterating through the elements of this field
// and adding "weight" to the sum if the node has pods which
// matches the corresponding podAffinityTerm; the node(s) with
// the highest sum are the most preferred.
preferredDuringSchedulingIgnoredDuringExecution?: [...{
// Required. A pod affinity term, associated with the
// corresponding weight.
podAffinityTerm: {
// A label query over a set of resources, in this case pods.
labelSelector?: {
// matchExpressions is a list of label selector requirements. The
// requirements are ANDed.
matchExpressions?: [...{
// key is the label key that the selector applies to.
key: string
// operator represents a key's relationship to a set of values.
// Valid operators are In, NotIn, Exists and DoesNotExist.
operator: string
// values is an array of string values. If the operator is In or
// NotIn, the values array must be non-empty. If the operator is
// Exists or DoesNotExist, the values array must be empty. This
// array is replaced during a strategic merge patch.
values?: [...string]
}]
// matchLabels is a map of {key,value} pairs. A single {key,value}
// in the matchLabels map is equivalent to an element of
// matchExpressions, whose key field is "key", the operator is
// "In", and the values array contains only "value". The
// requirements are ANDed.
matchLabels?: {
[string]: string
}
}
// A label query over the set of namespaces that the term applies
// to. The term is applied to the union of the namespaces
// selected by this field and the ones listed in the namespaces
// field. null selector and null or empty namespaces list means
// "this pod's namespace". An empty selector ({}) matches all
// namespaces.
namespaceSelector?: {
// matchExpressions is a list of label selector requirements. The
// requirements are ANDed.
matchExpressions?: [...{
// key is the label key that the selector applies to.
key: string
// operator represents a key's relationship to a set of values.
// Valid operators are In, NotIn, Exists and DoesNotExist.
operator: string
// values is an array of string values. If the operator is In or
// NotIn, the values array must be non-empty. If the operator is
// Exists or DoesNotExist, the values array must be empty. This
// array is replaced during a strategic merge patch.
values?: [...string]
}]
// matchLabels is a map of {key,value} pairs. A single {key,value}
// in the matchLabels map is equivalent to an element of
// matchExpressions, whose key field is "key", the operator is
// "In", and the values array contains only "value". The
// requirements are ANDed.
matchLabels?: {
[string]: string
}
}
// namespaces specifies a static list of namespace names that the
// term applies to. The term is applied to the union of the
// namespaces listed in this field and the ones selected by
// namespaceSelector. null or empty namespaces list and null
// namespaceSelector means "this pod's namespace".
namespaces?: [...string]
// This pod should be co-located (affinity) or not co-located
// (anti-affinity) with the pods matching the labelSelector in
// the specified namespaces, where co-located is defined as
// running on a node whose value of the label with key
// topologyKey matches that of any node on which any of the
// selected pods is running. Empty topologyKey is not allowed.
topologyKey: string
}
// weight associated with matching the corresponding
// podAffinityTerm, in the range 1-100.
weight: int
}]
// If the anti-affinity requirements specified by this field are
// not met at scheduling time, the pod will not be scheduled onto
// the node. If the anti-affinity requirements specified by this
// field cease to be met at some point during pod execution (e.g.
// due to a pod label update), the system may or may not try to
// eventually evict the pod from its node. When there are
// multiple elements, the lists of nodes corresponding to each
// podAffinityTerm are intersected, i.e. all terms must be
// satisfied.
requiredDuringSchedulingIgnoredDuringExecution?: [...{
// A label query over a set of resources, in this case pods.
labelSelector?: {
// matchExpressions is a list of label selector requirements. The
// requirements are ANDed.
matchExpressions?: [...{
// key is the label key that the selector applies to.
key: string
// operator represents a key's relationship to a set of values.
// Valid operators are In, NotIn, Exists and DoesNotExist.
operator: string
// values is an array of string values. If the operator is In or
// NotIn, the values array must be non-empty. If the operator is
// Exists or DoesNotExist, the values array must be empty. This
// array is replaced during a strategic merge patch.
values?: [...string]
}]
// matchLabels is a map of {key,value} pairs. A single {key,value}
// in the matchLabels map is equivalent to an element of
// matchExpressions, whose key field is "key", the operator is
// "In", and the values array contains only "value". The
// requirements are ANDed.
matchLabels?: {
[string]: string
}
}
// A label query over the set of namespaces that the term applies
// to. The term is applied to the union of the namespaces
// selected by this field and the ones listed in the namespaces
// field. null selector and null or empty namespaces list means
// "this pod's namespace". An empty selector ({}) matches all
// namespaces.
namespaceSelector?: {
// matchExpressions is a list of label selector requirements. The
// requirements are ANDed.
matchExpressions?: [...{
// key is the label key that the selector applies to.
key: string
// operator represents a key's relationship to a set of values.
// Valid operators are In, NotIn, Exists and DoesNotExist.
operator: string
// values is an array of string values. If the operator is In or
// NotIn, the values array must be non-empty. If the operator is
// Exists or DoesNotExist, the values array must be empty. This
// array is replaced during a strategic merge patch.
values?: [...string]
}]
// matchLabels is a map of {key,value} pairs. A single {key,value}
// in the matchLabels map is equivalent to an element of
// matchExpressions, whose key field is "key", the operator is
// "In", and the values array contains only "value". The
// requirements are ANDed.
matchLabels?: {
[string]: string
}
}
// namespaces specifies a static list of namespace names that the
// term applies to. The term is applied to the union of the
// namespaces listed in this field and the ones selected by
// namespaceSelector. null or empty namespaces list and null
// namespaceSelector means "this pod's namespace".
namespaces?: [...string]
// This pod should be co-located (affinity) or not co-located
// (anti-affinity) with the pods matching the labelSelector in
// the specified namespaces, where co-located is defined as
// running on a node whose value of the label with key
// topologyKey matches that of any node on which any of the
// selected pods is running. Empty topologyKey is not allowed.
topologyKey: string
}]
}
}
// Configuration settings for the pgAdmin process. Changes to any
// of these values will be loaded without validation. Be careful,
// as you may put pgAdmin into an unusable state.
config?: {
// Files allows the user to mount projected volumes into the
// pgAdmin container so that files can be referenced by pgAdmin
// as needed.
files?: [...{
// configMap information about the configMap data to project
configMap?: {
// items if unspecified, each key-value pair in the Data field of
// the referenced ConfigMap will be projected into the volume as
// a file whose name is the key and content is the value. If
// specified, the listed keys will be projected into the
// specified paths, and unlisted keys will not be present. If a
// key is specified which is not present in the ConfigMap, the
// volume setup will error unless it is marked optional. Paths
// must be relative and may not contain the '..' path or start
// with '..'.
items?: [...{
// key is the key to project.
key: string
// mode is Optional: mode bits used to set permissions on this
// file. Must be an octal value between 0000 and 0777 or a
// decimal value between 0 and 511. YAML accepts both octal and
// decimal values, JSON requires decimal values for mode bits. If
// not specified, the volume defaultMode will be used. This might
// be in conflict with other options that affect the file mode,
// like fsGroup, and the result can be other mode bits set.
mode?: int
// path is the relative path of the file to map the key to. May
// not be an absolute path. May not contain the path element
// '..'. May not start with the string '..'.
path: string
}]
// Name of the referent. More info:
// https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
name?: string
// optional specify whether the ConfigMap or its keys must be
// defined
optional?: bool
}
downwardAPI?: {
// Items is a list of DownwardAPIVolume file
items?: [...{
// Required: Selects a field of the pod: only annotations, labels,
// name and namespace are supported.
fieldRef?: {
// Version of the schema the FieldPath is written in terms of,
// defaults to "v1".
apiVersion?: string
// Path of the field to select in the specified API version.
fieldPath: string
}
// Optional: mode bits used to set permissions on this file, must
// be an octal value between 0000 and 0777 or a decimal value
// between 0 and 511. YAML accepts both octal and decimal values,
// JSON requires decimal values for mode bits. If not specified,
// the volume defaultMode will be used. This might be in conflict
// with other options that affect the file mode, like fsGroup,
// and the result can be other mode bits set.
mode?: int
// Required: Path is the relative path name of the file to be
// created. Must not be absolute or contain the '..' path. Must
// be utf-8 encoded. The first item of the relative path must not
// start with '..'
path: string
// Selects a resource of the container: only resources limits and
// requests (limits.cpu, limits.memory, requests.cpu and
// requests.memory) are currently supported.
resourceFieldRef?: {
// Container name: required for volumes, optional for env vars
containerName?: string
// Specifies the output format of the exposed resources, defaults
// to "1"
divisor?: (int | string) & {
=~"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$"
}
// Required: resource to select
resource: string
}
}]
}
// secret information about the secret data to project
secret?: {
// items if unspecified, each key-value pair in the Data field of
// the referenced Secret will be projected into the volume as a
// file whose name is the key and content is the value. If
// specified, the listed keys will be projected into the
// specified paths, and unlisted keys will not be present. If a
// key is specified which is not present in the Secret, the
// volume setup will error unless it is marked optional. Paths
// must be relative and may not contain the '..' path or start
// with '..'.
items?: [...{
// key is the key to project.
key: string
// mode is Optional: mode bits used to set permissions on this
// file. Must be an octal value between 0000 and 0777 or a
// decimal value between 0 and 511. YAML accepts both octal and
// decimal values, JSON requires decimal values for mode bits. If
// not specified, the volume defaultMode will be used. This might
// be in conflict with other options that affect the file mode,
// like fsGroup, and the result can be other mode bits set.
mode?: int
// path is the relative path of the file to map the key to. May
// not be an absolute path. May not contain the path element
// '..'. May not start with the string '..'.
path: string
}]
// Name of the referent. More info:
// https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
name?: string
// optional field specify whether the Secret or its key must be
// defined
optional?: bool
}
// serviceAccountToken is information about the
// serviceAccountToken data to project
serviceAccountToken?: {
// audience is the intended audience of the token. A recipient of
// a token must identify itself with an identifier specified in
// the audience of the token, and otherwise should reject the
// token. The audience defaults to the identifier of the
// apiserver.
audience?: string
// expirationSeconds is the requested duration of validity of the
// service account token. As the token approaches expiration, the
// kubelet volume plugin will proactively rotate the service
// account token. The kubelet will start trying to rotate the
// token if the token is older than 80 percent of its time to
// live or if the token is older than 24 hours.Defaults to 1 hour
// and must be at least 10 minutes.
expirationSeconds?: int
// path is the path relative to the mount point of the file to
// project the token into.
path: string
}
}]
// A Secret containing the value for the LDAP_BIND_PASSWORD
// setting. More info:
// https://www.pgadmin.org/docs/pgadmin4/latest/ldap.html
ldapBindPassword?: {
// The key of the secret to select from. Must be a valid secret
// key.
key: string
// Name of the referent. More info:
// https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
name?: string
// Specify whether the Secret or its key must be defined
optional?: bool
}
// Settings for the pgAdmin server process. Keys should be
// uppercase and values must be constants. More info:
// https://www.pgadmin.org/docs/pgadmin4/latest/config_py.html
settings?: {
...
}
}
// Defines a PersistentVolumeClaim for pgAdmin data. More info:
// https://kubernetes.io/docs/concepts/storage/persistent-volumes
dataVolumeClaimSpec: {
// accessModes contains the desired access modes the volume should
// have. More info:
// https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1
accessModes?: [...string]
// dataSource field can be used to specify either: * An existing
// VolumeSnapshot object (snapshot.storage.k8s.io/VolumeSnapshot)
// * An existing PVC (PersistentVolumeClaim) If the provisioner
// or an external controller can support the specified data
// source, it will create a new volume based on the contents of
// the specified data source. If the AnyVolumeDataSource feature
// gate is enabled, this field will always have the same contents
// as the DataSourceRef field.
dataSource?: {
// APIGroup is the group for the resource being referenced. If
// APIGroup is not specified, the specified Kind must be in the
// core API group. For any other third-party types, APIGroup is
// required.
apiGroup?: string
// Kind is the type of resource being referenced
kind: string
// Name is the name of resource being referenced
name: string
}
// dataSourceRef specifies the object from which to populate the
// volume with data, if a non-empty volume is desired. This may
// be any local object from a non-empty API group (non core
// object) or a PersistentVolumeClaim object. When this field is
// specified, volume binding will only succeed if the type of the
// specified object matches some installed volume populator or
// dynamic provisioner. This field will replace the functionality
// of the DataSource field and as such if both fields are
// non-empty, they must have the same value. For backwards
// compatibility, both fields (DataSource and DataSourceRef) will
// be set to the same value automatically if one of them is empty
// and the other is non-empty. There are two important
// differences between DataSource and DataSourceRef: * While
// DataSource only allows two specific types of objects,
// DataSourceRef allows any non-core object, as well as
// PersistentVolumeClaim objects. * While DataSource ignores
// disallowed values (dropping them), DataSourceRef preserves all
// values, and generates an error if a disallowed value is
// specified. (Beta) Using this field requires the
// AnyVolumeDataSource feature gate to be enabled.
dataSourceRef?: {
// APIGroup is the group for the resource being referenced. If
// APIGroup is not specified, the specified Kind must be in the
// core API group. For any other third-party types, APIGroup is
// required.
apiGroup?: string
// Kind is the type of resource being referenced
kind: string
// Name is the name of resource being referenced
name: string
}
// resources represents the minimum resources the volume should
// have. If RecoverVolumeExpansionFailure feature is enabled
// users are allowed to specify resource requirements that are
// lower than previous value but must still be higher than
// capacity recorded in the status field of the claim. More info:
// https://kubernetes.io/docs/concepts/storage/persistent-volumes#resources
resources?: {
// Limits describes the maximum amount of compute resources
// allowed. More info:
// https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
limits?: {
[string]: (int | string) & =~"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$"
}
// Requests describes the minimum amount of compute resources
// required. If Requests is omitted for a container, it defaults
// to Limits if that is explicitly specified, otherwise to an
// implementation-defined value. More info:
// https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
requests?: {
[string]: (int | string) & =~"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$"
}
}
// selector is a label query over volumes to consider for binding.
selector?: {
// matchExpressions is a list of label selector requirements. The
// requirements are ANDed.
matchExpressions?: [...{
// key is the label key that the selector applies to.
key: string
// operator represents a key's relationship to a set of values.
// Valid operators are In, NotIn, Exists and DoesNotExist.
operator: string
// values is an array of string values. If the operator is In or
// NotIn, the values array must be non-empty. If the operator is
// Exists or DoesNotExist, the values array must be empty. This
// array is replaced during a strategic merge patch.
values?: [...string]
}]
// matchLabels is a map of {key,value} pairs. A single {key,value}
// in the matchLabels map is equivalent to an element of
// matchExpressions, whose key field is "key", the operator is
// "In", and the values array contains only "value". The
// requirements are ANDed.
matchLabels?: {
[string]: string
}
}
// storageClassName is the name of the StorageClass required by
// the claim. More info:
// https://kubernetes.io/docs/concepts/storage/persistent-volumes#class-1
storageClassName?: string
// volumeMode defines what type of volume is required by the
// claim. Value of Filesystem is implied when not included in
// claim spec.
volumeMode?: string
// volumeName is the binding reference to the PersistentVolume
// backing this claim.
volumeName?: string
}
// The image name to use for pgAdmin instance.
image?: string
// ImagePullPolicy is used to determine when Kubernetes will
// attempt to pull (download) container images. More info:
// https://kubernetes.io/docs/concepts/containers/images/#image-pull-policy
imagePullPolicy?: "Always" | "Never" | "IfNotPresent"
// The image pull secrets used to pull from a private registry.
// Changing this value causes all running PGAdmin pods to
// restart.
// https://k8s.io/docs/tasks/configure-pod-container/pull-image-private-registry/
imagePullSecrets?: [...{
// Name of the referent. More info:
// https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
name?: string
}]
// Metadata contains metadata for custom resources
metadata?: {
annotations?: {
[string]: string
}
labels?: {
[string]: string
}
}
// Priority class name for the PGAdmin pod. Changing this value
// causes PGAdmin pod to restart. More info:
// https://kubernetes.io/docs/concepts/scheduling-eviction/pod-priority-preemption/
priorityClassName?: string
// Resource requirements for the PGAdmin container.
resources?: {
// Limits describes the maximum amount of compute resources
// allowed. More info:
// https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
limits?: {
[string]: (int | string) & =~"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$"
}
// Requests describes the minimum amount of compute resources
// required. If Requests is omitted for a container, it defaults
// to Limits if that is explicitly specified, otherwise to an
// implementation-defined value. More info:
// https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
requests?: {
[string]: (int | string) & =~"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$"
}
}
// ServerGroups for importing PostgresClusters to pgAdmin. To
// create a pgAdmin with no selectors, leave this field empty. A
// pgAdmin created with no `ServerGroups` will not automatically
// add any servers through discovery. PostgresClusters can still
// be added manually.
serverGroups?: [...{
// The name for the ServerGroup in pgAdmin. Must be unique in the
// pgAdmin's ServerGroups since it becomes the ServerGroup name
// in pgAdmin.
name: string
// PostgresClusterSelector selects clusters to dynamically add to
// pgAdmin by matching labels. An empty selector like `{}` will
// select ALL clusters in the namespace.
postgresClusterSelector: {
// matchExpressions is a list of label selector requirements. The
// requirements are ANDed.
matchExpressions?: [...{
// key is the label key that the selector applies to.
key: string
// operator represents a key's relationship to a set of values.
// Valid operators are In, NotIn, Exists and DoesNotExist.
operator: string
// values is an array of string values. If the operator is In or
// NotIn, the values array must be non-empty. If the operator is
// Exists or DoesNotExist, the values array must be empty. This
// array is replaced during a strategic merge patch.
values?: [...string]
}]
// matchLabels is a map of {key,value} pairs. A single {key,value}
// in the matchLabels map is equivalent to an element of
// matchExpressions, whose key field is "key", the operator is
// "In", and the values array contains only "value". The
// requirements are ANDed.
matchLabels?: {
[string]: string
}
}
}]
// Tolerations of the PGAdmin pod. More info:
// https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration
tolerations?: [...{
// Effect indicates the taint effect to match. Empty means match
// all taint effects. When specified, allowed values are
// NoSchedule, PreferNoSchedule and NoExecute.
effect?: string
// Key is the taint key that the toleration applies to. Empty
// means match all taint keys. If the key is empty, operator must
// be Exists; this combination means to match all values and all
// keys.
key?: string
// Operator represents a key's relationship to the value. Valid
// operators are Exists and Equal. Defaults to Equal. Exists is
// equivalent to wildcard for value, so that a pod can tolerate
// all taints of a particular category.
operator?: string
// TolerationSeconds represents the period of time the toleration
// (which must be of effect NoExecute, otherwise this field is
// ignored) tolerates the taint. By default, it is not set, which
// means tolerate the taint forever (do not evict). Zero and
// negative values will be treated as 0 (evict immediately) by
// the system.
tolerationSeconds?: int
// Value is the taint value the toleration matches to. If the
// operator is Exists, the value should be empty, otherwise just
// a regular string.
value?: string
}]
}

View File

@@ -0,0 +1,632 @@
// Code generated by timoni. DO NOT EDIT.
//timoni:generate timoni vendor crd -f /home/jeff/workspace/holos-run/holos-infra/deploy/clusters/core2/components/prod-pgo-crds/prod-pgo-crds.gen.yaml
package v1beta1
import "strings"
// PGUpgrade is the Schema for the pgupgrades API
#PGUpgrade: {
// APIVersion defines the versioned schema of this representation
// of an object. Servers should convert recognized schemas to the
// latest internal value, and may reject unrecognized values.
// More info:
// https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
apiVersion: "postgres-operator.crunchydata.com/v1beta1"
// Kind is a string value representing the REST resource this
// object represents. Servers may infer this from the endpoint
// the client submits requests to. Cannot be updated. In
// CamelCase. More info:
// https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
kind: "PGUpgrade"
metadata!: {
name!: strings.MaxRunes(253) & strings.MinRunes(1) & {
string
}
namespace!: strings.MaxRunes(63) & strings.MinRunes(1) & {
string
}
labels?: {
[string]: string
}
annotations?: {
[string]: string
}
}
// PGUpgradeSpec defines the desired state of PGUpgrade
spec!: #PGUpgradeSpec
}
// PGUpgradeSpec defines the desired state of PGUpgrade
#PGUpgradeSpec: {
// Scheduling constraints of the PGUpgrade pod. More info:
// https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node
affinity?: {
// Describes node affinity scheduling rules for the pod.
nodeAffinity?: {
// The scheduler will prefer to schedule pods to nodes that
// satisfy the affinity expressions specified by this field, but
// it may choose a node that violates one or more of the
// expressions. The node that is most preferred is the one with
// the greatest sum of weights, i.e. for each node that meets all
// of the scheduling requirements (resource request,
// requiredDuringScheduling affinity expressions, etc.), compute
// a sum by iterating through the elements of this field and
// adding "weight" to the sum if the node matches the
// corresponding matchExpressions; the node(s) with the highest
// sum are the most preferred.
preferredDuringSchedulingIgnoredDuringExecution?: [...{
// A node selector term, associated with the corresponding weight.
preference: {
// A list of node selector requirements by node's labels.
matchExpressions?: [...{
// The label key that the selector applies to.
key: string
// Represents a key's relationship to a set of values. Valid
// operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt.
operator: string
// An array of string values. If the operator is In or NotIn, the
// values array must be non-empty. If the operator is Exists or
// DoesNotExist, the values array must be empty. If the operator
// is Gt or Lt, the values array must have a single element,
// which will be interpreted as an integer. This array is
// replaced during a strategic merge patch.
values?: [...string]
}]
// A list of node selector requirements by node's fields.
matchFields?: [...{
// The label key that the selector applies to.
key: string
// Represents a key's relationship to a set of values. Valid
// operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt.
operator: string
// An array of string values. If the operator is In or NotIn, the
// values array must be non-empty. If the operator is Exists or
// DoesNotExist, the values array must be empty. If the operator
// is Gt or Lt, the values array must have a single element,
// which will be interpreted as an integer. This array is
// replaced during a strategic merge patch.
values?: [...string]
}]
}
// Weight associated with matching the corresponding
// nodeSelectorTerm, in the range 1-100.
weight: int
}]
requiredDuringSchedulingIgnoredDuringExecution?: {
// Required. A list of node selector terms. The terms are ORed.
nodeSelectorTerms: [...{
// A list of node selector requirements by node's labels.
matchExpressions?: [...{
// The label key that the selector applies to.
key: string
// Represents a key's relationship to a set of values. Valid
// operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt.
operator: string
// An array of string values. If the operator is In or NotIn, the
// values array must be non-empty. If the operator is Exists or
// DoesNotExist, the values array must be empty. If the operator
// is Gt or Lt, the values array must have a single element,
// which will be interpreted as an integer. This array is
// replaced during a strategic merge patch.
values?: [...string]
}]
// A list of node selector requirements by node's fields.
matchFields?: [...{
// The label key that the selector applies to.
key: string
// Represents a key's relationship to a set of values. Valid
// operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt.
operator: string
// An array of string values. If the operator is In or NotIn, the
// values array must be non-empty. If the operator is Exists or
// DoesNotExist, the values array must be empty. If the operator
// is Gt or Lt, the values array must have a single element,
// which will be interpreted as an integer. This array is
// replaced during a strategic merge patch.
values?: [...string]
}]
}]
}
}
// Describes pod affinity scheduling rules (e.g. co-locate this
// pod in the same node, zone, etc. as some other pod(s)).
podAffinity?: {
// The scheduler will prefer to schedule pods to nodes that
// satisfy the affinity expressions specified by this field, but
// it may choose a node that violates one or more of the
// expressions. The node that is most preferred is the one with
// the greatest sum of weights, i.e. for each node that meets all
// of the scheduling requirements (resource request,
// requiredDuringScheduling affinity expressions, etc.), compute
// a sum by iterating through the elements of this field and
// adding "weight" to the sum if the node has pods which matches
// the corresponding podAffinityTerm; the node(s) with the
// highest sum are the most preferred.
preferredDuringSchedulingIgnoredDuringExecution?: [...{
// Required. A pod affinity term, associated with the
// corresponding weight.
podAffinityTerm: {
// A label query over a set of resources, in this case pods.
labelSelector?: {
// matchExpressions is a list of label selector requirements. The
// requirements are ANDed.
matchExpressions?: [...{
// key is the label key that the selector applies to.
key: string
// operator represents a key's relationship to a set of values.
// Valid operators are In, NotIn, Exists and DoesNotExist.
operator: string
// values is an array of string values. If the operator is In or
// NotIn, the values array must be non-empty. If the operator is
// Exists or DoesNotExist, the values array must be empty. This
// array is replaced during a strategic merge patch.
values?: [...string]
}]
// matchLabels is a map of {key,value} pairs. A single {key,value}
// in the matchLabels map is equivalent to an element of
// matchExpressions, whose key field is "key", the operator is
// "In", and the values array contains only "value". The
// requirements are ANDed.
matchLabels?: {
[string]: string
}
}
// A label query over the set of namespaces that the term applies
// to. The term is applied to the union of the namespaces
// selected by this field and the ones listed in the namespaces
// field. null selector and null or empty namespaces list means
// "this pod's namespace". An empty selector ({}) matches all
// namespaces.
namespaceSelector?: {
// matchExpressions is a list of label selector requirements. The
// requirements are ANDed.
matchExpressions?: [...{
// key is the label key that the selector applies to.
key: string
// operator represents a key's relationship to a set of values.
// Valid operators are In, NotIn, Exists and DoesNotExist.
operator: string
// values is an array of string values. If the operator is In or
// NotIn, the values array must be non-empty. If the operator is
// Exists or DoesNotExist, the values array must be empty. This
// array is replaced during a strategic merge patch.
values?: [...string]
}]
// matchLabels is a map of {key,value} pairs. A single {key,value}
// in the matchLabels map is equivalent to an element of
// matchExpressions, whose key field is "key", the operator is
// "In", and the values array contains only "value". The
// requirements are ANDed.
matchLabels?: {
[string]: string
}
}
// namespaces specifies a static list of namespace names that the
// term applies to. The term is applied to the union of the
// namespaces listed in this field and the ones selected by
// namespaceSelector. null or empty namespaces list and null
// namespaceSelector means "this pod's namespace".
namespaces?: [...string]
// This pod should be co-located (affinity) or not co-located
// (anti-affinity) with the pods matching the labelSelector in
// the specified namespaces, where co-located is defined as
// running on a node whose value of the label with key
// topologyKey matches that of any node on which any of the
// selected pods is running. Empty topologyKey is not allowed.
topologyKey: string
}
// weight associated with matching the corresponding
// podAffinityTerm, in the range 1-100.
weight: int
}]
// If the affinity requirements specified by this field are not
// met at scheduling time, the pod will not be scheduled onto the
// node. If the affinity requirements specified by this field
// cease to be met at some point during pod execution (e.g. due
// to a pod label update), the system may or may not try to
// eventually evict the pod from its node. When there are
// multiple elements, the lists of nodes corresponding to each
// podAffinityTerm are intersected, i.e. all terms must be
// satisfied.
requiredDuringSchedulingIgnoredDuringExecution?: [...{
// A label query over a set of resources, in this case pods.
labelSelector?: {
// matchExpressions is a list of label selector requirements. The
// requirements are ANDed.
matchExpressions?: [...{
// key is the label key that the selector applies to.
key: string
// operator represents a key's relationship to a set of values.
// Valid operators are In, NotIn, Exists and DoesNotExist.
operator: string
// values is an array of string values. If the operator is In or
// NotIn, the values array must be non-empty. If the operator is
// Exists or DoesNotExist, the values array must be empty. This
// array is replaced during a strategic merge patch.
values?: [...string]
}]
// matchLabels is a map of {key,value} pairs. A single {key,value}
// in the matchLabels map is equivalent to an element of
// matchExpressions, whose key field is "key", the operator is
// "In", and the values array contains only "value". The
// requirements are ANDed.
matchLabels?: {
[string]: string
}
}
// A label query over the set of namespaces that the term applies
// to. The term is applied to the union of the namespaces
// selected by this field and the ones listed in the namespaces
// field. null selector and null or empty namespaces list means
// "this pod's namespace". An empty selector ({}) matches all
// namespaces.
namespaceSelector?: {
// matchExpressions is a list of label selector requirements. The
// requirements are ANDed.
matchExpressions?: [...{
// key is the label key that the selector applies to.
key: string
// operator represents a key's relationship to a set of values.
// Valid operators are In, NotIn, Exists and DoesNotExist.
operator: string
// values is an array of string values. If the operator is In or
// NotIn, the values array must be non-empty. If the operator is
// Exists or DoesNotExist, the values array must be empty. This
// array is replaced during a strategic merge patch.
values?: [...string]
}]
// matchLabels is a map of {key,value} pairs. A single {key,value}
// in the matchLabels map is equivalent to an element of
// matchExpressions, whose key field is "key", the operator is
// "In", and the values array contains only "value". The
// requirements are ANDed.
matchLabels?: {
[string]: string
}
}
// namespaces specifies a static list of namespace names that the
// term applies to. The term is applied to the union of the
// namespaces listed in this field and the ones selected by
// namespaceSelector. null or empty namespaces list and null
// namespaceSelector means "this pod's namespace".
namespaces?: [...string]
// This pod should be co-located (affinity) or not co-located
// (anti-affinity) with the pods matching the labelSelector in
// the specified namespaces, where co-located is defined as
// running on a node whose value of the label with key
// topologyKey matches that of any node on which any of the
// selected pods is running. Empty topologyKey is not allowed.
topologyKey: string
}]
}
// Describes pod anti-affinity scheduling rules (e.g. avoid
// putting this pod in the same node, zone, etc. as some other
// pod(s)).
podAntiAffinity?: {
// The scheduler will prefer to schedule pods to nodes that
// satisfy the anti-affinity expressions specified by this field,
// but it may choose a node that violates one or more of the
// expressions. The node that is most preferred is the one with
// the greatest sum of weights, i.e. for each node that meets all
// of the scheduling requirements (resource request,
// requiredDuringScheduling anti-affinity expressions, etc.),
// compute a sum by iterating through the elements of this field
// and adding "weight" to the sum if the node has pods which
// matches the corresponding podAffinityTerm; the node(s) with
// the highest sum are the most preferred.
preferredDuringSchedulingIgnoredDuringExecution?: [...{
// Required. A pod affinity term, associated with the
// corresponding weight.
podAffinityTerm: {
// A label query over a set of resources, in this case pods.
labelSelector?: {
// matchExpressions is a list of label selector requirements. The
// requirements are ANDed.
matchExpressions?: [...{
// key is the label key that the selector applies to.
key: string
// operator represents a key's relationship to a set of values.
// Valid operators are In, NotIn, Exists and DoesNotExist.
operator: string
// values is an array of string values. If the operator is In or
// NotIn, the values array must be non-empty. If the operator is
// Exists or DoesNotExist, the values array must be empty. This
// array is replaced during a strategic merge patch.
values?: [...string]
}]
// matchLabels is a map of {key,value} pairs. A single {key,value}
// in the matchLabels map is equivalent to an element of
// matchExpressions, whose key field is "key", the operator is
// "In", and the values array contains only "value". The
// requirements are ANDed.
matchLabels?: {
[string]: string
}
}
// A label query over the set of namespaces that the term applies
// to. The term is applied to the union of the namespaces
// selected by this field and the ones listed in the namespaces
// field. null selector and null or empty namespaces list means
// "this pod's namespace". An empty selector ({}) matches all
// namespaces.
namespaceSelector?: {
// matchExpressions is a list of label selector requirements. The
// requirements are ANDed.
matchExpressions?: [...{
// key is the label key that the selector applies to.
key: string
// operator represents a key's relationship to a set of values.
// Valid operators are In, NotIn, Exists and DoesNotExist.
operator: string
// values is an array of string values. If the operator is In or
// NotIn, the values array must be non-empty. If the operator is
// Exists or DoesNotExist, the values array must be empty. This
// array is replaced during a strategic merge patch.
values?: [...string]
}]
// matchLabels is a map of {key,value} pairs. A single {key,value}
// in the matchLabels map is equivalent to an element of
// matchExpressions, whose key field is "key", the operator is
// "In", and the values array contains only "value". The
// requirements are ANDed.
matchLabels?: {
[string]: string
}
}
// namespaces specifies a static list of namespace names that the
// term applies to. The term is applied to the union of the
// namespaces listed in this field and the ones selected by
// namespaceSelector. null or empty namespaces list and null
// namespaceSelector means "this pod's namespace".
namespaces?: [...string]
// This pod should be co-located (affinity) or not co-located
// (anti-affinity) with the pods matching the labelSelector in
// the specified namespaces, where co-located is defined as
// running on a node whose value of the label with key
// topologyKey matches that of any node on which any of the
// selected pods is running. Empty topologyKey is not allowed.
topologyKey: string
}
// weight associated with matching the corresponding
// podAffinityTerm, in the range 1-100.
weight: int
}]
// If the anti-affinity requirements specified by this field are
// not met at scheduling time, the pod will not be scheduled onto
// the node. If the anti-affinity requirements specified by this
// field cease to be met at some point during pod execution (e.g.
// due to a pod label update), the system may or may not try to
// eventually evict the pod from its node. When there are
// multiple elements, the lists of nodes corresponding to each
// podAffinityTerm are intersected, i.e. all terms must be
// satisfied.
requiredDuringSchedulingIgnoredDuringExecution?: [...{
// A label query over a set of resources, in this case pods.
labelSelector?: {
// matchExpressions is a list of label selector requirements. The
// requirements are ANDed.
matchExpressions?: [...{
// key is the label key that the selector applies to.
key: string
// operator represents a key's relationship to a set of values.
// Valid operators are In, NotIn, Exists and DoesNotExist.
operator: string
// values is an array of string values. If the operator is In or
// NotIn, the values array must be non-empty. If the operator is
// Exists or DoesNotExist, the values array must be empty. This
// array is replaced during a strategic merge patch.
values?: [...string]
}]
// matchLabels is a map of {key,value} pairs. A single {key,value}
// in the matchLabels map is equivalent to an element of
// matchExpressions, whose key field is "key", the operator is
// "In", and the values array contains only "value". The
// requirements are ANDed.
matchLabels?: {
[string]: string
}
}
// A label query over the set of namespaces that the term applies
// to. The term is applied to the union of the namespaces
// selected by this field and the ones listed in the namespaces
// field. null selector and null or empty namespaces list means
// "this pod's namespace". An empty selector ({}) matches all
// namespaces.
namespaceSelector?: {
// matchExpressions is a list of label selector requirements. The
// requirements are ANDed.
matchExpressions?: [...{
// key is the label key that the selector applies to.
key: string
// operator represents a key's relationship to a set of values.
// Valid operators are In, NotIn, Exists and DoesNotExist.
operator: string
// values is an array of string values. If the operator is In or
// NotIn, the values array must be non-empty. If the operator is
// Exists or DoesNotExist, the values array must be empty. This
// array is replaced during a strategic merge patch.
values?: [...string]
}]
// matchLabels is a map of {key,value} pairs. A single {key,value}
// in the matchLabels map is equivalent to an element of
// matchExpressions, whose key field is "key", the operator is
// "In", and the values array contains only "value". The
// requirements are ANDed.
matchLabels?: {
[string]: string
}
}
// namespaces specifies a static list of namespace names that the
// term applies to. The term is applied to the union of the
// namespaces listed in this field and the ones selected by
// namespaceSelector. null or empty namespaces list and null
// namespaceSelector means "this pod's namespace".
namespaces?: [...string]
// This pod should be co-located (affinity) or not co-located
// (anti-affinity) with the pods matching the labelSelector in
// the specified namespaces, where co-located is defined as
// running on a node whose value of the label with key
// topologyKey matches that of any node on which any of the
// selected pods is running. Empty topologyKey is not allowed.
topologyKey: string
}]
}
}
// The major version of PostgreSQL before the upgrade.
fromPostgresVersion: uint & >=10 & <=16
// The image name to use for major PostgreSQL upgrades.
image?: string
// ImagePullPolicy is used to determine when Kubernetes will
// attempt to pull (download) container images. More info:
// https://kubernetes.io/docs/concepts/containers/images/#image-pull-policy
imagePullPolicy?: "Always" | "Never" | "IfNotPresent"
// The image pull secrets used to pull from a private registry.
// Changing this value causes all running PGUpgrade pods to
// restart.
// https://k8s.io/docs/tasks/configure-pod-container/pull-image-private-registry/
imagePullSecrets?: [...{
// Name of the referent. More info:
// https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
name?: string
}]
// Metadata contains metadata for custom resources
metadata?: {
annotations?: {
[string]: string
}
labels?: {
[string]: string
}
}
// The name of the cluster to be updated
postgresClusterName: strings.MinRunes(1)
// Priority class name for the PGUpgrade pod. Changing this value
// causes PGUpgrade pod to restart. More info:
// https://kubernetes.io/docs/concepts/scheduling-eviction/pod-priority-preemption/
priorityClassName?: string
// Resource requirements for the PGUpgrade container.
resources?: {
// Limits describes the maximum amount of compute resources
// allowed. More info:
// https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
limits?: {
[string]: (int | string) & =~"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$"
}
// Requests describes the minimum amount of compute resources
// required. If Requests is omitted for a container, it defaults
// to Limits if that is explicitly specified, otherwise to an
// implementation-defined value. More info:
// https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
requests?: {
[string]: (int | string) & =~"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$"
}
}
// The image name to use for PostgreSQL containers after upgrade.
// When omitted, the value comes from an operator environment
// variable.
toPostgresImage?: string
// The major version of PostgreSQL to be upgraded to.
toPostgresVersion: uint & >=10 & <=16
// Tolerations of the PGUpgrade pod. More info:
// https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration
tolerations?: [...{
// Effect indicates the taint effect to match. Empty means match
// all taint effects. When specified, allowed values are
// NoSchedule, PreferNoSchedule and NoExecute.
effect?: string
// Key is the taint key that the toleration applies to. Empty
// means match all taint keys. If the key is empty, operator must
// be Exists; this combination means to match all values and all
// keys.
key?: string
// Operator represents a key's relationship to the value. Valid
// operators are Exists and Equal. Defaults to Equal. Exists is
// equivalent to wildcard for value, so that a pod can tolerate
// all taints of a particular category.
operator?: string
// TolerationSeconds represents the period of time the toleration
// (which must be of effect NoExecute, otherwise this field is
// ignored) tolerates the taint. By default, it is not set, which
// means tolerate the taint forever (do not evict). Zero and
// negative values will be treated as 0 (evict immediately) by
// the system.
tolerationSeconds?: int
// Value is the taint value the toleration matches to. If the
// operator is Exists, the value should be empty, otherwise just
// a regular string.
value?: string
}]
}

View File

@@ -44,7 +44,8 @@ package holos
_name: string
_cluster: string
_wildcard: true | *false
metadata: name: string | *"\(_cluster)-\(_name)"
// Enforce this value
metadata: name: "\(_cluster)-\(_name)"
metadata: namespace: string | *"istio-ingress"
spec: {
commonName: string | *"\(_name).\(_cluster).\(#Platform.org.domain)"

View File

@@ -0,0 +1,7 @@
package holos
// Components under this directory are part of this collection
#InputKeys: project: "iam"
// Shared dependencies for all components in this collection.
#DependsOn: namespaces: name: "\(#StageName)-secrets-namespaces"

View File

@@ -0,0 +1,13 @@
package holos
#InputKeys: component: "postgres-certs"
#KubernetesObjects & {
apiObjects: {
ExternalSecret: {
"\(_DBName)-primary-tls": _
"\(_DBName)-repl-tls": _
"\(_DBName)-client-tls": _
"\(_DBName)-root-ca": _
}
}
}

View File

@@ -0,0 +1,171 @@
package holos
#InputKeys: component: "postgres"
#DependsOn: "postgres-certs": _
let Cluster = #Platform.clusters[#ClusterName]
let S3Secret = "pgo-s3-creds"
let ZitadelUser = _DBName
let ZitadelAdmin = "\(_DBName)-admin"
// This must be an external storage bucket for our architecture.
let BucketRepoName = "repo2"
// Restore options. Set the timestamp to a known good point in time.
// time="2024-03-11T17:08:58Z" level=info msg="crunchy-pgbackrest ends"
let RestoreOptions = ["--type=time", "--target=\"2024-03-11 17:10:00+00\""]
#KubernetesObjects & {
apiObjects: {
ExternalSecret: "pgo-s3-creds": _
PostgresCluster: db: #PostgresCluster & HighlyAvailable & {
metadata: name: _DBName
metadata: namespace: #TargetNamespace
spec: {
image: "registry.developers.crunchydata.com/crunchydata/crunchy-postgres:ubi8-16.2-0"
postgresVersion: 16
// Custom certs are necessary for streaming standby replication which we use to replicate between two regions.
// Refer to https://access.crunchydata.com/documentation/postgres-operator/latest/tutorials/backups-disaster-recovery/disaster-recovery#streaming-standby
customTLSSecret: name: "\(_DBName)-primary-tls"
customReplicationTLSSecret: name: "\(_DBName)-repl-tls"
// Refer to https://access.crunchydata.com/documentation/postgres-operator/latest/references/crd/5.5.x/postgrescluster#postgresclusterspecusersindex
users: [
{name: ZitadelUser},
// NOTE: Users with SUPERUSER role cannot log in through pgbouncer. Use options that allow zitadel admin to use pgbouncer.
// Refer to: https://github.com/CrunchyData/postgres-operator/issues/3095#issuecomment-1904712211
{name: ZitadelAdmin, options: "CREATEDB CREATEROLE", databases: [_DBName, "postgres"]},
]
users: [...{databases: [_DBName, ...]}]
instances: [{
replicas: 2
dataVolumeClaimSpec: {
accessModes: ["ReadWriteOnce"]
resources: requests: storage: string | *"1Gi"
}
}]
standby: {
repoName: BucketRepoName
if Cluster.primary {
enabled: false
}
if !Cluster.primary {
enabled: true
}
}
// Restore from backup if and only if the cluster is primary
if Cluster.primary {
dataSource: pgbackrest: {
stanza: "db"
configuration: backups.pgbackrest.configuration
// Restore from known good full backup taken
options: RestoreOptions
global: {
"\(BucketRepoName)-path": "/pgbackrest/\(#TargetNamespace)/\(metadata.name)/\(BucketRepoName)"
"\(BucketRepoName)-cipher-type": "aes-256-cbc"
}
repo: {
name: BucketRepoName
s3: backups.pgbackrest.repos[1].s3
}
}
}
// Refer to https://access.crunchydata.com/documentation/postgres-operator/latest/tutorials/backups-disaster-recovery/backups
backups: pgbackrest: {
configuration: [{secret: name: S3Secret}]
// Defines details for manual pgBackRest backup Jobs
manual: {
// Note: the repoName value must match the config keys in the S3Secret.
// This must be an external repository for backup / restore / regional failovers.
repoName: BucketRepoName
options: ["--type=full", ...]
}
// Defines details for performing an in-place restore using pgBackRest
restore: {
// Enables triggering a restore by annotating the postgrescluster with postgres-operator.crunchydata.com/pgbackrest-restore="$(date)"
enabled: true
repoName: BucketRepoName
}
global: {
// Store only one full backup in the PV because it's more expensive than object storage.
"\(repos[0].name)-retention-full": "1"
// Store 14 days of full backups in the bucket.
"\(BucketRepoName)-retention-full": string | *"14"
"\(BucketRepoName)-retention-full-type": "count" | *"time" // time in days
// Refer to https://access.crunchydata.com/documentation/postgres-operator/latest/tutorials/backups-disaster-recovery/backups#encryption
"\(BucketRepoName)-cipher-type": "aes-256-cbc"
// "The convention we recommend for setting this variable is /pgbackrest/$NAMESPACE/$CLUSTER_NAME/repoN"
// Ref: https://access.crunchydata.com/documentation/postgres-operator/latest/tutorials/backups-disaster-recovery/backups#understanding-backup-configuration-and-basic-operations
"\(BucketRepoName)-path": "/pgbackrest/\(#TargetNamespace)/\(metadata.name)/\(manual.repoName)"
}
repos: [
{
name: "repo1"
volume: volumeClaimSpec: {
accessModes: ["ReadWriteOnce"]
resources: requests: storage: string | *"1Gi"
}
},
{
name: BucketRepoName
// Full backup weekly on Sunday at 1am, differntial daily at 1am every day except Sunday.
schedules: full: string | *"0 1 * * 0"
schedules: differential: string | *"0 1 * * 1-6"
s3: {
bucket: string | *"\(#Platform.org.name)-zitadel-backups"
region: string | *#Backups.s3.region
endpoint: string | *"s3.dualstack.\(region).amazonaws.com"
}
},
]
}
}
}
}
}
// Refer to https://github.com/holos-run/postgres-operator-examples/blob/main/kustomize/high-availability/ha-postgres.yaml
let HighlyAvailable = {
apiVersion: "postgres-operator.crunchydata.com/v1beta1"
kind: "PostgresCluster"
metadata: name: string
spec: {
image: "registry.developers.crunchydata.com/crunchydata/crunchy-postgres:ubi8-16.2-0"
postgresVersion: 16
instances: [{
name: "pgha1"
replicas: 2
dataVolumeClaimSpec: {
accessModes: ["ReadWriteOnce"]
resources: requests: storage: "1Gi"
}
affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: [{
weight: 1
podAffinityTerm: {
topologyKey: "kubernetes.io/hostname"
labelSelector: matchLabels: {
"postgres-operator.crunchydata.com/cluster": metadata.name
"postgres-operator.crunchydata.com/instance-set": name
}
}
}]
}]
backups: pgbackrest: {
image: "registry.developers.crunchydata.com/crunchydata/crunchy-pgbackrest:ubi8-2.49-0"
}
proxy: pgBouncer: {
image: "registry.developers.crunchydata.com/crunchydata/crunchy-pgbouncer:ubi8-1.21-3"
replicas: 2
affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: [{
weight: 1
podAffinityTerm: {
topologyKey: "kubernetes.io/hostname"
labelSelector: matchLabels: {
"postgres-operator.crunchydata.com/cluster": metadata.name
"postgres-operator.crunchydata.com/role": "pgbouncer"
}
}
}]
}
}
}

View File

@@ -0,0 +1,10 @@
package holos
#TargetNamespace: #InstancePrefix + "-zitadel"
// _DBName is the database name used across multiple holos components in this project
_DBName: "zitadel"
// The canonical login domain for the entire platform. Zitadel will be active
// on a single cluster at a time, but always accessible from this domain.
#ExternalDomain: "login.\(#Platform.org.domain)"

View File

@@ -125,7 +125,7 @@ package holos
securityContext: {}
// Additional environment variables
env: []
env: [...]
// - name: ZITADEL_DATABASE_POSTGRES_HOST
// valueFrom:
// secretKeyRef:

View File

@@ -0,0 +1,89 @@
package holos
#Values: {
// Database credentials
// Refer to https://access.crunchydata.com/documentation/postgres-operator/5.2.0/architecture/user-management/
// Refer to https://zitadel.com/docs/self-hosting/manage/database#postgres
env: [
// Connection
{
name: "ZITADEL_DATABASE_POSTGRES_HOST"
valueFrom: secretKeyRef: name: "\(_DBName)-pguser-\(_DBName)"
valueFrom: secretKeyRef: key: "pgbouncer-host"
},
{
name: "ZITADEL_DATABASE_POSTGRES_PORT"
valueFrom: secretKeyRef: name: "\(_DBName)-pguser-\(_DBName)"
valueFrom: secretKeyRef: key: "pgbouncer-port"
},
{
name: "ZITADEL_DATABASE_POSTGRES_DATABASE"
valueFrom: secretKeyRef: name: "\(_DBName)-pguser-\(_DBName)"
valueFrom: secretKeyRef: key: "dbname"
},
// The <db>-pguser-<db> secret contains creds for the unpriviliged zitadel user
{
name: "ZITADEL_DATABASE_POSTGRES_USER_USERNAME"
valueFrom: secretKeyRef: name: "\(_DBName)-pguser-\(_DBName)"
valueFrom: secretKeyRef: key: "user"
},
{
name: "ZITADEL_DATABASE_POSTGRES_USER_PASSWORD"
valueFrom: secretKeyRef: name: "\(_DBName)-pguser-\(_DBName)"
valueFrom: secretKeyRef: key: "password"
},
// The postgres component configures privileged postgres user creds.
{
name: "ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME"
valueFrom: secretKeyRef: name: "\(_DBName)-pguser-\(_DBName)-admin"
valueFrom: secretKeyRef: key: "user"
},
{
name: "ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD"
valueFrom: secretKeyRef: name: "\(_DBName)-pguser-\(_DBName)-admin"
valueFrom: secretKeyRef: key: "password"
},
// CA Cert issued by PGO which issued the pgbouncer tls cert
{
name: "ZITADEL_DATABASE_POSTGRES_USER_SSL_ROOTCERT"
value: "/\(_PGBouncer)/ca.crt"
},
{
name: "ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_ROOTCERT"
value: "/\(_PGBouncer)/ca.crt"
},
]
// Refer to https://zitadel.com/docs/self-hosting/manage/database
zitadel: {
// Zitadel master key
masterkeySecretName: "zitadel-masterkey"
// dbSslCaCrtSecret: "pgo-root-cacert"
// All settings: https://zitadel.com/docs/self-hosting/manage/configure#runtime-configuration-file
// Helm interface: https://github.com/zitadel/zitadel-charts/blob/zitadel-7.4.0/charts/zitadel/values.yaml#L20-L21
configmapConfig: {
// NOTE: You can change the ExternalDomain, ExternalPort and ExternalSecure
// configuration options at any time. However, for ZITADEL to be able to
// pick up the changes, you need to rerun ZITADELs setup phase. Do so with
// kubectl delete job zitadel-setup, then re-apply the new config.
//
// https://zitadel.com/docs/self-hosting/manage/custom-domain
ExternalSecure: true
ExternalDomain: #ExternalDomain
ExternalPort: 443
TLS: Enabled: false
// Database connection credentials are injected via environment variables from the db-pguser-db secret.
Database: postgres: {
MaxOpenConns: 25
MaxIdleConns: 10
MaxConnLifetime: "1h"
MaxConnIdleTime: "5m"
// verify-full verifies the host name matches cert dns names in addition to root ca signature
User: SSL: Mode: "verify-full"
Admin: SSL: Mode: "verify-full"
}
}
}
}

View File

@@ -0,0 +1,102 @@
package holos
import "encoding/yaml"
let Name = "zitadel"
#InputKeys: component: Name
#DependsOn: postgres: _
// Upstream helm chart doesn't specify the namespace field for all resources.
#Kustomization: spec: targetNamespace: #TargetNamespace
#HelmChart & {
namespace: #TargetNamespace
chart: {
name: Name
version: "7.9.0"
repository: {
name: Name
url: "https://charts.zitadel.com"
}
}
values: #Values
apiObjects: {
ExternalSecret: "zitadel-masterkey": _
VirtualService: "\(Name)": {
metadata: name: Name
metadata: namespace: #TargetNamespace
spec: hosts: ["login.\(#Platform.org.domain)"]
spec: gateways: ["istio-ingress/default"]
spec: http: [{route: [{destination: host: Name}]}]
}
}
}
// TODO: Generalize this common pattern of injecting the istio sidecar into a Deployment
let IstioInject = [{op: "add", path: "/spec/template/metadata/labels/sidecar.istio.io~1inject", value: "true"}]
_PGBouncer: "pgbouncer"
let DatabaseCACertPatch = [
{
op: "add"
path: "/spec/template/spec/volumes/-"
value: {
name: _PGBouncer
secret: {
secretName: "\(_DBName)-pgbouncer"
items: [{key: "pgbouncer-frontend.ca-roots", path: "ca.crt"}]
}
}
},
{
op: "add"
path: "/spec/template/spec/containers/0/volumeMounts/-"
value: {
name: _PGBouncer
mountPath: "/" + _PGBouncer
}
},
]
#Kustomize: {
patches: [
{
target: {
group: "apps"
version: "v1"
kind: "Deployment"
name: Name
}
patch: yaml.Marshal(IstioInject)
},
{
target: {
group: "apps"
version: "v1"
kind: "Deployment"
name: Name
}
patch: yaml.Marshal(DatabaseCACertPatch)
},
{
target: {
group: "batch"
version: "v1"
kind: "Job"
name: "\(Name)-init"
}
patch: yaml.Marshal(DatabaseCACertPatch)
},
{
target: {
group: "batch"
version: "v1"
kind: "Job"
name: "\(Name)-setup"
}
patch: yaml.Marshal(DatabaseCACertPatch)
},
]
}

View File

@@ -0,0 +1,9 @@
package holos
// GitHub Actions Runner Controller
#InputKeys: project: "github"
#DependsOn: Namespaces: name: "prod-secrets-namespaces"
#TargetNamespace: #InputKeys.component
#HelmChart: namespace: #TargetNamespace
#HelmChart: chart: version: "0.8.3"

View File

@@ -0,0 +1,26 @@
package holos
#InputKeys: component: "arc-runner"
#Kustomization: spec: targetNamespace: #TargetNamespace
#HelmChart & {
values: {
#Values
controllerServiceAccount: name: "gha-rs-controller"
controllerServiceAccount: namespace: "arc-system"
githubConfigSecret: "controller-manager"
githubConfigUrl: "https://github.com/" + #Platform.org.github.orgs.primary.name
}
apiObjects: ExternalSecret: "\(values.githubConfigSecret)": _
chart: {
// Match the gha-base-name in the chart _helpers.tpl to avoid long full names.
// NOTE: Unfortunately the INSTALLATION_NAME is used as the helm release
// name and GitHub removed support for runner labels, so the only way to
// specify which runner a workflow runs on is using this helm release name.
// The quote is "Update the INSTALLATION_NAME value carefully. You will use
// the installation name as the value of runs-on in your workflows." Refer to
// https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners-with-actions-runner-controller/quickstart-for-actions-runner-controller
release: "gha-rs"
name: "oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set"
}
}

View File

@@ -0,0 +1,192 @@
package holos
#Values: {
//# githubConfigUrl is the GitHub url for where you want to configure runners
//# ex: https://github.com/myorg/myrepo or https://github.com/myorg
githubConfigUrl: string | *""
//# githubConfigSecret is the k8s secrets to use when auth with GitHub API.
//# You can choose to use GitHub App or a PAT token
githubConfigSecret: string | {
//## GitHub Apps Configuration
//# NOTE: IDs MUST be strings, use quotes
//github_app_id: ""
//github_app_installation_id: ""
//github_app_private_key: |
//## GitHub PAT Configuration
github_token: ""
}
//# If you have a pre-define Kubernetes secret in the same namespace the gha-runner-scale-set is going to deploy,
//# you can also reference it via `githubConfigSecret: pre-defined-secret`.
//# You need to make sure your predefined secret has all the required secret data set properly.
//# For a pre-defined secret using GitHub PAT, the secret needs to be created like this:
//# > kubectl create secret generic pre-defined-secret --namespace=my_namespace --from-literal=github_token='ghp_your_pat'
//# For a pre-defined secret using GitHub App, the secret needs to be created like this:
//# > kubectl create secret generic pre-defined-secret --namespace=my_namespace --from-literal=github_app_id=123456 --from-literal=github_app_installation_id=654321 --from-literal=github_app_private_key='-----BEGIN CERTIFICATE-----*******'
// githubConfigSecret: pre-defined-secret
//# proxy can be used to define proxy settings that will be used by the
//# controller, the listener and the runner of this scale set.
//
// proxy:
// http:
// url: http://proxy.com:1234
// credentialSecretRef: proxy-auth # a secret with `username` and `password` keys
// https:
// url: http://proxy.com:1234
// credentialSecretRef: proxy-auth # a secret with `username` and `password` keys
// noProxy:
// - example.com
// - example.org
//# maxRunners is the max number of runners the autoscaling runner set will scale up to.
// maxRunners: 5
//# minRunners is the min number of idle runners. The target number of runners created will be
//# calculated as a sum of minRunners and the number of jobs assigned to the scale set.
// minRunners: 0
// runnerGroup: "default"
//# name of the runner scale set to create. Defaults to the helm release name
// runnerScaleSetName: ""
//# A self-signed CA certificate for communication with the GitHub server can be
//# provided using a config map key selector. If `runnerMountPath` is set, for
//# each runner pod ARC will:
//# - create a `github-server-tls-cert` volume containing the certificate
//# specified in `certificateFrom`
//# - mount that volume on path `runnerMountPath`/{certificate name}
//# - set NODE_EXTRA_CA_CERTS environment variable to that same path
//# - set RUNNER_UPDATE_CA_CERTS environment variable to "1" (as of version
//# 2.303.0 this will instruct the runner to reload certificates on the host)
//#
//# If any of the above had already been set by the user in the runner pod
//# template, ARC will observe those and not overwrite them.
//# Example configuration:
//
// githubServerTLS:
// certificateFrom:
// configMapKeyRef:
// name: config-map-name
// key: ca.crt
// runnerMountPath: /usr/local/share/ca-certificates/
//# Container mode is an object that provides out-of-box configuration
//# for dind and kubernetes mode. Template will be modified as documented under the
//# template object.
//#
//# If any customization is required for dind or kubernetes mode, containerMode should remain
//# empty, and configuration should be applied to the template.
// containerMode:
// type: "dind" ## type can be set to dind or kubernetes
// ## the following is required when containerMode.type=kubernetes
// kubernetesModeWorkVolumeClaim:
// accessModes: ["ReadWriteOnce"]
// # For local testing, use https://github.com/openebs/dynamic-localpv-provisioner/blob/develop/docs/quickstart.md to provide dynamic provision volume with storageClassName: openebs-hostpath
// storageClassName: "dynamic-blob-storage"
// resources:
// requests:
// storage: 1Gi
// kubernetesModeServiceAccount:
// annotations:
//# template is the PodSpec for each listener Pod
//# For reference: https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#PodSpec
// listenerTemplate:
// spec:
// containers:
// # Use this section to append additional configuration to the listener container.
// # If you change the name of the container, the configuration will not be applied to the listener,
// # and it will be treated as a side-car container.
// - name: listener
// securityContext:
// runAsUser: 1000
// # Use this section to add the configuration of a side-car container.
// # Comment it out or remove it if you don't need it.
// # Spec for this container will be applied as is without any modifications.
// - name: side-car
// image: example-sidecar
//# template is the PodSpec for each runner Pod
//# For reference: https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#PodSpec
template: {
//# template.spec will be modified if you change the container mode
//# with containerMode.type=dind, we will populate the template.spec with following pod spec
//# template:
//# spec:
//# initContainers:
//# - name: init-dind-externals
//# image: ghcr.io/actions/actions-runner:latest
//# command: ["cp", "-r", "-v", "/home/runner/externals/.", "/home/runner/tmpDir/"]
//# volumeMounts:
//# - name: dind-externals
//# mountPath: /home/runner/tmpDir
//# containers:
//# - name: runner
//# image: ghcr.io/actions/actions-runner:latest
//# command: ["/home/runner/run.sh"]
//# env:
//# - name: DOCKER_HOST
//# value: unix:///run/docker/docker.sock
//# volumeMounts:
//# - name: work
//# mountPath: /home/runner/_work
//# - name: dind-sock
//# mountPath: /run/docker
//# readOnly: true
//# - name: dind
//# image: docker:dind
//# args:
//# - dockerd
//# - --host=unix:///run/docker/docker.sock
//# - --group=$(DOCKER_GROUP_GID)
//# env:
//# - name: DOCKER_GROUP_GID
//# value: "123"
//# securityContext:
//# privileged: true
//# volumeMounts:
//# - name: work
//# mountPath: /home/runner/_work
//# - name: dind-sock
//# mountPath: /run/docker
//# - name: dind-externals
//# mountPath: /home/runner/externals
//# volumes:
//# - name: work
//# emptyDir: {}
//# - name: dind-sock
//# emptyDir: {}
//# - name: dind-externals
//# emptyDir: {}
//#####################################################################################################
//# with containerMode.type=kubernetes, we will populate the template.spec with following pod spec
//# template:
//# spec:
//# containers:
//# - name: runner
//# image: ghcr.io/actions/actions-runner:latest
//# command: ["/home/runner/run.sh"]
//# env:
//# - name: ACTIONS_RUNNER_CONTAINER_HOOKS
//# value: /home/runner/k8s/index.js
//# - name: ACTIONS_RUNNER_POD_NAME
//# valueFrom:
//# fieldRef:
//# fieldPath: metadata.name
//# - name: ACTIONS_RUNNER_REQUIRE_JOB_CONTAINER
//# value: "true"
//# volumeMounts:
//# - name: work
//# mountPath: /home/runner/_work
//# volumes:
//# - name: work
//# ephemeral:
//# volumeClaimTemplate:
//# spec:
//# accessModes: [ "ReadWriteOnce" ]
//# storageClassName: "local-path"
//# resources:
//# requests:
//# storage: 1Gi
spec: {
containers: [{
name: "runner"
image: "ghcr.io/actions/actions-runner:latest"
command: ["/home/runner/run.sh"]
}]
}
}
}

View File

@@ -0,0 +1,15 @@
package holos
#TargetNamespace: "arc-system"
#InputKeys: component: "arc-system"
#HelmChart & {
values: #Values & #DefaultSecurityContext
namespace: #TargetNamespace
chart: {
// Match the gha-base-name in the chart _helpers.tpl to avoid long full names.
release: "gha-rs-controller"
name: "oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set-controller"
version: "0.8.3"
}
}

View File

@@ -0,0 +1,127 @@
package holos
#Values: {
// Default values for gha-runner-scale-set-controller.
// This is a YAML-formatted file.
// Declare variables to be passed into your templates.
labels: {}
// leaderElection will be enabled when replicaCount>1,
// So, only one replica will in charge of reconciliation at a given time
// leaderElectionId will be set to {{ define gha-runner-scale-set-controller.fullname }}.
replicaCount: 1
image: {
repository: "ghcr.io/actions/gha-runner-scale-set-controller"
pullPolicy: "IfNotPresent"
// Overrides the image tag whose default is the chart appVersion.
tag: ""
}
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
env: null
//# Define environment variables for the controller pod
// - name: "ENV_VAR_NAME_1"
// value: "ENV_VAR_VALUE_1"
// - name: "ENV_VAR_NAME_2"
// valueFrom:
// secretKeyRef:
// key: ENV_VAR_NAME_2
// name: secret-name
// optional: true
serviceAccount: {
// Specifies whether a service account should be created for running the controller pod
create: true
// Annotations to add to the service account
annotations: {}
// The name of the service account to use.
// If not set and create is true, a name is generated using the fullname template
// You can not use the default service account for this.
name: ""
}
podAnnotations: {}
podLabels: {}
podSecurityContext: {}
// fsGroup: 2000
securityContext: {...}
// capabilities:
// drop:
// - ALL
// readOnlyRootFilesystem: true
// runAsNonRoot: true
// runAsUser: 1000
resources: {}
//# We usually recommend not to specify default resources and to leave this as a conscious
//# choice for the user. This also increases chances charts run on environments with little
//# resources, such as Minikube. If you do want to specify resources, uncomment the following
//# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
// limits:
// cpu: 100m
// memory: 128Mi
// requests:
// cpu: 100m
// memory: 128Mi
nodeSelector: {}
tolerations: []
affinity: {}
// Mount volumes in the container.
volumes: []
volumeMounts: []
// Leverage a PriorityClass to ensure your pods survive resource shortages
// ref: https://kubernetes.io/docs/concepts/configuration/pod-priority-preemption/
// PriorityClass: system-cluster-critical
priorityClassName: ""
//# If `metrics:` object is not provided, or commented out, the following flags
//# will be applied the controller-manager and listener pods with empty values:
//# `--metrics-addr`, `--listener-metrics-addr`, `--listener-metrics-endpoint`.
//# This will disable metrics.
//#
//# To enable metrics, uncomment the following lines.
// metrics:
// controllerManagerAddr: ":8080"
// listenerAddr: ":8080"
// listenerEndpoint: "/metrics"
flags: {
//# Log level can be set here with one of the following values: "debug", "info", "warn", "error".
//# Defaults to "debug".
logLevel: "debug"
//# Log format can be set with one of the following values: "text", "json"
//# Defaults to "text"
logFormat: "text"
//# Restricts the controller to only watch resources in the desired namespace.
//# Defaults to watch all namespaces when unset.
// watchSingleNamespace: ""
//# Defines how the controller should handle upgrades while having running jobs.
//#
//# The strategies available are:
//# - "immediate": (default) The controller will immediately apply the change causing the
//# recreation of the listener and ephemeral runner set. This can lead to an
//# overprovisioning of runners, if there are pending / running jobs. This should not
//# be a problem at a small scale, but it could lead to a significant increase of
//# resources if you have a lot of jobs running concurrently.
//#
//# - "eventual": The controller will remove the listener and ephemeral runner set
//# immediately, but will not recreate them (to apply changes) until all
//# pending / running jobs have completed.
//# This can lead to a longer time to apply the change but it will ensure
//# that you don't have any overprovisioning of runners.
updateStrategy: "immediate"
}
}

View File

@@ -18,9 +18,7 @@ let Cert = #PlatformCerts[SecretName]
#KubernetesObjects & {
apiObjects: {
ExternalSecret: httpbin: #ExternalSecret & {
_name: Cert.spec.secretName
}
ExternalSecret: "\(Cert.spec.secretName)": _
Deployment: httpbin: #Deployment & {
metadata: Metadata
spec: selector: matchLabels: MatchLabels

View File

@@ -63,7 +63,7 @@ let RedirectMetaName = {
// https-redirect
_APIObjects: {
Gateway: {
httpsRedirect: #Gateway & {
"\(RedirectMetaName.name)": #Gateway & {
metadata: RedirectMetaName
spec: selector: GatewayLabels
spec: servers: [{
@@ -79,7 +79,7 @@ _APIObjects: {
}
}
VirtualService: {
httpsRedirect: #VirtualService & {
"\(RedirectMetaName.name)": #VirtualService & {
metadata: RedirectMetaName
spec: hosts: ["*"]
spec: gateways: [RedirectMetaName.name]

View File

@@ -526,7 +526,7 @@ package holos
base: {
// For istioctl usage to disable istio config crds in base
enableIstioConfigCRDs: true
enableIstioConfigCRDs: *true | false
// If enabled, gateway-api types will be validated using the standard upstream validation logic.
// This is an alternative to deploying the standalone validation server the project provides.

View File

@@ -16,8 +16,8 @@ package holos
remotePilotAddress: ""
}
base: {
// Include the CRDs in the helm template output
enableCRDTemplates: true
// holos includes crd templates with the --include-crds helm flag.
enableCRDTemplates: false
// Validation webhook configuration url
// For example: https://$remotePilotAddress:15017/validate
validationURL: ""

View File

@@ -0,0 +1,6 @@
package holos
#DependsOn: Namespaces: name: "prod-secrets-namespaces"
#DependsOn: CRDS: name: "\(#InstancePrefix)-crds"
#InputKeys: component: "controller"
{} & #KustomizeBuild

View File

@@ -0,0 +1,21 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: postgres-operator
labels:
- includeTemplates: true
pairs:
app.kubernetes.io/name: pgo
# The version below should match the version on the PostgresCluster CRD
app.kubernetes.io/version: 5.5.1
postgres-operator.crunchydata.com/control-plane: postgres-operator
resources:
- ./rbac/cluster
- ./manager
images:
- name: postgres-operator
newName: registry.developers.crunchydata.com/crunchydata/postgres-operator
newTag: ubi8-5.5.1-0

View File

@@ -0,0 +1,5 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- manager.yaml

View File

@@ -0,0 +1,56 @@
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: pgo
labels:
postgres-operator.crunchydata.com/control-plane: postgres-operator
spec:
replicas: 1
strategy: { type: Recreate }
selector:
matchLabels:
postgres-operator.crunchydata.com/control-plane: postgres-operator
template:
metadata:
labels:
postgres-operator.crunchydata.com/control-plane: postgres-operator
spec:
containers:
- name: operator
image: postgres-operator
env:
- name: PGO_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: CRUNCHY_DEBUG
value: "true"
- name: RELATED_IMAGE_POSTGRES_15
value: "registry.developers.crunchydata.com/crunchydata/crunchy-postgres:ubi8-15.6-0"
- name: RELATED_IMAGE_POSTGRES_15_GIS_3.3
value: "registry.developers.crunchydata.com/crunchydata/crunchy-postgres-gis:ubi8-15.6-3.3-0"
- name: RELATED_IMAGE_POSTGRES_16
value: "registry.developers.crunchydata.com/crunchydata/crunchy-postgres:ubi8-16.2-0"
- name: RELATED_IMAGE_POSTGRES_16_GIS_3.3
value: "registry.developers.crunchydata.com/crunchydata/crunchy-postgres-gis:ubi8-16.2-3.3-0"
- name: RELATED_IMAGE_POSTGRES_16_GIS_3.4
value: "registry.developers.crunchydata.com/crunchydata/crunchy-postgres-gis:ubi8-16.2-3.4-0"
- name: RELATED_IMAGE_PGADMIN
value: "registry.developers.crunchydata.com/crunchydata/crunchy-pgadmin4:ubi8-4.30-22"
- name: RELATED_IMAGE_PGBACKREST
value: "registry.developers.crunchydata.com/crunchydata/crunchy-pgbackrest:ubi8-2.49-0"
- name: RELATED_IMAGE_PGBOUNCER
value: "registry.developers.crunchydata.com/crunchydata/crunchy-pgbouncer:ubi8-1.21-3"
- name: RELATED_IMAGE_PGEXPORTER
value: "registry.developers.crunchydata.com/crunchydata/crunchy-postgres-exporter:ubi8-0.15.0-3"
- name: RELATED_IMAGE_PGUPGRADE
value: "registry.developers.crunchydata.com/crunchydata/crunchy-upgrade:ubi8-5.5.1-0"
- name: RELATED_IMAGE_STANDALONE_PGADMIN
value: "registry.developers.crunchydata.com/crunchydata/crunchy-pgadmin4:ubi8-7.8-3"
securityContext:
allowPrivilegeEscalation: false
capabilities: { drop: [ALL] }
readOnlyRootFilesystem: true
runAsNonRoot: true
serviceAccountName: pgo

View File

@@ -0,0 +1,7 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- service_account.yaml
- role.yaml
- role_binding.yaml

View File

@@ -0,0 +1,146 @@
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: postgres-operator
rules:
- apiGroups:
- ''
resources:
- configmaps
- persistentvolumeclaims
- secrets
- services
verbs:
- create
- delete
- get
- list
- patch
- watch
- apiGroups:
- ''
resources:
- endpoints
verbs:
- create
- delete
- deletecollection
- get
- list
- patch
- watch
- apiGroups:
- ''
resources:
- endpoints/restricted
- pods/exec
verbs:
- create
- apiGroups:
- ''
resources:
- events
verbs:
- create
- patch
- apiGroups:
- ''
resources:
- pods
verbs:
- delete
- get
- list
- patch
- watch
- apiGroups:
- ''
resources:
- serviceaccounts
verbs:
- create
- get
- list
- patch
- watch
- apiGroups:
- apps
resources:
- deployments
- statefulsets
verbs:
- create
- delete
- get
- list
- patch
- watch
- apiGroups:
- batch
resources:
- cronjobs
- jobs
verbs:
- create
- delete
- get
- list
- patch
- watch
- apiGroups:
- policy
resources:
- poddisruptionbudgets
verbs:
- create
- delete
- get
- list
- patch
- watch
- apiGroups:
- postgres-operator.crunchydata.com
resources:
- pgadmins
- pgupgrades
verbs:
- get
- list
- watch
- apiGroups:
- postgres-operator.crunchydata.com
resources:
- pgadmins/finalizers
- pgupgrades/finalizers
- postgresclusters/finalizers
verbs:
- update
- apiGroups:
- postgres-operator.crunchydata.com
resources:
- pgadmins/status
- pgupgrades/status
- postgresclusters/status
verbs:
- patch
- apiGroups:
- postgres-operator.crunchydata.com
resources:
- postgresclusters
verbs:
- get
- list
- patch
- watch
- apiGroups:
- rbac.authorization.k8s.io
resources:
- rolebindings
- roles
verbs:
- create
- get
- list
- patch
- watch

View File

@@ -0,0 +1,14 @@
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: postgres-operator
labels:
postgres-operator.crunchydata.com/control-plane: postgres-operator
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: postgres-operator
subjects:
- kind: ServiceAccount
name: pgo

View File

@@ -0,0 +1,7 @@
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: pgo
labels:
postgres-operator.crunchydata.com/control-plane: postgres-operator

View File

@@ -0,0 +1,7 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- service_account.yaml
- role.yaml
- role_binding.yaml

View File

@@ -0,0 +1,146 @@
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: postgres-operator
rules:
- apiGroups:
- ''
resources:
- configmaps
- persistentvolumeclaims
- secrets
- services
verbs:
- create
- delete
- get
- list
- patch
- watch
- apiGroups:
- ''
resources:
- endpoints
verbs:
- create
- delete
- deletecollection
- get
- list
- patch
- watch
- apiGroups:
- ''
resources:
- endpoints/restricted
- pods/exec
verbs:
- create
- apiGroups:
- ''
resources:
- events
verbs:
- create
- patch
- apiGroups:
- ''
resources:
- pods
verbs:
- delete
- get
- list
- patch
- watch
- apiGroups:
- ''
resources:
- serviceaccounts
verbs:
- create
- get
- list
- patch
- watch
- apiGroups:
- apps
resources:
- deployments
- statefulsets
verbs:
- create
- delete
- get
- list
- patch
- watch
- apiGroups:
- batch
resources:
- cronjobs
- jobs
verbs:
- create
- delete
- get
- list
- patch
- watch
- apiGroups:
- policy
resources:
- poddisruptionbudgets
verbs:
- create
- delete
- get
- list
- patch
- watch
- apiGroups:
- postgres-operator.crunchydata.com
resources:
- pgadmins
- pgupgrades
verbs:
- get
- list
- watch
- apiGroups:
- postgres-operator.crunchydata.com
resources:
- pgadmins/finalizers
- pgupgrades/finalizers
- postgresclusters/finalizers
verbs:
- update
- apiGroups:
- postgres-operator.crunchydata.com
resources:
- pgadmins/status
- pgupgrades/status
- postgresclusters/status
verbs:
- patch
- apiGroups:
- postgres-operator.crunchydata.com
resources:
- postgresclusters
verbs:
- get
- list
- patch
- watch
- apiGroups:
- rbac.authorization.k8s.io
resources:
- rolebindings
- roles
verbs:
- create
- get
- list
- patch
- watch

View File

@@ -0,0 +1,14 @@
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: postgres-operator
labels:
postgres-operator.crunchydata.com/control-plane: postgres-operator
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: postgres-operator
subjects:
- kind: ServiceAccount
name: pgo

View File

@@ -0,0 +1,7 @@
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: pgo
labels:
postgres-operator.crunchydata.com/control-plane: postgres-operator

View File

@@ -0,0 +1,7 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- bases/postgres-operator.crunchydata.com_postgresclusters.yaml
- bases/postgres-operator.crunchydata.com_pgupgrades.yaml
- bases/postgres-operator.crunchydata.com_pgadmins.yaml

View File

@@ -0,0 +1,6 @@
package holos
// Refer to https://github.com/CrunchyData/postgres-operator-examples/tree/main/kustomize/install/crd
#InputKeys: component: "crds"
{} & #KustomizeBuild

View File

@@ -0,0 +1,4 @@
package holos
// Crunchy Postgres Operator
#InputKeys: project: "pgo"

View File

@@ -2,6 +2,15 @@ package holos
import "encoding/json"
#DependsOn: _ESO
#InputKeys: {
project: "secrets"
component: "eso-creds-refresher"
}
#TargetNamespace: #CredsRefresher.namespace
// output kubernetes api objects for holos
#KubernetesObjects & {
apiObjects: {
@@ -13,15 +22,6 @@ import "encoding/json"
}
}
#InputKeys: {
project: "secrets"
component: "eso-creds-refresher"
}
#TargetNamespace: #CredsRefresher.namespace
#DependsOn: Namespaces: name: #InstancePrefix + "-namespaces"
let NAME = #CredsRefresher.name
let AUD = "//iam.googleapis.com/projects/\(#InputKeys.gcpProjectNumber)/locations/global/workloadIdentityPools/holos/providers/k8s-\(#InputKeys.cluster)"
let MOUNT = "/var/run/service-account"

View File

@@ -1,10 +1,12 @@
package holos
// Components under this directory are part of this collection
#InputKeys: project: "iam"
#InputKeys: project: "secrets"
// Shared dependencies for all components in this collection.
#DependsOn: _Namespaces
// Common Dependencies
_Namespaces: Namespaces: name: "\(#StageName)-secrets-namespaces"
_ESO: ESO: name: "\(#InstancePrefix)-eso"
_ESOCreds: ESOCreds: name: "\(#InstancePrefix)-eso-creds-refresher"

View File

@@ -0,0 +1,34 @@
package holos
#DependsOn: _ESOCreds
#TargetNamespace: "default"
#InputKeys: {
project: "secrets"
component: "stores"
}
// #PlatformNamespaceObjects defines the api objects necessary for eso SecretStores in external clusters to access secrets in a given namespace in the provisioner cluster.
#PlatformNamespaceObjects: {
_ns: #PlatformNamespace
objects: [
#SecretStore & {
_namespace: _ns.name
},
]
}
#KubernetesObjects & {
apiObjects: {
for ns in #PlatformNamespaces {
for obj in (#PlatformNamespaceObjects & {_ns: ns}).objects {
let Kind = obj.kind
let NS = ns.name
let Name = obj.metadata.name
"\(Kind)": "\(NS)/\(Name)": obj
}
}
}
}

View File

@@ -9,7 +9,7 @@ package holos
component: "validate"
}
#DependsOn: Namespaces: name: #InstancePrefix + "-eso"
#DependsOn: _ESO
#KubernetesObjects & {
apiObjects: {

View File

@@ -48,7 +48,7 @@ package holos
// (required) Ceph pool into which the RBD image shall be created
// eg: pool: replicapool
pool: "k8s-dev"
pool: #Platform.clusters[#ClusterName].pool
// (optional) RBD image features, CSI creates image with image-format 2 CSI
// RBD currently supports `layering`, `journaling`, `exclusive-lock`,

View File

@@ -1,108 +0,0 @@
package holos
// Manage an Issuer for cockroachdb for zitadel.
// For the iam login service, zitadel connects to cockroach db using tls certs for authz.
// Upstream: "The recommended approach is to use cert-manager for certificate management. For details, refer to Deploy cert-manager for mTLS."
// Refer to https://www.cockroachlabs.com/docs/stable/secure-cockroachdb-kubernetes#deploy-cert-manager-for-mtls
#InputKeys: component: "crdb"
#KubernetesObjects & {
apiObjects: {
Issuer: {
// https://github.com/cockroachdb/helm-charts/blob/3dcf96726ebcfe3784afb526ddcf4095a1684aea/README.md?plain=1#L196-L201
crdb: #Issuer & {
_description: "Issues the self signed root ca cert for cockroach db"
metadata: name: #ComponentName
metadata: namespace: #TargetNamespace
spec: selfSigned: {}
}
"crdb-ca-issuer": #Issuer & {
_description: "Issues mtls certs for cockroach db"
metadata: name: "crdb-ca-issuer"
metadata: namespace: #TargetNamespace
spec: ca: secretName: "cockroach-ca"
}
}
Certificate: {
"crdb-ca-cert": #Certificate & {
_description: "Root CA cert for cockroach db"
metadata: name: "crdb-ca-cert"
metadata: namespace: #TargetNamespace
spec: {
commonName: "root"
isCA: true
issuerRef: group: "cert-manager.io"
issuerRef: kind: "Issuer"
issuerRef: name: "crdb"
privateKey: algorithm: "ECDSA"
privateKey: size: 256
secretName: "cockroach-ca"
subject: organizations: ["Cockroach"]
}
}
"crdb-node": #Certificate & {
metadata: name: "crdb-node"
metadata: namespace: #TargetNamespace
spec: {
commonName: "node"
dnsNames: [
"localhost",
"127.0.0.1",
"crdb-public",
"crdb-public.\(#TargetNamespace)",
"crdb-public.\(#TargetNamespace).svc.cluster.local",
"*.crdb",
"*.crdb.\(#TargetNamespace)",
"*.crdb.\(#TargetNamespace).svc.cluster.local",
]
duration: "876h"
issuerRef: group: "cert-manager.io"
issuerRef: kind: "Issuer"
issuerRef: name: "crdb-ca-issuer"
privateKey: algorithm: "RSA"
privateKey: size: 2048
renewBefore: "168h"
secretName: "cockroachdb-node"
subject: organizations: ["Cockroach"]
usages: ["digital signature", "key encipherment", "server auth", "client auth"]
}
}
"crdb-root-client": #Certificate & {
metadata: name: "crdb-root-client"
metadata: namespace: #TargetNamespace
spec: {
commonName: "root"
duration: "672h"
issuerRef: group: "cert-manager.io"
issuerRef: kind: "Issuer"
issuerRef: name: "crdb-ca-issuer"
privateKey: algorithm: "RSA"
privateKey: size: 2048
renewBefore: "48h"
secretName: "cockroachdb-root"
subject: organizations: ["Cockroach"]
usages: ["digital signature", "key encipherment", "client auth"]
}
}
}
Certificate: zitadel: #Certificate & {
metadata: name: "crdb-zitadel-client"
metadata: namespace: #TargetNamespace
spec: {
commonName: "zitadel"
issuerRef: {
group: "cert-manager.io"
kind: "Issuer"
name: "crdb-ca-issuer"
}
privateKey: algorithm: "RSA"
privateKey: size: 2048
renewBefore: "48h0m0s"
secretName: "cockroachdb-zitadel"
subject: organizations: ["Cockroach"]
usages: ["digital signature", "key encipherment", "client auth"]
}
}
}
}

View File

@@ -0,0 +1,101 @@
package holos
// Manage an Issuer for the database.
// Both cockroach and postgres handle tls database connections with cert manager
// PGO: https://github.com/CrunchyData/postgres-operator-examples/tree/main/kustomize/certmanager/certman
// CRDB: https://github.com/cockroachdb/helm-charts/blob/3dcf96726ebcfe3784afb526ddcf4095a1684aea/README.md?plain=1#L196-L201
// Refer to [Using Cert Manager to Deploy TLS for Postgres on Kubernetes](https://www.crunchydata.com/blog/using-cert-manager-to-deploy-tls-for-postgres-on-kubernetes)
#InputKeys: component: "postgres-certs"
let SelfSigned = "\(_DBName)-selfsigned"
let RootCA = "\(_DBName)-root-ca"
let Orgs = ["Database"]
#KubernetesObjects & {
apiObjects: {
// Put everything in the target namespace.
[_]: {
[Name=_]: {
metadata: name: Name
metadata: namespace: #TargetNamespace
}
}
Issuer: {
"\(SelfSigned)": #Issuer & {
_description: "Self signed issuer to issue ca certs"
metadata: name: SelfSigned
spec: selfSigned: {}
}
"\(RootCA)": #Issuer & {
_description: "Root signed intermediate ca to issue mtls database certs"
metadata: name: RootCA
spec: ca: secretName: RootCA
}
}
Certificate: {
"\(RootCA)": #Certificate & {
_description: "Root CA cert for database"
metadata: name: RootCA
spec: {
commonName: RootCA
isCA: true
issuerRef: group: "cert-manager.io"
issuerRef: kind: "Issuer"
issuerRef: name: SelfSigned
privateKey: algorithm: "ECDSA"
privateKey: size: 256
secretName: RootCA
subject: organizations: Orgs
}
}
"\(_DBName)-primary-tls": #DatabaseCert & {
// PGO managed name is "<cluster name>-cluster-cert" e.g. zitadel-cluster-cert
spec: {
commonName: "\(_DBName)-primary"
dnsNames: [
commonName,
"\(commonName).\(#TargetNamespace)",
"\(commonName).\(#TargetNamespace).svc",
"\(commonName).\(#TargetNamespace).svc.cluster.local",
"localhost",
"127.0.0.1",
]
usages: ["digital signature", "key encipherment"]
}
}
"\(_DBName)-repl-tls": #DatabaseCert & {
spec: {
commonName: "_crunchyrepl"
dnsNames: [commonName]
usages: ["digital signature", "key encipherment"]
}
}
"\(_DBName)-client-tls": #DatabaseCert & {
spec: {
commonName: "\(_DBName)-client"
dnsNames: [commonName]
usages: ["digital signature", "key encipherment"]
}
}
}
}
}
#DatabaseCert: #Certificate & {
metadata: name: string
metadata: namespace: #TargetNamespace
spec: {
duration: "2160h" // 90d
renewBefore: "360h" // 15d
issuerRef: group: "cert-manager.io"
issuerRef: kind: "Issuer"
issuerRef: name: RootCA
privateKey: algorithm: "ECDSA"
privateKey: size: 256
secretName: metadata.name
subject: organizations: Orgs
}
}

View File

@@ -0,0 +1,7 @@
# Database Certs
This component issues postgres certificates from the provisioner cluster using certmanager.
The purpose is to define customTLSSecret and customReplicationTLSSecret to provide certs that allow the standby to authenticate to the primary. For this type of standby, you must use custom TLS.
Refer to the PGO [Streaming Standby](https://access.crunchydata.com/documentation/postgres-operator/latest/tutorials/backups-disaster-recovery/disaster-recovery#streaming-standby) tutorial.

View File

@@ -2,6 +2,5 @@ package holos
#TargetNamespace: #InstancePrefix + "-zitadel"
#DB: {
Host: "crdb-public"
}
// _DBName is the database name used across multiple holos components in this project
_DBName: "zitadel"

View File

@@ -1,10 +0,0 @@
package holos
#TargetNamespace: #InstancePrefix + "-zitadel"
#DB: {
Host: "crdb-public"
}
// The canonical login domain for the entire platform. Zitadel will be active on a singlec cluster at a time, but always accessible from this hostname.
#ExternalDomain: "login.\(#Platform.org.domain)"

View File

@@ -1,34 +0,0 @@
package holos
#Values: {
// https://raw.githubusercontent.com/zitadel/zitadel-charts/main/examples/4-cockroach-secure/zitadel-values.yaml
zitadel: {
masterkeySecretName: "zitadel-masterkey"
// https://github.com/zitadel/zitadel-charts/blob/zitadel-7.4.0/charts/zitadel/templates/configmap.yaml#L13
configmapConfig: {
// NOTE: You can change the ExternalDomain, ExternalPort and ExternalSecure
// configuration options at any time. However, for ZITADEL to be able to
// pick up the changes, you need to rerun ZITADELs setup phase. Do so with
// kubectl delete job zitadel-setup, then re-apply the new config.
//
// https://zitadel.com/docs/self-hosting/manage/custom-domain
ExternalDomain: #ExternalDomain
ExternalPort: 443
ExternalSecure: true
TLS: Enabled: false
Database: Cockroach: {
Host: #DB.Host
User: SSL: Mode: "verify-full"
Admin: SSL: Mode: "verify-full"
}
}
// Managed by crdb component
dbSslCaCrtSecret: "cockroach-ca"
dbSslAdminCrtSecret: "cockroachdb-root"
// Managed by this component
dbSslUserCrtSecret: "cockroachdb-zitadel"
}
}

View File

@@ -1,55 +0,0 @@
package holos
import "encoding/yaml"
let Name = "zitadel"
#InputKeys: component: Name
// Upstream helm chart doesn't specify the namespace field for all resources.
#Kustomization: spec: targetNamespace: #TargetNamespace
#HelmChart & {
namespace: #TargetNamespace
chart: {
name: Name
version: "7.9.0"
repository: {
name: Name
url: "https://charts.zitadel.com"
}
}
values: #Values
apiObjects: {
ExternalSecret: masterkey: #ExternalSecret & {
_name: "zitadel-masterkey"
}
ExternalSecret: zitadel: #ExternalSecret & {
_name: "cockroachdb-zitadel"
}
VirtualService: zitadel: #VirtualService & {
metadata: name: Name
metadata: namespace: #TargetNamespace
spec: hosts: ["login.\(#Platform.org.domain)"]
spec: gateways: ["istio-ingress/default"]
spec: http: [{route: [{destination: host: Name}]}]
}
}
}
// TODO: Generalize this common pattern of injecting the istio sidecar into a Deployment
let Patch = [{op: "add", path: "/spec/template/metadata/labels/sidecar.istio.io~1inject", value: "true"}]
#Kustomize: {
patches: [
{
target: {
group: "apps"
version: "v1"
kind: "Deployment"
name: Name
}
patch: yaml.Marshal(Patch)
},
]
}

View File

@@ -12,6 +12,7 @@ let Privileged = {
// #PlatformNamespaces is the union of all namespaces across all cluster types. Namespaces are created in all clusters regardless of if they're
// used within the cluster or not. The is important for security and consistency with IAM, RBAC, and Secrets sync between clusters.
// Holos adopts the namespace sameness position of SIG Multicluster, refer to https://github.com/kubernetes/community/blob/dd4c8b704ef1c9c3bfd928c6fa9234276d61ad18/sig-multicluster/namespace-sameness-position-statement.md
#PlatformNamespaces: [
{name: "external-secrets"},
{name: "holos-system"},
@@ -22,4 +23,8 @@ let Privileged = {
{name: "cert-manager"},
{name: "argocd"},
{name: "prod-iam-zitadel"},
{name: "arc-system"},
{name: "arc-runner"},
// https://github.com/CrunchyData/postgres-operator-examples/blob/main/kustomize/install/namespace/namespace.yaml
{name: "postgres-operator"},
]

View File

@@ -15,6 +15,7 @@ import (
gw "networking.istio.io/gateway/v1beta1"
vs "networking.istio.io/virtualservice/v1beta1"
kc "sigs.k8s.io/kustomize/api/types"
pg "postgres-operator.crunchydata.com/postgrescluster/v1beta1"
"encoding/yaml"
)
@@ -81,6 +82,7 @@ _apiVersion: "holos.run/v1alpha1"
}
#NamespaceObject: #ClusterObject & {
metadata: name: string
metadata: namespace: string
...
}
@@ -96,19 +98,20 @@ _apiVersion: "holos.run/v1alpha1"
#ClusterRoleBinding: #ClusterObject & rbacv1.#ClusterRoleBinding
#ClusterIssuer: #ClusterObject & ci.#ClusterIssuer & {...}
#Issuer: #NamespaceObject & is.#Issuer
#Role: #NamespaceObject & rbacv1.#Role
#RoleBinding: #NamespaceObject & rbacv1.#RoleBinding
#ConfigMap: #NamespaceObject & corev1.#ConfigMap
#ServiceAccount: #NamespaceObject & corev1.#ServiceAccount
#Pod: #NamespaceObject & corev1.#Pod
#Service: #NamespaceObject & corev1.#Service
#Job: #NamespaceObject & batchv1.#Job
#CronJob: #NamespaceObject & batchv1.#CronJob
#Deployment: #NamespaceObject & appsv1.#Deployment
#Gateway: #NamespaceObject & gw.#Gateway
#VirtualService: #NamespaceObject & vs.#VirtualService
#Certificate: #NamespaceObject & crt.#Certificate
#Issuer: #NamespaceObject & is.#Issuer
#Role: #NamespaceObject & rbacv1.#Role
#RoleBinding: #NamespaceObject & rbacv1.#RoleBinding
#ConfigMap: #NamespaceObject & corev1.#ConfigMap
#ServiceAccount: #NamespaceObject & corev1.#ServiceAccount
#Pod: #NamespaceObject & corev1.#Pod
#Service: #NamespaceObject & corev1.#Service
#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
// #HTTP01Cert defines a http01 certificate.
#HTTP01Cert: {
@@ -156,8 +159,8 @@ _apiVersion: "holos.run/v1alpha1"
// #DependsOn stores all of the dependencies between components. It's a struct to support merging across levels in the tree.
#DependsOn: {
[NAME=_]: {
name: string
[Name=_]: {
name: string | *"\(#InstancePrefix)-\(Name)"
}
...
}
@@ -227,18 +230,48 @@ _apiVersion: "holos.run/v1alpha1"
provisionerURL: string @tag(provisionerURL, type=string)
}
// #ClusterSpec is the specification of a holos platform cluster member.
#ClusterSpec: {
// name is the cluster name.
name: string
// pool is the optional ceph pool of the cluster.
pool?: string
// region is the geographic region of the cluster.
region?: string
// primary is true if name matches the primaryCluster name
primary: bool
}
// #Platform defines the primary lookup table for the platform. Lookup keys should be limited to those defined in #KeyTags.
#Platform: {
// org holds user defined values scoped organization wide. A platform has one and only one organization.
org: {
name: string
// e.g. "example"
name: string
// e.g. "example.com"
domain: string
contact: email: string
// e.g. "Example"
displayName: string
// e.g. "platform@example.com"
contact: email: string
// e.g. "platform@example.com"
cloudflare: email: string
// e.g. "example"
github: orgs: primary: name: string
}
clusters: [ID=_]: {
name: string & ID
region?: string
// Only one cluster may be primary at a time. All others are standby.
// Refer to [repo based standby](https://access.crunchydata.com/documentation/postgres-operator/latest/tutorials/backups-disaster-recovery/disaster-recovery#repo-based-standby)
primaryCluster: {
name: string
}
clusters: [Name=_]: #ClusterSpec & {
name: string & Name
if Name == primaryCluster.name {
primary: true
}
if Name != primaryCluster.name {
primary: false
}
}
stages: [ID=_]: {
name: string & ID
@@ -252,6 +285,15 @@ _apiVersion: "holos.run/v1alpha1"
}
}
// #Backups defines backup configuration.
// TODO: Consider the best place for this, possibly as part of the site platform config. This represents the primary location for backups.
#Backups: {
s3: {
region: string
endpoint: string | *"s3.dualstack.\(region).amazonaws.com"
}
}
// #APIObjects is the output type for api objects produced by cue. A map is used to aid debugging and clarity.
#APIObjects: {
// apiObjects holds each the api objects produced by cue.
@@ -261,6 +303,9 @@ _apiVersion: "holos.run/v1alpha1"
kind: Kind
}
}
ExternalSecret?: [Name=_]: #ExternalSecret & {_name: Name}
VirtualService?: [Name=_]: #VirtualService & {metadata: name: Name}
Issuer?: [Name=_]: #Issuer & {metadata: name: Name}
}
// apiObjectMap holds the marshalled representation of apiObjects
@@ -312,9 +357,10 @@ _apiVersion: "holos.run/v1alpha1"
#Chart: {
name: string
version: string
release: string | *name
repository: {
name: string
url: string
name?: string
url?: string
}
}
@@ -349,6 +395,21 @@ _apiVersion: "holos.run/v1alpha1"
kustomizeFiles: #KustomizeFiles.Files
}
// #KustomizeBuild is a holos component that uses plain yaml files as the source of api objects for a holos component.
// Intended for upstream components like the CrunchyData Postgres Operator. The holos cli is expected to execute kustomize build on the component directory to produce the rendered output.
#KustomizeBuild: {
#OutputTypeMeta
#APIObjects
kind: "KustomizeBuild"
metadata: name: #InstanceName
// ksObjects holds the flux Kustomization objects for gitops.
ksObjects: [...#Kustomization] | *[#Kustomization]
// ksContent is the yaml representation of kustomization.
ksContent: yaml.MarshalStream(ksObjects)
// namespace defines the value passed to the helm --namespace flag
namespace: #TargetNamespace
}
// #PlatformSpec is the output schema of a platform specification.
#PlatformSpec: {
#OutputTypeMeta
@@ -389,6 +450,24 @@ _apiVersion: "holos.run/v1alpha1"
...
}
// #DefaultSecurityContext is the holos default security context to comply with the restricted namespace policy.
// Refer to https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted
#DefaultSecurityContext: {
securityContext: {
allowPrivilegeEscalation: false
runAsNonRoot: true
capabilities: drop: ["ALL"]
seccompProfile: type: "RuntimeDefault"
}
...
}
// Certificate name should always match the secret name.
#Certificate: {
metadata: name: _
spec: secretName: metadata.name
}
// By default, render kind: Skipped so holos knows to skip over intermediate cue files.
// This enables the use of holos render ./foo/bar/baz/... when bar contains intermediary constraints which are not complete components.
// Holos skips over these intermediary cue instances.

View File

@@ -133,9 +133,12 @@ This section configured:
1. Provisioner Cluster to provide secrets to workload clusters.
2. IAM service account `eso-creds-refresher` to identify the credential refresher job.
3. Workload identity pool to authenticate the `eso-creds-refresher` Kubernetes service account in an external cluster.
4. IAM policy to allow `eso-creds-refresher` to authenticate to the Provisioner Cluster.
5. RoleBinding to allow `eso-creds-refresher` to create kubernetes service account tokens representing the credentials for use by SecretStore resources in workload clusters.
3. Workload identity pool to authenticate the `system:serviceaccount:holos-system:eso-creds-refresher` Kubernetes service account in all clusters that share the workload identity pool.
4. IAM policy to allow the `eso-creds-refresher` IAM service account to authenticate to the Provisioner Cluster.
5. RoleBinding to allow the `eso-creds-refresher` IAM service account to create kubernetes service account tokens representing the credentials for use by SecretStore resources in workload clusters.
> [!NOTE]
> Any cluster in the workload identity pool can impersonate the eso-creds-refresher IAM service account.
## Cluster Setup
@@ -150,6 +153,12 @@ HOLOS_CLUSTER_NAME=west1
ISSUER_URL="https://example.com/clusters/${HOLOS_CLUSTER_NAME}"
```
Alternatively:
```shell
ISSUER_URL="$(kubectl get --raw='/.well-known/openid-configuration' | jq -r .issuer)"
```
```shell
gcloud iam workload-identity-pools providers create-oidc \
k8s-$HOLOS_CLUSTER_NAME \

View File

@@ -1,6 +1,7 @@
package secret
import (
"bytes"
"fmt"
"github.com/holos-run/holos/pkg/cli/command"
"github.com/holos-run/holos/pkg/holos"
@@ -29,9 +30,10 @@ func NewCreateCmd(hc *holos.Config) *cobra.Command {
cfg.dryRun = flagSet.Bool("dry-run", false, "dry run")
cfg.appendHash = flagSet.Bool("append-hash", true, "append hash to kubernetes secret name")
cfg.dataStdin = flagSet.Bool("data-stdin", false, "read data field as json from stdin if")
cfg.trimTrailingNewlines = flagSet.Bool("trim-trailing-newlines", true, "trim trailing newlines if true")
cmd.Flags().SortFlags = false
cmd.Flags().AddGoFlagSet(flagSet)
cmd.Flags().AddFlagSet(flagSet)
cmd.RunE = makeCreateRunFunc(hc, cfg)
return cmd
@@ -80,7 +82,7 @@ func makeCreateRunFunc(hc *holos.Config, cfg *config) command.RunFunc {
}
for _, file := range cfg.files {
if err := filepath.WalkDir(file, makeWalkFunc(secret.Data, file)); err != nil {
if err := filepath.WalkDir(file, makeWalkFunc(secret.Data, file, *cfg.trimTrailingNewlines)); err != nil {
return wrapper.Wrap(err)
}
}
@@ -125,7 +127,7 @@ func makeCreateRunFunc(hc *holos.Config, cfg *config) command.RunFunc {
}
}
func makeWalkFunc(data secretData, root string) fs.WalkDirFunc {
func makeWalkFunc(data secretData, root string, trimNewlines bool) fs.WalkDirFunc {
return func(path string, d os.DirEntry, err error) error {
if err != nil {
return err
@@ -143,6 +145,9 @@ func makeWalkFunc(data secretData, root string) fs.WalkDirFunc {
if data[key], err = os.ReadFile(path); err != nil {
return wrapper.Wrap(err)
}
if trimNewlines {
data[key] = bytes.TrimRight(data[key], "\r\n")
}
}
return nil

View File

@@ -31,7 +31,7 @@ func NewGetCmd(hc *holos.Config) *cobra.Command {
cfg.extractTo = flagSet.String("extract-to", ".", "extract to directory")
cmd.Flags().SortFlags = false
cmd.Flags().AddGoFlagSet(flagSet)
cmd.Flags().AddFlagSet(flagSet)
cmd.RunE = makeGetRunFunc(hc, cfg)
return cmd
}

View File

@@ -1,8 +1,8 @@
package secret
import (
"flag"
"github.com/holos-run/holos/pkg/holos"
"github.com/spf13/pflag"
)
const NameLabel = "holos.run/secret.name"
@@ -12,21 +12,22 @@ const ClusterLabel = "holos.run/cluster.name"
type secretData map[string][]byte
type config struct {
files holos.StringSlice
printFile *string
extract *bool
dryRun *bool
appendHash *bool
dataStdin *bool
cluster *string
namespace *string
extractTo *string
files holos.StringSlice
printFile *string
extract *bool
dryRun *bool
appendHash *bool
dataStdin *bool
trimTrailingNewlines *bool
cluster *string
namespace *string
extractTo *string
}
func newConfig() (*config, *flag.FlagSet) {
func newConfig() (*config, *pflag.FlagSet) {
cfg := &config{}
flagSet := flag.NewFlagSet("", flag.ContinueOnError)
cfg.namespace = flagSet.String("namespace", holos.DefaultProvisionerNamespace, "namespace in the provisioner cluster")
flagSet := pflag.NewFlagSet("", pflag.ContinueOnError)
cfg.namespace = flagSet.StringP("namespace", "n", holos.DefaultProvisionerNamespace, "namespace in the provisioner cluster")
cfg.cluster = flagSet.String("cluster-name", "", "cluster name selector")
return cfg, flagSet
}

View File

@@ -1,5 +1,5 @@
# Create the secret
holos create secret directory --from-file=$WORK/fixture --dry-run
holos create secret directory --trim-trailing-newlines=false --from-file=$WORK/fixture --dry-run
# Want no warnings.
! stderr 'WRN'

View File

@@ -1,5 +1,5 @@
# Create the secret
holos create secret directory --from-file=$WORK/want
holos create secret directory --trim-trailing-newlines=false --from-file=$WORK/want
stderr 'created: directory-..........'
stderr 'secret=directory-..........'
stderr 'name=directory'

View File

@@ -0,0 +1,17 @@
# Create a secret from files with trailing newlines
holos create secret smtp --from-file=$WORK/smtp
# Get the secret back expecting no trailing newlines
mkdir have
holos get secret smtp
stdout '"username": "holos.run@gmail.com"'
stdout '"password": "secret"'
-- smtp/username --
holos.run@gmail.com
-- smtp/password --
secret
-- smtp/host --
smtp.gmail.com
-- smtp/port --
587

View File

@@ -1,5 +1,5 @@
# Create the secret
holos create secret directory --from-file=$WORK/want
holos create secret directory --trim-trailing-newlines=false --from-file=$WORK/want
# Get the secret back
mkdir have

View File

@@ -13,6 +13,11 @@ func (i *StringSlice) String() string {
return fmt.Sprint(*i)
}
// Type implements the pflag.Value interface and describes the type.
func (i *StringSlice) Type() string {
return "strings"
}
// Set implements the flag.Value interface.
func (i *StringSlice) Set(value string) error {
for _, str := range strings.Split(value, ",") {

View File

@@ -33,6 +33,8 @@ const (
Skip = "Skip"
// ChartDir is the chart cache directory name.
ChartDir = "vendor"
// KustomizeBuild is the value of the kind field of cue output indicating holos should process the component using kustomize build to render output.
KustomizeBuild = "KustomizeBuild"
)
// An Option configures a Builder
@@ -106,6 +108,7 @@ type Repository struct {
type Chart struct {
Name string `json:"name"`
Version string `json:"version"`
Release string `json:"release"`
Repository Repository `json:"repository"`
}
@@ -340,6 +343,20 @@ func (b *Builder) Run(ctx context.Context) (results []*Result, err error) {
if err := result.kustomize(ctx); err != nil {
return nil, wrapper.Wrap(fmt.Errorf("could not kustomize: %w", err))
}
case KustomizeBuild:
// CUE directly provides the kubernetes api objects in result.Content
if err := value.Decode(&result); err != nil {
return nil, wrapper.Wrap(fmt.Errorf("could not decode: %w", err))
}
// Run kustomize.
kOut, err := util.RunCmd(ctx, "kubectl", "kustomize", instance.Dir)
if err != nil {
log.ErrorContext(ctx, kOut.Stderr.String())
return nil, wrapper.Wrap(err)
}
// Replace the accumulated output
result.accumulatedOutput = kOut.Stdout.String()
result.addOverlayObjects(log)
default:
return nil, wrapper.Wrap(fmt.Errorf("build kind not implemented: %v", kind))
}
@@ -387,21 +404,26 @@ func runHelm(ctx context.Context, hc *HelmChart, r *Result, path holos.PathCompo
return nil
}
cachedChartPath := filepath.Join(string(path), ChartDir, hc.Chart.Name)
cachedChartPath := filepath.Join(string(path), ChartDir, filepath.Base(hc.Chart.Name))
if isNotExist(cachedChartPath) {
// Add repositories
repo := hc.Chart.Repository
out, err := util.RunCmd(ctx, "helm", "repo", "add", repo.Name, repo.URL)
if err != nil {
log.ErrorContext(ctx, "could not run helm", "stderr", out.Stderr.String(), "stdout", out.Stdout.String())
return wrapper.Wrap(fmt.Errorf("could not run helm repo add: %w", err))
}
// Update repository
out, err = util.RunCmd(ctx, "helm", "repo", "update", repo.Name)
if err != nil {
log.ErrorContext(ctx, "could not run helm", "stderr", out.Stderr.String(), "stdout", out.Stdout.String())
return wrapper.Wrap(fmt.Errorf("could not run helm repo update: %w", err))
if repo.URL != "" {
out, err := util.RunCmd(ctx, "helm", "repo", "add", repo.Name, repo.URL)
if err != nil {
log.ErrorContext(ctx, "could not run helm", "stderr", out.Stderr.String(), "stdout", out.Stdout.String())
return wrapper.Wrap(fmt.Errorf("could not run helm repo add: %w", err))
}
// Update repository
out, err = util.RunCmd(ctx, "helm", "repo", "update", repo.Name)
if err != nil {
log.ErrorContext(ctx, "could not run helm", "stderr", out.Stderr.String(), "stdout", out.Stdout.String())
return wrapper.Wrap(fmt.Errorf("could not run helm repo update: %w", err))
}
} else {
log.DebugContext(ctx, "no chart repository url proceeding assuming oci chart")
}
// Cache the chart
if err := cacheChart(ctx, path, ChartDir, hc.Chart); err != nil {
return fmt.Errorf("could not cache chart: %w", err)
@@ -423,7 +445,7 @@ func runHelm(ctx context.Context, hc *HelmChart, r *Result, path holos.PathCompo
// Run charts
chart := hc.Chart
helmOut, err := util.RunCmd(ctx, "helm", "template", "--values", valuesPath, "--namespace", hc.Namespace, "--kubeconfig", "/dev/null", "--version", chart.Version, chart.Name, cachedChartPath)
helmOut, err := util.RunCmd(ctx, "helm", "template", "--include-crds", "--values", valuesPath, "--namespace", hc.Namespace, "--kubeconfig", "/dev/null", "--version", chart.Version, chart.Release, cachedChartPath)
if err != nil {
stderr := helmOut.Stderr.String()
lines := strings.Split(stderr, "\n")
@@ -465,7 +487,10 @@ func cacheChart(ctx context.Context, path holos.PathComponent, chartDir string,
}
defer remove(ctx, cacheTemp)
chartName := fmt.Sprintf("%s/%s", chart.Repository.Name, chart.Name)
chartName := chart.Name
if chart.Repository.Name != "" {
chartName = fmt.Sprintf("%s/%s", chart.Repository.Name, chart.Name)
}
helmOut, err := util.RunCmd(ctx, "helm", "pull", "--destination", cacheTemp, "--untar=true", "--version", chart.Version, chartName)
if err != nil {
return wrapper.Wrap(fmt.Errorf("could not run helm pull: %w", err))

View File

@@ -1 +1 @@
53
56

View File

@@ -1 +1 @@
2
0