diff --git a/1password/index.ts b/1password/index.ts deleted file mode 100644 index 475bae2..0000000 --- a/1password/index.ts +++ /dev/null @@ -1,48 +0,0 @@ -import * as fs from "fs"; -import { Construct } from "constructs"; -import { Manifest } from "@cdktf/provider-kubernetes/lib/manifest"; -import { KubernetesProvider } from "@cdktf/provider-kubernetes/lib/provider"; - -type OnePasswordSecret = { - id?: string; - namespace?: string; - name: string; - itemPath: string; -}; - -type OnePasswordOptions = { - provider: KubernetesProvider; - namespace: string; -}; - -export class OnePassword extends Construct { - constructor(scope: Construct, id: string, options: OnePasswordOptions) { - super(scope, id); - - const secrets: OnePasswordSecret[] = JSON.parse( - fs.readFileSync("1password/secrets.json", { - encoding: "utf8", - }), - ); - - secrets.forEach((secret) => { - new Manifest(this, secret.id ?? secret.name, { - provider: options.provider, - manifest: { - apiVersion: "onepassword.com/v1", - kind: "OnePasswordItem", - metadata: { - name: secret.name, - namespace: secret.namespace ?? options.namespace, - annotations: { - "operator.1password.io/auto-restart": "true", - }, - }, - spec: { - itemPath: secret.itemPath, - }, - }, - }); - }); - } -} diff --git a/1password/secrets.json b/1password/secrets.json deleted file mode 100644 index 00f63f7..0000000 --- a/1password/secrets.json +++ /dev/null @@ -1,60 +0,0 @@ -[ - { - "name": "gitea-admin", - "itemPath": "vaults/Lab/items/gitea-admin" - }, - { - "name": "pihole-admin", - "itemPath": "vaults/Lab/items/pihole" - }, - { - "name": "postgres-password", - "itemPath": "vaults/Lab/items/Postgres" - }, - { - "name": "runner-secret", - "itemPath": "vaults/Lab/items/Gitea" - }, - { - "name": "cloudflare-token", - "itemPath": "vaults/Lab/items/cloudflare" - }, - { - "id": "cloudflare-token-longhorn", - "name": "cloudflare-token", - "itemPath": "vaults/Lab/items/cloudflare", - "namespace": "longhorn-system" - }, - { - "name": "valkey", - "itemPath": "vaults/Lab/items/valkey" - }, - { - "name": "gitea-oauth", - "itemPath": "vaults/Lab/items/gitea-oauth" - }, - { - "name": "gitea-elasticsearch", - "itemPath": "vaults/Lab/items/gitea-elasticsearch" - }, - { - "name": "smtp-token", - "itemPath": "vaults/Lab/items/smtp-token" - }, - { - "name": "longhorn-encryption", - "itemPath": "vaults/Lab/items/longhorn-encryption" - }, - { - "name": "authentik-secret-key", - "itemPath": "vaults/Lab/items/authentik-secret-key" - }, - { - "name": "curseforge", - "itemPath": "vaults/Lab/items/curseforge" - }, - { - "name": "devpi-secret", - "itemPath": "vaults/Lab/items/devpi-secret" - } -] diff --git a/authentik/index.ts b/authentik/index.ts deleted file mode 100644 index 7abf129..0000000 --- a/authentik/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -import * as fs from "fs"; -import { HelmProvider } from "@cdktf/provider-helm/lib/provider"; -import { Release } from "@cdktf/provider-helm/lib/release"; -import { Construct } from "constructs"; - -type AuthentikServerOptions = { - provider: HelmProvider; - name: string; - namespace: string; -}; - -export class AuthentikServer extends Construct { - constructor(scope: Construct, id: string, options: AuthentikServerOptions) { - super(scope, id); - - new Release(this, id, { - ...options, - repository: "https://charts.goauthentik.io", - chart: "authentik", - createNamespace: true, - values: [ - fs.readFileSync("helm/values/authentik.values.yaml", { - encoding: "utf8", - }), - ], - }); - } -} diff --git a/barman.yaml b/barman.yaml new file mode 100644 index 0000000..95ed059 --- /dev/null +++ b/barman.yaml @@ -0,0 +1,1085 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + name: objectstores.barmancloud.cnpg.io +spec: + group: barmancloud.cnpg.io + names: + kind: ObjectStore + listKind: ObjectStoreList + plural: objectstores + singular: objectstore + scope: Namespaced + versions: + - name: v1 + schema: + openAPIV3Schema: + description: ObjectStore is the Schema for the objectstores API. + properties: + apiVersion: + description: |- + 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 + type: string + kind: + description: |- + 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 + type: string + metadata: + type: object + spec: + description: |- + Specification of the desired behavior of the ObjectStore. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status + properties: + configuration: + description: The configuration for the barman-cloud tool suite + properties: + azureCredentials: + description: The credentials to use to upload data to Azure Blob + Storage + properties: + connectionString: + description: The connection string to be used + properties: + key: + description: The key to select + type: string + name: + description: Name of the referent. + type: string + required: + - key + - name + type: object + inheritFromAzureAD: + description: Use the Azure AD based authentication without + providing explicitly the keys. + type: boolean + storageAccount: + description: The storage account where to upload data + properties: + key: + description: The key to select + type: string + name: + description: Name of the referent. + type: string + required: + - key + - name + type: object + storageKey: + description: |- + The storage account key to be used in conjunction + with the storage account name + properties: + key: + description: The key to select + type: string + name: + description: Name of the referent. + type: string + required: + - key + - name + type: object + storageSasToken: + description: |- + A shared-access-signature to be used in conjunction with + the storage account name + properties: + key: + description: The key to select + type: string + name: + description: Name of the referent. + type: string + required: + - key + - name + type: object + type: object + data: + description: |- + The configuration to be used to backup the data files + When not defined, base backups files will be stored uncompressed and may + be unencrypted in the object store, according to the bucket default + policy. + properties: + additionalCommandArgs: + description: |- + AdditionalCommandArgs represents additional arguments that can be appended + to the 'barman-cloud-backup' command-line invocation. These arguments + provide flexibility to customize the backup process further according to + specific requirements or configurations. + + Example: + In a scenario where specialized backup options are required, such as setting + a specific timeout or defining custom behavior, users can use this field + to specify additional command arguments. + + Note: + It's essential to ensure that the provided arguments are valid and supported + by the 'barman-cloud-backup' command, to avoid potential errors or unintended + behavior during execution. + items: + type: string + type: array + compression: + description: |- + Compress a backup file (a tar file per tablespace) while streaming it + to the object store. Available options are empty string (no + compression, default), `gzip`, `bzip2`, and `snappy`. + enum: + - bzip2 + - gzip + - snappy + type: string + encryption: + description: |- + Whenever to force the encryption of files (if the bucket is + not already configured for that). + Allowed options are empty string (use the bucket policy, default), + `AES256` and `aws:kms` + enum: + - AES256 + - aws:kms + type: string + immediateCheckpoint: + description: |- + Control whether the I/O workload for the backup initial checkpoint will + be limited, according to the `checkpoint_completion_target` setting on + the PostgreSQL server. If set to true, an immediate checkpoint will be + used, meaning PostgreSQL will complete the checkpoint as soon as + possible. `false` by default. + type: boolean + jobs: + description: |- + The number of parallel jobs to be used to upload the backup, defaults + to 2 + format: int32 + minimum: 1 + type: integer + type: object + destinationPath: + description: |- + The path where to store the backup (i.e. s3://bucket/path/to/folder) + this path, with different destination folders, will be used for WALs + and for data + minLength: 1 + type: string + endpointCA: + description: |- + EndpointCA store the CA bundle of the barman endpoint. + Useful when using self-signed certificates to avoid + errors with certificate issuer and barman-cloud-wal-archive + properties: + key: + description: The key to select + type: string + name: + description: Name of the referent. + type: string + required: + - key + - name + type: object + endpointURL: + description: |- + Endpoint to be used to upload data to the cloud, + overriding the automatic endpoint discovery + type: string + googleCredentials: + description: The credentials to use to upload data to Google Cloud + Storage + properties: + applicationCredentials: + description: The secret containing the Google Cloud Storage + JSON file with the credentials + properties: + key: + description: The key to select + type: string + name: + description: Name of the referent. + type: string + required: + - key + - name + type: object + gkeEnvironment: + description: |- + If set to true, will presume that it's running inside a GKE environment, + default to false. + type: boolean + type: object + historyTags: + additionalProperties: + type: string + description: |- + HistoryTags is a list of key value pairs that will be passed to the + Barman --history-tags option. + type: object + s3Credentials: + description: The credentials to use to upload data to S3 + properties: + accessKeyId: + description: The reference to the access key id + properties: + key: + description: The key to select + type: string + name: + description: Name of the referent. + type: string + required: + - key + - name + type: object + inheritFromIAMRole: + description: Use the role based authentication without providing + explicitly the keys. + type: boolean + region: + description: The reference to the secret containing the region + name + properties: + key: + description: The key to select + type: string + name: + description: Name of the referent. + type: string + required: + - key + - name + type: object + secretAccessKey: + description: The reference to the secret access key + properties: + key: + description: The key to select + type: string + name: + description: Name of the referent. + type: string + required: + - key + - name + type: object + sessionToken: + description: The references to the session key + properties: + key: + description: The key to select + type: string + name: + description: Name of the referent. + type: string + required: + - key + - name + type: object + type: object + serverName: + description: |- + The server name on S3, the cluster name is used if this + parameter is omitted + type: string + tags: + additionalProperties: + type: string + description: |- + Tags is a list of key value pairs that will be passed to the + Barman --tags option. + type: object + wal: + description: |- + The configuration for the backup of the WAL stream. + When not defined, WAL files will be stored uncompressed and may be + unencrypted in the object store, according to the bucket default policy. + properties: + archiveAdditionalCommandArgs: + description: |- + Additional arguments that can be appended to the 'barman-cloud-wal-archive' + command-line invocation. These arguments provide flexibility to customize + the WAL archive process further, according to specific requirements or configurations. + + Example: + In a scenario where specialized backup options are required, such as setting + a specific timeout or defining custom behavior, users can use this field + to specify additional command arguments. + + Note: + It's essential to ensure that the provided arguments are valid and supported + by the 'barman-cloud-wal-archive' command, to avoid potential errors or unintended + behavior during execution. + items: + type: string + type: array + compression: + description: |- + Compress a WAL file before sending it to the object store. Available + options are empty string (no compression, default), `gzip`, `bzip2`, + `lz4`, `snappy`, `xz`, and `zstd`. + enum: + - bzip2 + - gzip + - lz4 + - snappy + - xz + - zstd + type: string + encryption: + description: |- + Whenever to force the encryption of files (if the bucket is + not already configured for that). + Allowed options are empty string (use the bucket policy, default), + `AES256` and `aws:kms` + enum: + - AES256 + - aws:kms + type: string + maxParallel: + description: |- + Number of WAL files to be either archived in parallel (when the + PostgreSQL instance is archiving to a backup object store) or + restored in parallel (when a PostgreSQL standby is fetching WAL + files from a recovery object store). If not specified, WAL files + will be processed one at a time. It accepts a positive integer as a + value - with 1 being the minimum accepted value. + minimum: 1 + type: integer + restoreAdditionalCommandArgs: + description: |- + Additional arguments that can be appended to the 'barman-cloud-wal-restore' + command-line invocation. These arguments provide flexibility to customize + the WAL restore process further, according to specific requirements or configurations. + + Example: + In a scenario where specialized backup options are required, such as setting + a specific timeout or defining custom behavior, users can use this field + to specify additional command arguments. + + Note: + It's essential to ensure that the provided arguments are valid and supported + by the 'barman-cloud-wal-restore' command, to avoid potential errors or unintended + behavior during execution. + items: + type: string + type: array + type: object + required: + - destinationPath + type: object + x-kubernetes-validations: + - fieldPath: .serverName + message: use the 'serverName' plugin parameter in the Cluster resource + reason: FieldValueForbidden + rule: '!has(self.serverName)' + instanceSidecarConfiguration: + description: The configuration for the sidecar that runs in the instance + pods + properties: + additionalContainerArgs: + description: |- + AdditionalContainerArgs is an optional list of command-line arguments + to be passed to the sidecar container when it starts. + The provided arguments are appended to the container’s default arguments. + items: + type: string + type: array + x-kubernetes-validations: + - message: do not set --log-level in additionalContainerArgs; + use spec.instanceSidecarConfiguration.logLevel + reason: FieldValueForbidden + rule: '!self.exists(a, a.startsWith(''--log-level''))' + env: + description: The environment to be explicitly passed to the sidecar + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the + specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: |- + The key within the env file. An invalid key will prevent the pod from starting. + The keys defined within a source may consist of any printable ASCII characters except '='. + During Alpha stage of the EnvFiles feature gate, the key size is limited to 128 characters. + type: string + optional: + default: false + description: |- + Specify whether the file or its key must be defined. If the file or key + does not exist, then the env var is not published. + If optional is set to true and the specified key does not exist, + the environment variable will not be set in the Pod's containers. + + If optional is set to false and the specified key does not exist, + an error will be returned during Pod creation. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '..' path or start with '..'. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the + exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + logLevel: + default: info + description: 'The log level for PostgreSQL instances. Valid values + are: `error`, `warning`, `info` (default), `debug`, `trace`' + enum: + - error + - warning + - info + - debug + - trace + type: string + resources: + description: Resources define cpu/memory requests and limits for + the sidecar that runs in the instance pods. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This field depends on the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + request: + description: |- + Request is the name chosen for a request in the referenced claim. + If empty, everything from the claim is made available, otherwise + only the result of this request. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + 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. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + retentionPolicyIntervalSeconds: + default: 1800 + description: |- + The retentionCheckInterval defines the frequency at which the + system checks and enforces retention policies. + type: integer + type: object + retentionPolicy: + description: |- + RetentionPolicy is the retention policy to be used for backups + and WALs (i.e. '60d'). The retention policy is expressed in the form + of `XXu` where `XX` is a positive integer and `u` is in `[dwm]` - + days, weeks, months. + pattern: ^[1-9][0-9]*[dwm]$ + type: string + required: + - configuration + type: object + status: + description: |- + Most recently observed status of the ObjectStore. This data may not be up to + date. Populated by the system. Read-only. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status + properties: + serverRecoveryWindow: + additionalProperties: + description: |- + RecoveryWindow represents the time span between the first + recoverability point and the last successful backup of a PostgreSQL + server, defining the period during which data can be restored. + properties: + firstRecoverabilityPoint: + description: |- + The first recoverability point in a PostgreSQL server refers to + the earliest point in time to which the database can be + restored. + format: date-time + type: string + lastFailedBackupTime: + description: The last failed backup time + format: date-time + type: string + lastSuccessfulBackupTime: + description: The last successful backup time + format: date-time + type: string + type: object + description: ServerRecoveryWindow maps each server to its recovery + window + type: object + type: object + required: + - metadata + - spec + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + labels: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: plugin-barman-cloud + name: plugin-barman-cloud + namespace: cnpg-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: plugin-barman-cloud + name: barman-plugin-leader-election-role + namespace: cnpg-system +rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: barman-plugin-metrics-auth-role +rules: +- apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create +- apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: barman-plugin-metrics-reader +rules: +- nonResourceURLs: + - /metrics + verbs: + - get +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: plugin-barman-cloud + name: barman-plugin-objectstore-editor-role +rules: +- apiGroups: + - barmancloud.cnpg.io + resources: + - objectstores + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - barmancloud.cnpg.io + resources: + - objectstores/status + verbs: + - get +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: plugin-barman-cloud + name: barman-plugin-objectstore-viewer-role +rules: +- apiGroups: + - barmancloud.cnpg.io + resources: + - objectstores + verbs: + - get + - list + - watch +- apiGroups: + - barmancloud.cnpg.io + resources: + - objectstores/status + verbs: + - get +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: plugin-barman-cloud +rules: +- apiGroups: + - "" + resources: + - secrets + verbs: + - create + - delete + - get + - list + - watch +- apiGroups: + - barmancloud.cnpg.io + resources: + - objectstores + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - barmancloud.cnpg.io + resources: + - objectstores/finalizers + verbs: + - update +- apiGroups: + - barmancloud.cnpg.io + resources: + - objectstores/status + verbs: + - get + - patch + - update +- apiGroups: + - postgresql.cnpg.io + resources: + - backups + verbs: + - get + - list + - watch +- apiGroups: + - postgresql.cnpg.io + resources: + - clusters/finalizers + verbs: + - update +- apiGroups: + - rbac.authorization.k8s.io + resources: + - rolebindings + - roles + verbs: + - create + - get + - list + - patch + - update + - watch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: plugin-barman-cloud + name: barman-plugin-leader-election-rolebinding + namespace: cnpg-system +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: barman-plugin-leader-election-role +subjects: +- kind: ServiceAccount + name: plugin-barman-cloud + namespace: cnpg-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: barman-plugin-metrics-auth-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: barman-plugin-metrics-auth-role +subjects: +- kind: ServiceAccount + name: plugin-barman-cloud + namespace: cnpg-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + labels: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: plugin-barman-cloud + name: plugin-barman-cloud-binding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: plugin-barman-cloud +subjects: +- kind: ServiceAccount + name: plugin-barman-cloud + namespace: cnpg-system +--- +apiVersion: v1 +data: + SIDECAR_IMAGE: | + Z2hjci5pby9jbG91ZG5hdGl2ZS1wZy9wbHVnaW4tYmFybWFuLWNsb3VkLXNpZGVjYXItdG + VzdGluZzptYWlu +kind: Secret +metadata: + name: plugin-barman-cloud-8tfddg42gf + namespace: cnpg-system +type: Opaque +--- +apiVersion: v1 +kind: Service +metadata: + annotations: + cnpg.io/pluginClientSecret: barman-cloud-client-tls + cnpg.io/pluginPort: "9090" + cnpg.io/pluginServerSecret: barman-cloud-server-tls + labels: + app: barman-cloud + cnpg.io/pluginName: barman-cloud.cloudnative-pg.io + name: barman-cloud + namespace: cnpg-system +spec: + ports: + - port: 9090 + protocol: TCP + targetPort: 9090 + selector: + app: barman-cloud +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: barman-cloud + name: barman-cloud + namespace: cnpg-system +spec: + replicas: 1 + selector: + matchLabels: + app: barman-cloud + strategy: + type: Recreate + template: + metadata: + labels: + app: barman-cloud + spec: + containers: + - args: + - operator + - --server-cert=/server/tls.crt + - --server-key=/server/tls.key + - --client-cert=/client/tls.crt + - --server-address=:9090 + - --leader-elect + - --log-level=debug + env: + - name: SIDECAR_IMAGE + valueFrom: + secretKeyRef: + key: SIDECAR_IMAGE + name: plugin-barman-cloud-8tfddg42gf + image: ghcr.io/cloudnative-pg/plugin-barman-cloud-testing:main + name: barman-cloud + ports: + - containerPort: 9090 + protocol: TCP + readinessProbe: + initialDelaySeconds: 10 + periodSeconds: 10 + tcpSocket: + port: 9090 + resources: {} + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + runAsGroup: 10001 + runAsUser: 10001 + seccompProfile: + type: RuntimeDefault + volumeMounts: + - mountPath: /server + name: server + - mountPath: /client + name: client + securityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + serviceAccountName: plugin-barman-cloud + volumes: + - name: server + secret: + secretName: barman-cloud-server-tls + - name: client + secret: + secretName: barman-cloud-client-tls +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: barman-cloud-client + namespace: cnpg-system +spec: + commonName: barman-cloud-client + duration: 2160h + isCA: false + issuerRef: + group: cert-manager.io + kind: Issuer + name: selfsigned-issuer + renewBefore: 360h + secretName: barman-cloud-client-tls + usages: + - client auth +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: barman-cloud-server + namespace: cnpg-system +spec: + commonName: barman-cloud + dnsNames: + - barman-cloud + duration: 2160h + isCA: false + issuerRef: + group: cert-manager.io + kind: Issuer + name: selfsigned-issuer + renewBefore: 360h + secretName: barman-cloud-server-tls + usages: + - server auth +--- +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + name: selfsigned-issuer + namespace: cnpg-system +spec: + selfSigned: {} diff --git a/cache-infrastructure/index.ts b/cache-infrastructure/index.ts new file mode 100644 index 0000000..0cc15a4 --- /dev/null +++ b/cache-infrastructure/index.ts @@ -0,0 +1,17 @@ +import { Construct } from "constructs"; +import { TerraformStack } from "cdktf"; +import { KubernetesProvider } from "@cdktf/provider-kubernetes/lib/provider"; +import { NixCache } from "./nixcache"; + +export class CacheInfrastructure extends TerraformStack { + constructor(scope: Construct, id: string) { + super(scope, id); + + const kubernetes = new KubernetesProvider(this, "kubernetes", { + configPath: "~/.kube/config", + }); + + // Add cache-related infrastructure components here + new NixCache(this, "nix-cache", kubernetes); + } +} diff --git a/nixcache/index.ts b/cache-infrastructure/nixcache/index.ts similarity index 90% rename from nixcache/index.ts rename to cache-infrastructure/nixcache/index.ts index 0da1755..5337909 100644 --- a/nixcache/index.ts +++ b/cache-infrastructure/nixcache/index.ts @@ -1,23 +1,18 @@ import * as fs from "fs"; import * as path from "path"; import { Construct } from "constructs"; -import { TerraformStack } from "cdktf"; import { PersistentVolumeClaimV1 } from "@cdktf/provider-kubernetes/lib/persistent-volume-claim-v1"; import { ConfigMapV1 } from "@cdktf/provider-kubernetes/lib/config-map-v1"; import { DeploymentV1 } from "@cdktf/provider-kubernetes/lib/deployment-v1"; import { KubernetesProvider } from "@cdktf/provider-kubernetes/lib/provider"; -import { TraefikIngressRoute } from "../traefik/ingress-route"; +import { IngressRoute } from "../../utils"; import { ServiceV1 } from "@cdktf/provider-kubernetes/lib/service-v1"; -export class NixCache extends TerraformStack { - constructor(scope: Construct, id: string) { +export class NixCache extends Construct { + constructor(scope: Construct, id: string, kubernetes: KubernetesProvider) { super(scope, id); - const kubernetes = new KubernetesProvider(this, "kubernetes", { - configPath: "~/.kube/config", - }); - const pvc = new PersistentVolumeClaimV1(this, "pvc", { provider: kubernetes, metadata: { @@ -134,7 +129,7 @@ export class NixCache extends TerraformStack { }, }); - new TraefikIngressRoute(this, "ingress-route", { + new IngressRoute(this, "ingress-route", { provider: kubernetes, namespace: "homelab", host: "nix.dogar.dev", diff --git a/nixcache/nginx.conf b/cache-infrastructure/nixcache/nginx.conf similarity index 100% rename from nixcache/nginx.conf rename to cache-infrastructure/nixcache/nginx.conf diff --git a/gitea/index.ts b/gitea/index.ts deleted file mode 100644 index b2de3fd..0000000 --- a/gitea/index.ts +++ /dev/null @@ -1,69 +0,0 @@ -import * as fs from "fs"; -import { HelmProvider } from "@cdktf/provider-helm/lib/provider"; -import { Release } from "@cdktf/provider-helm/lib/release"; -import { Construct } from "constructs"; -import { Manifest } from "@cdktf/provider-kubernetes/lib/manifest"; -import { KubernetesProvider } from "@cdktf/provider-kubernetes/lib/provider"; - -type GiteaServerOptions = { - providers: { - helm: HelmProvider; - kubernetes: KubernetesProvider; - }; - name: string; - namespace: string; - r2Endpoint: string; -}; - -export class GiteaServer extends Construct { - constructor(scope: Construct, id: string, options: GiteaServerOptions) { - super(scope, id); - - const { kubernetes, helm } = options.providers; - - new Release(this, id, { - ...options, - provider: helm, - repository: "https://dl.gitea.com/charts", - chart: "gitea", - createNamespace: true, - set: [ - { - name: "gitea.config.storage.MINIO_ENDPOINT", - value: options.r2Endpoint, - }, - ], - values: [ - fs.readFileSync("helm/values/gitea.values.yaml", { - encoding: "utf8", - }), - ], - }); - - new Manifest(this, `${id}-ssh-ingress`, { - provider: kubernetes, - manifest: { - apiVersion: "traefik.io/v1alpha1", - kind: "IngressRouteTCP", - metadata: { - name: "gitea-ssh-ingress", - namespace: options.namespace, - }, - spec: { - entryPoints: ["ssh"], - routes: [ - { - match: "HostSNI(`*`)", - services: [ - { - name: `${options.name}-ssh`, - port: 22, - }, - ], - }, - ], - }, - }, - }); - } -} diff --git a/k8s-operators/barman.ts b/k8s-operators/barman.ts new file mode 100644 index 0000000..8fb40f5 --- /dev/null +++ b/k8s-operators/barman.ts @@ -0,0 +1,42 @@ +import { Construct } from "constructs"; +import { NullProvider } from "@cdktf/provider-null/lib/provider"; +import { Resource } from "@cdktf/provider-null/lib/resource"; + +export interface BarmanCloudPluginInstallOptions { + /** URL to the CloudNativePG barman-cloud plugin manifest */ + url: string; +} + +export class BarmanCloudPluginInstall extends Construct { + constructor( + scope: Construct, + id: string, + opts: BarmanCloudPluginInstallOptions, + ) { + super(scope, id); + + const { url } = opts; + + const applyCmd = ["kubectl", "apply", "-f", url].join(" "); + const deleteCmd = ["kubectl", "delete", "-f", url].join(" "); + + new Resource(this, "barman-install", { + provider: new NullProvider(this, "barman"), + provisioners: [ + { + type: "local-exec", + when: "create", + command: applyCmd, + }, + { + type: "local-exec", + when: "destroy", + command: deleteCmd, + }, + ], + triggers: { + url, + }, + }); + } +} diff --git a/k8s-operators/index.ts b/k8s-operators/index.ts new file mode 100644 index 0000000..1b90286 --- /dev/null +++ b/k8s-operators/index.ts @@ -0,0 +1,72 @@ +import * as fs from "fs"; +import * as path from "path"; +import { HelmProvider } from "@cdktf/provider-helm/lib/provider"; +import { Release } from "@cdktf/provider-helm/lib/release"; +import { TerraformStack } from "cdktf"; +import { Construct } from "constructs"; +import { BarmanCloudPluginInstall } from "./barman"; +import { Prometheus } from "./prometheus"; + +export class K8SOperators extends TerraformStack { + constructor(scope: Construct, id: string) { + super(scope, id); + + const helm = new HelmProvider(this, "helm", { + kubernetes: { + configPath: "~/.kube/config", + }, + }); + + new Prometheus(this, "prometheus", { + provider: helm, + namespace: "monitoring", + name: "prometheus-operator", + version: "75.10.0", + }); + + new Release(this, "onepassword-operator", { + provider: helm, + name: "onepassword-operator", + chart: "connect", + repository: "https://1password.github.io/connect-helm-charts/", + namespace: "1password", + createNamespace: true, + set: [ + { + name: "operator.create", + value: "true", + }, + ], + setSensitive: [ + { + name: "operator.token.value", + value: process.env.OP_CONNECT_TOKEN!, + }, + { + name: "connect.credentials_base64", + value: btoa( + fs.readFileSync( + path.join(__dirname, "1password-credentials.json"), + "utf-8", + ), + ), + }, + ], + }); + + const cnpg = new Release(this, "cnpg-operator", { + provider: helm, + repository: "https://cloudnative-pg.github.io/charts", + chart: "cloudnative-pg", + name: "postgres-system", + namespace: "cnpg-system", + createNamespace: true, + }); + + const barman = new BarmanCloudPluginInstall(this, "barman-cloud-plugin", { + url: "https://github.com/cloudnative-pg/plugin-barman-cloud/releases/download/v0.9.0/manifest.yaml", + }); + + barman.node.addDependency(cnpg); + } +} diff --git a/prometheus/index.ts b/k8s-operators/prometheus/index.ts similarity index 100% rename from prometheus/index.ts rename to k8s-operators/prometheus/index.ts diff --git a/main.ts b/main.ts index c414eb6..3d61eef 100644 --- a/main.ts +++ b/main.ts @@ -1,35 +1,27 @@ import * as dotenv from "dotenv"; import { cleanEnv, str } from "envalid"; import { Construct } from "constructs"; -import { App, TerraformStack, LocalBackend, PgBackend } from "cdktf"; +import { App, TerraformStack, LocalBackend, TerraformOutput } from "cdktf"; import { HelmProvider } from "@cdktf/provider-helm/lib/provider"; import { KubernetesProvider } from "@cdktf/provider-kubernetes/lib/provider"; import { NamespaceV1 } from "@cdktf/provider-kubernetes/lib/namespace-v1"; -import { GiteaServer } from "./gitea"; -import { OnePassword } from "./1password"; -import { PostgresCluster } from "./postgres"; import { Longhorn } from "./longhorn"; -import { AuthentikServer } from "./authentik"; -import { ValkeyCluster } from "./valkey"; import { CertManager } from "./cert-manager"; import { Traefik } from "./traefik"; -import { Prometheus } from "./prometheus"; import { MetalLB } from "./metallb"; -import { NixCache } from "./nixcache"; +import { CacheInfrastructure } from "./cache-infrastructure"; +import { UtilityServices } from "./utility-services"; +import { K8SOperators } from "./k8s-operators"; dotenv.config(); -const env = cleanEnv(process.env, { +cleanEnv(process.env, { ACCOUNT_ID: str({ desc: "Cloudflare account id." }), - PG_CONN_STR: str({ - desc: "PostgreSQL connection string for Terraform state backend.", - }), + OP_CONNECT_TOKEN: str({ desc: "1Password Connect token." }), }); -const r2Endpoint = `https://${env.ACCOUNT_ID}.r2.cloudflarestorage.com`; - -class Homelab extends TerraformStack { +class CoreServices extends TerraformStack { constructor(scope: Construct, id: string) { super(scope, id); @@ -52,6 +44,10 @@ class Homelab extends TerraformStack { }, }); + new TerraformOutput(this, "namespace-output", { + value: namespace, + }); + new Longhorn(this, "longhorn", { name: "longhorn", providers: { @@ -66,21 +62,14 @@ class Homelab extends TerraformStack { namespace: "metallb-system", }); - new OnePassword(this, "one-password", { - provider: kubernetes, - namespace, - }); - new Traefik(this, "traefik", { provider: helm, namespace, name: "traefik", }); - const certManagerApiVersion = "cert-manager.io/v1"; - new CertManager(this, "cert-manager", { - certManagerApiVersion, + certManagerApiVersion: "cert-manager.io/v1", name: "cert-manager", namespace, version: "1.18.2", @@ -89,71 +78,39 @@ class Homelab extends TerraformStack { helm, }, }); - - new Prometheus(this, "prometheus", { - provider: helm, - namespace, - name: "prometheus-operator", - version: "75.10.0", - }); - - const pg = new PostgresCluster(this, "postgres-cluster", { - certManagerApiVersion, - name: "postgres-cluster", - namespace, - providers: { - kubernetes, - helm, - }, - users: ["shahab", "budget-tracker", "authentik", "gitea"], - primaryUser: "shahab", - initSecretName: "postgres-password", - backupR2EndpointURL: r2Endpoint, - }); - - const valkey = new ValkeyCluster(this, "valkey-cluster", { - provider: kubernetes, - namespace, - name: "valkey", - }); - - const authentik = new AuthentikServer(this, "authentik-server", { - provider: helm, - name: "authentik", - namespace, - }); - - authentik.node.addDependency(pg); - authentik.node.addDependency(valkey); - - const gitea = new GiteaServer(this, "gitea-server", { - name: "gitea", - namespace, - providers: { - helm, - kubernetes, - }, - r2Endpoint: `${env.ACCOUNT_ID}.r2.cloudflarestorage.com`, - }); - - gitea.node.addDependency(authentik); } } const app = new App(); -const homelab = new Homelab(app, "homelab"); +const coreServices = new CoreServices(app, "homelab"); -const nixCache = new NixCache(app, "nix-cache"); -nixCache.node.addDependency(homelab); +const k8sOperators = new K8SOperators(app, "k8s-operators"); +k8sOperators.node.addDependency(coreServices); -new LocalBackend(homelab, { +const utilityServices = new UtilityServices(app, "utility-services"); +utilityServices.node.addDependency(k8sOperators); + +const caches = new CacheInfrastructure(app, "cache-infrastructure"); +caches.node.addDependency(utilityServices); + +new LocalBackend(coreServices, { path: "terraform.tfstate", workspaceDir: ".", }); -new PgBackend(nixCache, { - schemaName: "nix_cache", - connStr: env.PG_CONN_STR, +new LocalBackend(caches, { + path: "terraform.tfstate", + workspaceDir: "./cachestf", +}); + +new LocalBackend(utilityServices, { + path: "terraform.tfstate", + workspaceDir: "./utilityservicestf", +}); + +new LocalBackend(k8sOperators, { + path: "terraform.tfstate", + workspaceDir: "./k8soperatorstf", }); app.synth(); diff --git a/package-lock.json b/package-lock.json index 4042a26..04dd70c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,46 +9,60 @@ "version": "1.0.0", "license": "GPL-3.0-or-later", "dependencies": { - "@cdktf/provider-helm": "10.5.0", - "@cdktf/provider-kubernetes": "11.12.1", - "cdktf": "^0.20.12", - "constructs": "^10.4.2", - "dotenv": "^16.5.0", - "envalid": "^8.0.0" + "@cdktf/provider-helm": "12.1.1", + "@cdktf/provider-kubernetes": "12.1.0", + "@cdktf/provider-null": "^11.0.0", + "cdktf": "^0.21.0", + "constructs": "^10.4.3", + "dotenv": "^17.2.3", + "envalid": "^8.1.1" }, "devDependencies": { - "@types/node": "^24.0.3", + "@types/node": "^24.10.1", "ts-node": "^10.9.2", - "typescript": "^5.8.3" + "typescript": "^5.9.3" }, "engines": { "node": "24" } }, "node_modules/@cdktf/provider-helm": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/@cdktf/provider-helm/-/provider-helm-10.5.0.tgz", - "integrity": "sha512-u3Q6VNIayaSFfEKZh+JG3PDrwcl9igHLWUdi6cK1G385tw4UyUpZ8osUnGhOErxbZtlcp4yeZ1c5+1OMP4epLA==", + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/@cdktf/provider-helm/-/provider-helm-12.1.1.tgz", + "integrity": "sha512-bi1Smig+b38eKs0yP/JJhbwTKHclp91fNLkcEDS7nI+6AQ4+uqN24CxHGUc6hpwNmatNnLH90gR0+iq/p6KEuw==", "license": "MPL-2.0", "engines": { - "node": ">= 18.12.0" + "node": ">= 20.9.0" }, "peerDependencies": { - "cdktf": "^0.20.0", - "constructs": "^10.3.0" + "cdktf": "^0.21.0", + "constructs": "^10.4.2" } }, "node_modules/@cdktf/provider-kubernetes": { - "version": "11.12.1", - "resolved": "https://registry.npmjs.org/@cdktf/provider-kubernetes/-/provider-kubernetes-11.12.1.tgz", - "integrity": "sha512-8LgaY0VULF/2f8iXqojGujP7DKSzl1didqbxMb7uMX0oE3EVDdtmJNIAY2D6oXjW95b9+NVQmhg4iN/jiF7zpA==", + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/@cdktf/provider-kubernetes/-/provider-kubernetes-12.1.0.tgz", + "integrity": "sha512-GVFbQIPaMeGbzbGyvTWwBUgdc9kKOGWRQNmzvD5A1bFtDTAVk77kRfdfooVuj869TDHF77WXIn6LGp8uuHoJrQ==", "license": "MPL-2.0", "engines": { - "node": ">= 18.12.0" + "node": ">= 20.9.0" }, "peerDependencies": { - "cdktf": "^0.20.0", - "constructs": "^10.3.0" + "cdktf": "^0.21.0", + "constructs": "^10.4.2" + } + }, + "node_modules/@cdktf/provider-null": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@cdktf/provider-null/-/provider-null-11.0.0.tgz", + "integrity": "sha512-OX/ADMXtPWBV/ZTBxCiMGUX0EMI+ooxXZWrZAskJAKIO2Ny1tpBXLE13NpfU9fG+6GkR4e1hLNsOMdO99DuhCA==", + "license": "MPL-2.0", + "engines": { + "node": ">= 20.9.0" + }, + "peerDependencies": { + "cdktf": "^0.21.0", + "constructs": "^10.4.2" } }, "node_modules/@cspotcode/source-map-support": { @@ -93,9 +107,9 @@ } }, "node_modules/@tsconfig/node10": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", - "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", "dev": true, "license": "MIT" }, @@ -121,9 +135,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.9.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz", - "integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==", + "version": "24.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", + "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", "dev": true, "license": "MIT", "peer": true, @@ -165,9 +179,9 @@ "license": "MIT" }, "node_modules/cdktf": { - "version": "0.20.12", - "resolved": "https://registry.npmjs.org/cdktf/-/cdktf-0.20.12.tgz", - "integrity": "sha512-ZBg2gA3Uw0WvGFlgrY1uxo6QHWn+ZdHiDkZQyOsTBl68k62UlaV8K7RR51d0E/amQG/CjtKOJr5XPFFAcOq0VA==", + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/cdktf/-/cdktf-0.21.0.tgz", + "integrity": "sha512-bdTOOyrFSXw0p5d7/3dye7ZWYzrUatyMjWEAAwTGoqghjygRj6Q55y1QZnSB021NRDzYZ3BhFGsOkpmIjQMzNQ==", "bundleDependencies": [ "archiver", "json-stable-stringify", @@ -177,11 +191,11 @@ "peer": true, "dependencies": { "archiver": "7.0.1", - "json-stable-stringify": "1.2.1", - "semver": "7.7.1" + "json-stable-stringify": "1.3.0", + "semver": "7.7.2" }, "peerDependencies": { - "constructs": "^10.3.0" + "constructs": "^10.4.2" } }, "node_modules/cdktf/node_modules/@isaacs/cliui": { @@ -766,12 +780,12 @@ } }, "node_modules/cdktf/node_modules/json-stable-stringify": { - "version": "1.2.1", + "version": "1.3.0", "inBundle": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.8", - "call-bound": "^1.0.3", + "call-bound": "^1.0.4", "isarray": "^2.0.5", "jsonify": "^0.0.1", "object-keys": "^1.1.1" @@ -852,6 +866,17 @@ "node": ">= 0.4" } }, + "node_modules/cdktf/node_modules/minimatch": { + "version": "5.1.6", + "inBundle": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/cdktf/node_modules/minipass": { "version": "7.1.2", "inBundle": true, @@ -940,17 +965,6 @@ "minimatch": "^5.1.0" } }, - "node_modules/cdktf/node_modules/readdir-glob/node_modules/minimatch": { - "version": "5.1.6", - "inBundle": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/cdktf/node_modules/safe-buffer": { "version": "5.2.1", "funding": [ @@ -971,7 +985,7 @@ "license": "MIT" }, "node_modules/cdktf/node_modules/semver": { - "version": "7.7.1", + "version": "7.7.2", "inBundle": true, "license": "ISC", "bin": { @@ -1249,9 +1263,9 @@ } }, "node_modules/constructs": { - "version": "10.4.2", - "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.4.2.tgz", - "integrity": "sha512-wsNxBlAott2qg8Zv87q3eYZYgheb9lchtBfjHzzLHtXbttwSrHPs1NNQbBrmbb1YZvYg2+Vh0Dor76w4mFxJkA==", + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.4.3.tgz", + "integrity": "sha512-3+ZB67qWGM1vEstNpj6pGaLNN1qz4gxC1CBhEUhZDZk0PqzQWY65IzC1Doq17MGPa9xa2wJ1G/DJ3swU8kWAHQ==", "license": "Apache-2.0", "peer": true }, @@ -1273,9 +1287,9 @@ } }, "node_modules/dotenv": { - "version": "16.6.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -1285,9 +1299,9 @@ } }, "node_modules/envalid": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/envalid/-/envalid-8.1.0.tgz", - "integrity": "sha512-OT6+qVhKVyCidaGoXflb2iK1tC8pd0OV2Q+v9n33wNhUJ+lus+rJobUj4vJaQBPxPZ0vYrPGuxdrenyCAIJcow==", + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/envalid/-/envalid-8.1.1.tgz", + "integrity": "sha512-vOUfHxAFFvkBjbVQbBfgnCO9d3GcNfMMTtVfgqSU2rQGMFEVqWy9GBuoSfHnwGu7EqR0/GeukQcL3KjFBaga9w==", "license": "MIT", "dependencies": { "tslib": "2.8.1" diff --git a/package.json b/package.json index 8f46a7f..771e181 100644 --- a/package.json +++ b/package.json @@ -23,16 +23,17 @@ "upgrade:next": "npm i cdktf@next cdktf-cli@next" }, "dependencies": { - "@cdktf/provider-helm": "10.5.0", - "@cdktf/provider-kubernetes": "11.12.1", - "cdktf": "^0.20.12", - "constructs": "^10.4.2", - "dotenv": "^16.5.0", - "envalid": "^8.0.0" + "@cdktf/provider-helm": "12.1.1", + "@cdktf/provider-kubernetes": "12.1.0", + "@cdktf/provider-null": "^11.0.0", + "cdktf": "^0.21.0", + "constructs": "^10.4.3", + "dotenv": "^17.2.3", + "envalid": "^8.1.1" }, "devDependencies": { - "@types/node": "^24.0.3", + "@types/node": "^24.10.1", "ts-node": "^10.9.2", - "typescript": "^5.8.3" + "typescript": "^5.9.3" } } diff --git a/postgres/index.ts b/postgres/index.ts index ccab3b2..cb0c5de 100644 --- a/postgres/index.ts +++ b/postgres/index.ts @@ -1,14 +1,9 @@ -import { HelmProvider } from "@cdktf/provider-helm/lib/provider"; -import { Release } from "@cdktf/provider-helm/lib/release"; import { Manifest } from "@cdktf/provider-kubernetes/lib/manifest"; import { KubernetesProvider } from "@cdktf/provider-kubernetes/lib/provider"; import { Construct } from "constructs"; type PostgresClusterOptions = { - providers: { - kubernetes: KubernetesProvider; - helm: HelmProvider; - }; + provider: KubernetesProvider; name: string; namespace: string; users: string[]; @@ -22,16 +17,7 @@ export class PostgresCluster extends Construct { constructor(scope: Construct, id: string, options: PostgresClusterOptions) { super(scope, id); - const { kubernetes, helm } = options.providers; - - new Release(this, "cnpg-operator", { - provider: helm, - repository: "https://cloudnative-pg.github.io/charts", - chart: "cloudnative-pg", - name: "postgres-system", - namespace: "cnpg-system", - createNamespace: true, - }); + const { provider } = options; const destinationPath = "s3://postgres-backups/"; const endpointURL = options.backupR2EndpointURL; @@ -64,7 +50,7 @@ export class PostgresCluster extends Construct { }; new Manifest(this, "r2-backup-store", { - provider: kubernetes, + provider, manifest: { apiVersion: "barmancloud.cnpg.io/v1", kind: "ObjectStore", @@ -95,7 +81,7 @@ export class PostgresCluster extends Construct { // Self-signed issuer for creating CA certificates new Manifest(this, "selfsigned-issuer", { - provider: kubernetes, + provider, manifest: { apiVersion: certManagerApiVersion, kind: "Issuer", @@ -111,7 +97,7 @@ export class PostgresCluster extends Construct { // Server CA certificate new Manifest(this, "server-ca-cert", { - provider: kubernetes, + provider, manifest: { apiVersion: certManagerApiVersion, kind: "Certificate", @@ -140,7 +126,7 @@ export class PostgresCluster extends Construct { // Issuer using the server CA new Manifest(this, "server-ca-issuer", { - provider: kubernetes, + provider, manifest: { apiVersion: certManagerApiVersion, kind: "Issuer", @@ -158,7 +144,7 @@ export class PostgresCluster extends Construct { // Server certificate new Manifest(this, "server-cert", { - provider: kubernetes, + provider, manifest: { apiVersion: certManagerApiVersion, kind: "Certificate", @@ -187,7 +173,7 @@ export class PostgresCluster extends Construct { // Client CA certificate new Manifest(this, "client-ca", { - provider: kubernetes, + provider, manifest: { apiVersion: certManagerApiVersion, kind: "Certificate", @@ -216,7 +202,7 @@ export class PostgresCluster extends Construct { // Issuer using the client CA new Manifest(this, "client-ca-issuer", { - provider: kubernetes, + provider, manifest: { apiVersion: certManagerApiVersion, kind: "Issuer", @@ -234,7 +220,7 @@ export class PostgresCluster extends Construct { // Secret for client certificate new Manifest(this, `${certNames.client}-secret`, { - provider: kubernetes, + provider, manifest: { apiVersion: "v1", kind: "Secret", @@ -250,7 +236,7 @@ export class PostgresCluster extends Construct { // Client certificate for streaming replica new Manifest(this, "streaming-replica-cert", { - provider: kubernetes, + provider, manifest: { apiVersion: certManagerApiVersion, kind: "Certificate", @@ -277,7 +263,7 @@ export class PostgresCluster extends Construct { options.users.forEach( (user) => new Manifest(this, `${user}-client-cert`, { - provider: kubernetes, + provider, manifest: { apiVersion: certManagerApiVersion, kind: "Certificate", @@ -302,7 +288,7 @@ export class PostgresCluster extends Construct { ); new Manifest(this, "postgres-cluster", { - provider: kubernetes, + provider, fieldManager: { forceConflicts: true }, manifest: { apiVersion: "postgresql.cnpg.io/v1", @@ -435,7 +421,7 @@ export class PostgresCluster extends Construct { }); new Manifest(this, "postgres-backup-job", { - provider: kubernetes, + provider, manifest: { apiVersion: "postgresql.cnpg.io/v1", kind: "ScheduledBackup", diff --git a/types/index.ts b/types/index.ts new file mode 100644 index 0000000..71aa24a --- /dev/null +++ b/types/index.ts @@ -0,0 +1,7 @@ +import { KubernetesProvider } from "@cdktf/provider-kubernetes/lib/provider"; +import { HelmProvider } from "@cdktf/provider-helm/lib/provider"; + +export type Providers = { + kubernetes: KubernetesProvider; + helm: HelmProvider; +}; diff --git a/utility-services/authentik/index.ts b/utility-services/authentik/index.ts new file mode 100644 index 0000000..477beee --- /dev/null +++ b/utility-services/authentik/index.ts @@ -0,0 +1,47 @@ +import * as fs from "fs"; +import * as path from "path"; +import { Release } from "@cdktf/provider-helm/lib/release"; +import { Construct } from "constructs"; +import { OnePasswordSecret } from "../../utils"; +import { Providers } from "../../types"; + +type AuthentikServerOptions = { + providers: Providers; + name: string; + namespace: string; +}; + +export class AuthentikServer extends Construct { + constructor(scope: Construct, id: string, options: AuthentikServerOptions) { + super(scope, id); + + const { kubernetes, helm } = options.providers; + + new OnePasswordSecret(this, "secret-key", { + provider: kubernetes, + name: "authentik-secret-key", + namespace: options.namespace, + itemPath: "vaults/Lab/items/authentik-secret-key", + }); + + new OnePasswordSecret(this, "smtp", { + provider: kubernetes, + name: "authentik-smtp-token", + namespace: options.namespace, + itemPath: "vaults/Lab/items/smtp-token", + }); + + new Release(this, id, { + ...options, + provider: helm, + repository: "https://charts.goauthentik.io", + chart: "authentik", + createNamespace: true, + values: [ + fs.readFileSync(path.join(__dirname, "values.yaml"), { + encoding: "utf8", + }), + ], + }).importFrom("homelab/authentik"); + } +} diff --git a/utility-services/authentik/values.yaml b/utility-services/authentik/values.yaml new file mode 100644 index 0000000..f839e89 --- /dev/null +++ b/utility-services/authentik/values.yaml @@ -0,0 +1,110 @@ +global: + addPrometheusAnnotations: true + securityContext: + runAsUser: 1000 + fsGroup: 1000 + podLabels: + app: authentik + nodeSelector: + nodepool: worker + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: kubernetes.io/hostname + whenUnsatisfiable: DoNotSchedule + labelSelector: + matchLabels: + app: authentik + env: + - name: AUTHENTIK_SECRET_KEY + valueFrom: + secretKeyRef: + name: authentik-secret-key + key: password + - name: AUTHENTIK_EMAIL__USERNAME + valueFrom: + secretKeyRef: + name: authentik-smtp-token + key: authentik-username + - name: AUTHENTIK_EMAIL__PASSWORD + valueFrom: + secretKeyRef: + name: authentik-smtp-token + key: authentik-password + - name: AUTHENTIK_EMAIL__FROM + valueFrom: + secretKeyRef: + name: authentik-smtp-token + key: authentik-username + - name: AUTHENTIK_EMAIL__USE_TLS + value: "true" + - name: AUTHENTIK_POSTGRESQL__SSLMODE + value: verify-full + - name: AUTHENTIK_POSTGRESQL__SSLROOTCERT + value: "/opt/authentik/certs/ca.crt" + - name: AUTHENTIK_POSTGRESQL__SSLCERT + value: "/opt/authentik/certs/tls.crt" + - name: AUTHENTIK_POSTGRESQL__SSLKEY + value: "/opt/authentik/certs/tls.key" + - name: AUTHENTIK_REDIS__PASSWORD + valueFrom: + secretKeyRef: + name: valkey + key: password + volumes: + - name: ssl-bundle + projected: + sources: + - secret: + name: authentik-client-cert + items: + - key: tls.crt + path: tls.crt + - key: tls.key + path: tls.key + mode: 0600 + - secret: + name: postgres-server-cert + items: + - key: ca.crt + path: ca.crt + volumeMounts: + - name: ssl-bundle + mountPath: /opt/authentik/certs + readOnly: true + +authentik: + error_reporting: + enabled: false + email: + host: "smtp.protonmail.ch" + port: 587 + postgresql: + host: postgres-cluster-rw + user: authentik + name: authentik + redis: + host: valkey + +server: + replicas: 3 + ingress: + enabled: true + annotations: + cert-manager.io/cluster-issuer: cloudflare-issuer + cert-manager.io/acme-challenge-type: dns01 + cert-manager.io/private-key-size: "4096" + ingressClassName: traefik + hosts: + - auth.dogar.dev + tls: + - secretName: authentik-tls + hosts: + - auth.dogar.dev + +worker: + replicas: 3 + +postgresql: + enabled: false +redis: + enabled: false diff --git a/utility-services/gitea/index.ts b/utility-services/gitea/index.ts new file mode 100644 index 0000000..3ccfa12 --- /dev/null +++ b/utility-services/gitea/index.ts @@ -0,0 +1 @@ +export { GiteaServer } from "./server"; diff --git a/utility-services/gitea/server.ts b/utility-services/gitea/server.ts new file mode 100644 index 0000000..cd9ce37 --- /dev/null +++ b/utility-services/gitea/server.ts @@ -0,0 +1,82 @@ +import * as fs from "fs"; +import * as path from "path"; +import { HelmProvider } from "@cdktf/provider-helm/lib/provider"; +import { Release } from "@cdktf/provider-helm/lib/release"; +import { Construct } from "constructs"; +import { KubernetesProvider } from "@cdktf/provider-kubernetes/lib/provider"; + +import { OnePasswordSecret } from "../../utils"; +import { IngressRouteTcp } from "../../utils/traefik"; + +type GiteaServerOptions = { + providers: { + helm: HelmProvider; + kubernetes: KubernetesProvider; + }; + name: string; + namespace: string; + r2Endpoint: string; +}; + +export class GiteaServer extends Construct { + constructor(scope: Construct, id: string, options: GiteaServerOptions) { + super(scope, id); + + const { kubernetes, helm } = options.providers; + + new OnePasswordSecret(this, "admin", { + provider: kubernetes, + name: "gitea-admin", + namespace: options.namespace, + itemPath: "vaults/Lab/items/gitea-admin", + }); + + new OnePasswordSecret(this, "oauth", { + provider: kubernetes, + name: "gitea-oauth", + namespace: options.namespace, + itemPath: "vaults/Lab/items/gitea-oauth", + }); + + new OnePasswordSecret(this, "smtp", { + provider: kubernetes, + name: "gitea-smtp-token", + namespace: options.namespace, + itemPath: "vaults/Lab/items/smtp-token", + }); + + new OnePasswordSecret(this, "r2", { + provider: kubernetes, + name: "gitea-cloudflare-token", + namespace: options.namespace, + itemPath: "vaults/Lab/items/cloudflare", + }); + + new Release(this, id, { + ...options, + provider: helm, + repository: "https://dl.gitea.com/charts", + chart: "gitea", + createNamespace: true, + set: [ + { + name: "gitea.config.storage.MINIO_ENDPOINT", + value: options.r2Endpoint, + }, + ], + values: [ + fs.readFileSync(path.join(__dirname, "values.yaml"), { + encoding: "utf8", + }), + ], + }); + + new IngressRouteTcp(this, "ssh-ingress", { + provider: kubernetes, + namespace: options.namespace, + entryPoint: "ssh", + serviceName: `${options.name}-ssh`, + servicePort: 22, + }); + } +} diff --git a/utility-services/gitea/values.yaml b/utility-services/gitea/values.yaml new file mode 100644 index 0000000..86e8a7b --- /dev/null +++ b/utility-services/gitea/values.yaml @@ -0,0 +1,161 @@ +global: + storageClass: longhorn +image: + rootless: false +service: + http: + annotations: + metallb.universe.tf/allow-shared-ip: gitea + ssh: + annotations: + metallb.universe.tf/allow-shared-ip: gitea +ingress: + enabled: true + annotations: + cert-manager.io/cluster-issuer: cloudflare-issuer + cert-manager.io/acme-challenge-type: dns01 + cert-manager.io/private-key-size: 4096 + className: traefik + hosts: + - host: git.dogar.dev + paths: + - path: / + pathType: Prefix + tls: + - secretName: gitea-tls + hosts: + - git.dogar.dev +gitea: + podAnnotations: + prometheus.io/scrape: "true" + prometheus.io/port: "6060" + admin: + existingSecret: gitea-admin + metrics: + enabled: true + serviceMonitor: + enabled: true + config: + server: + ENABLE_PPROF: true + ENABLE_GZIP: true + LFS_START_SERVER: true + SSH_DOMAIN: git.dogar.dev + database: + DB_TYPE: postgres + HOST: postgres-cluster-rw + NAME: gitea + USER: gitea + SSL_MODE: verify-full + metrics: + ENABLED: true + cache: + ADAPTER: memory + session: + PROVIDER: db + PROVIDER_CONFIG: "" + queue: + TYPE: channel + storage: + STORAGE_TYPE: minio + MINIO_USE_SSL: true + MINIO_BUCKET_LOOKUP_STYLE: path + MINIO_LOCATION: auto + service: + DISABLE_REGISTRATION: true + oauth2_client: + ENABLE_AUTO_REGISTRATION: true + mailer: + ENABLED: true + PROTOCOL: smtp+starttls + SMTP_ADDR: smtp.protonmail.ch + SMTP_PORT: 587 + FROM: git@dogar.dev + picture: + GRAVATAR_SOURCE: gravatar + oauth: + - name: "authentik" + provider: "openidConnect" + existingSecret: gitea-oauth + autoDiscoverUrl: "https://auth.dogar.dev/application/o/gitea/.well-known/openid-configuration" + iconUrl: "https://goauthentik.io/img/icon.png" + scopes: "email profile" + additionalConfigFromEnvs: + - name: GITEA__MAILER__PASSWD + valueFrom: + secretKeyRef: + name: gitea-smtp-token + key: gitea-password + - name: GITEA__PACKAGES__CHUNKED_UPLOAD_PATH + value: "/tmp/gitea-uploads" + - name: GITEA__PACKAGES__CHUNKED_UPLOAD_CONCURRENCY + value: "4" + - name: GITEA__STORAGE__MINIO_ACCESS_KEY_ID + valueFrom: + secretKeyRef: + name: gitea-cloudflare-token + key: access_key_id + - name: GITEA__STORAGE__MINIO_SECRET_ACCESS_KEY + valueFrom: + secretKeyRef: + name: gitea-cloudflare-token + key: secret_access_key +persistence: + labels: + recurring-job.longhorn.io/source: "enabled" + recurring-job.longhorn.io/daily-backup: "enabled" + enabled: true + size: 50Gi + accessModes: + - ReadWriteMany +deployment: + env: + - name: PGSSLMODE + value: verify-full + - name: PGSSLROOTCERT + value: /opt/gitea/.postgresql/root.crt + - name: PGSSLCERT + value: /opt/gitea/.postgresql/postgresql.crt + - name: PGSSLKEY + value: /opt/gitea/.postgresql/postgresql.key +resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 6 + memory: 6Gi +extraVolumes: + - name: ssl-bundle + projected: + sources: + - secret: + name: gitea-client-cert + items: + - key: tls.crt + path: postgresql.crt + - key: tls.key + path: postgresql.key + mode: 0600 + - secret: + name: postgres-server-cert + items: + - key: ca.crt + path: root.crt + - name: gitea-temp + emptyDir: {} +extraInitVolumeMounts: + - name: ssl-bundle + mountPath: /opt/gitea/.postgresql + readOnly: true +extraContainerVolumeMounts: + - name: ssl-bundle + mountPath: /opt/gitea/.postgresql + readOnly: true + readOnly: true + - name: gitea-temp + mountPath: /tmp/gitea-uploads +postgresql-ha: + enabled: false +valkey-cluster: + enabled: false diff --git a/utility-services/index.ts b/utility-services/index.ts new file mode 100644 index 0000000..83dbf1e --- /dev/null +++ b/utility-services/index.ts @@ -0,0 +1,94 @@ +import * as path from "path"; +import { DataKubernetesNamespaceV1 } from "@cdktf/provider-kubernetes/lib/data-kubernetes-namespace-v1"; +import { KubernetesProvider } from "@cdktf/provider-kubernetes/lib/provider"; +import { HelmProvider } from "@cdktf/provider-helm/lib/provider"; +import { DataTerraformRemoteStateLocal, TerraformStack } from "cdktf"; +import { Construct } from "constructs"; + +import { ValkeyCluster } from "./valkey"; +import { GiteaServer } from "./gitea"; +import { AuthentikServer } from "./authentik"; +import { PostgresCluster } from "./postgres"; + +export class UtilityServices extends TerraformStack { + constructor(scope: Construct, id: string) { + super(scope, id); + + const kubernetes = new KubernetesProvider(this, "kubernetes", { + configPath: "~/.kube/config", + }); + + const helm = new HelmProvider(this, "helm", { + kubernetes: { + configPath: "~/.kube/config", + }, + }); + + const homelabState = new DataTerraformRemoteStateLocal( + this, + "homelab-state", + { + path: path.join( + __dirname, + "../cdktf.out/stacks/homelab/terraform.tfstate", + ), + }, + ); + + const namespaceName = homelabState.getString("namespace-output"); + const namespaceResource = new DataKubernetesNamespaceV1( + this, + "homelab-namespace", + { + provider: kubernetes, + metadata: { + name: namespaceName, + }, + }, + ); + const namespace = namespaceResource.metadata.name; + + const r2Endpoint = `${process.env.ACCOUNT_ID!}.r2.cloudflarestorage.com`; + + const valkeyCluster = new ValkeyCluster(this, "valkey-cluster", { + namespace, + provider: kubernetes, + name: "valkey", + }); + + const postgres = new PostgresCluster(this, "postgres-cluster", { + certManagerApiVersion: "cert-manager.io/v1", + name: "postgres-cluster", + namespace, + provider: kubernetes, + users: ["shahab", "budget-tracker", "authentik", "gitea"], + primaryUser: "shahab", + initSecretName: "postgres-password", + backupR2EndpointURL: `https://${r2Endpoint}`, + }); + + const authentik = new AuthentikServer(this, "authentik-server", { + providers: { + helm, + kubernetes, + }, + name: "authentik", + namespace, + }); + + authentik.node.addDependency(valkeyCluster); + authentik.node.addDependency(postgres); + + const gitea = new GiteaServer(this, "gitea-server", { + providers: { + helm, + kubernetes, + }, + name: "gitea", + namespace, + r2Endpoint: r2Endpoint, + }); + + gitea.node.addDependency(authentik); + } +} diff --git a/utility-services/postgres/index.ts b/utility-services/postgres/index.ts new file mode 100644 index 0000000..0d93c4a --- /dev/null +++ b/utility-services/postgres/index.ts @@ -0,0 +1,456 @@ +import { Manifest } from "@cdktf/provider-kubernetes/lib/manifest"; +import { KubernetesProvider } from "@cdktf/provider-kubernetes/lib/provider"; +import { Construct } from "constructs"; +import { OnePasswordSecret } from "../../utils"; + +type PostgresClusterOptions = { + provider: KubernetesProvider; + name: string; + namespace: string; + users: string[]; + primaryUser: string; + initSecretName: string; + certManagerApiVersion: string; + backupR2EndpointURL: string; +}; + +export class PostgresCluster extends Construct { + constructor(scope: Construct, id: string, options: PostgresClusterOptions) { + super(scope, id); + + const { provider } = options; + + const destinationPath = "s3://postgres-backups/"; + const endpointURL = options.backupR2EndpointURL; + const barmanStoreName = "r2-postgres-backup-store"; + const backupServerName = `${options.name}-backup`; + + const barmanConfiguration = { + destinationPath, + endpointURL, + s3Credentials: { + accessKeyId: { + name: "barman-cloudflare-token", + key: "access_key_id", + }, + secretAccessKey: { + name: "barman-cloudflare-token", + key: "secret_access_key", + }, + region: { + name: "barman-cloudflare-token", + key: "AWS_REGION", + }, + }, + wal: { + compression: "gzip", + }, + data: { + compression: "gzip", + }, + }; + + new OnePasswordSecret(this, "barman-cloudflare-token", { + provider: options.provider, + name: "barman-cloudflare-token", + namespace: options.namespace, + itemPath: "vaults/Lab/items/cloudflare", + }); + + new Manifest(this, "r2-backup-store", { + provider, + manifest: { + apiVersion: "barmancloud.cnpg.io/v1", + kind: "ObjectStore", + metadata: { + namespace: options.namespace, + name: barmanStoreName, + }, + spec: { + retentionPolicy: "15d", + configuration: { + ...barmanConfiguration, + }, + }, + }, + }); + + const { certManagerApiVersion } = options; + + const certNames = { + server: "postgres-server-cert", + client: "postgres-client-cert", + }; + + const caNames = { + server: "postgres-server-ca", + client: "postgres-client-ca", + }; + + // Self-signed issuer for creating CA certificates + new Manifest(this, "selfsigned-issuer", { + provider, + manifest: { + apiVersion: certManagerApiVersion, + kind: "Issuer", + metadata: { + name: "selfsigned-issuer", + namespace: options.namespace, + }, + spec: { + selfSigned: {}, + }, + }, + }); + + // Server CA certificate + new Manifest(this, "server-ca-cert", { + provider, + manifest: { + apiVersion: certManagerApiVersion, + kind: "Certificate", + metadata: { + name: "server-ca", + namespace: options.namespace, + }, + spec: { + isCA: true, + commonName: caNames.server, + secretName: caNames.server, + privateKey: { + algorithm: "ECDSA", + size: 384, + }, + duration: "52560h", // 6 years + renewBefore: "8760h", // 1 year before expiration + issuerRef: { + name: "selfsigned-issuer", + kind: "Issuer", + group: "cert-manager.io", + }, + }, + }, + }); + + // Issuer using the server CA + new Manifest(this, "server-ca-issuer", { + provider, + manifest: { + apiVersion: certManagerApiVersion, + kind: "Issuer", + metadata: { + name: `${caNames.server}-issuer`, + namespace: options.namespace, + }, + spec: { + ca: { + secretName: caNames.server, + }, + }, + }, + }); + + // Server certificate + new Manifest(this, "server-cert", { + provider, + manifest: { + apiVersion: certManagerApiVersion, + kind: "Certificate", + metadata: { + name: certNames.server, + namespace: options.namespace, + }, + spec: { + secretName: certNames.server, + usages: ["server auth"], + dnsNames: [ + "postgres-cluster-rw", + "postgres-cluster-rw.homelab.svc.cluster.local", + "postgres.dogar.dev", + ], + duration: "4380h", // 6 months + renewBefore: "720h", // 30 days before expiration + issuerRef: { + name: `${caNames.server}-issuer`, + kind: "Issuer", + group: "cert-manager.io", + }, + }, + }, + }); + + // Client CA certificate + new Manifest(this, "client-ca", { + provider, + manifest: { + apiVersion: certManagerApiVersion, + kind: "Certificate", + metadata: { + name: "client-ca", + namespace: options.namespace, + }, + spec: { + isCA: true, + commonName: caNames.client, + secretName: caNames.client, + privateKey: { + algorithm: "ECDSA", + size: 256, + }, + duration: "52560h", // 6 years + renewBefore: "8760h", // 1 year before expiration + issuerRef: { + name: "selfsigned-issuer", + kind: "Issuer", + group: "cert-manager.io", + }, + }, + }, + }); + + // Issuer using the client CA + new Manifest(this, "client-ca-issuer", { + provider, + manifest: { + apiVersion: certManagerApiVersion, + kind: "Issuer", + metadata: { + name: `${caNames.client}-issuer`, + namespace: options.namespace, + }, + spec: { + ca: { + secretName: caNames.client, + }, + }, + }, + }); + + // Secret for client certificate + new Manifest(this, `${certNames.client}-secret`, { + provider, + manifest: { + apiVersion: "v1", + kind: "Secret", + metadata: { + name: certNames.client, + namespace: options.namespace, + labels: { + "cnpg.io/reload": "", + }, + }, + }, + }); + + // Client certificate for streaming replica + new Manifest(this, "streaming-replica-cert", { + provider, + manifest: { + apiVersion: certManagerApiVersion, + kind: "Certificate", + metadata: { + name: certNames.client, + namespace: options.namespace, + }, + spec: { + secretName: certNames.client, + usages: ["client auth"], + commonName: "streaming_replica", + duration: "4380h", // 6 months + renewBefore: "720h", // 30 days before expiration + issuerRef: { + name: "postgres-client-ca-issuer", + kind: "Issuer", + group: "cert-manager.io", + }, + }, + }, + }); + + // Client certificates for users + options.users.forEach( + (user) => + new Manifest(this, `${user}-client-cert`, { + provider, + manifest: { + apiVersion: certManagerApiVersion, + kind: "Certificate", + metadata: { + name: `${user}-client-cert`, + namespace: options.namespace, + }, + spec: { + secretName: `${user}-client-cert`, + usages: ["client auth"], + commonName: user, + duration: "4380h", // 6 months + renewBefore: "720h", // 30 days before expiration + issuerRef: { + name: "postgres-client-ca-issuer", + kind: "Issuer", + group: "cert-manager.io", + }, + }, + }, + }), + ); + + new Manifest(this, "postgres-cluster", { + provider, + fieldManager: { forceConflicts: true }, + manifest: { + apiVersion: "postgresql.cnpg.io/v1", + kind: "Cluster", + metadata: { + name: options.name, + namespace: options.namespace, + }, + spec: { + instances: 3, + minSyncReplicas: 1, + maxSyncReplicas: 2, + primaryUpdateStrategy: "unsupervised", + certificates: { + serverCASecret: certNames.server, + serverTLSSecret: certNames.server, + clientCASecret: certNames.client, + replicationTLSSecret: certNames.client, + }, + postgresql: { + parameters: { + archive_mode: "on", + archive_timeout: "60min", + checkpoint_timeout: "10min", + checkpoint_completion_target: "0.7", + dynamic_shared_memory_type: "posix", + full_page_writes: "on", + log_destination: "csvlog", + log_directory: "/controller/log", + log_filename: "postgres", + log_rotation_age: "0", + log_rotation_size: "0", + log_truncate_on_rotation: "false", + logging_collector: "on", + max_parallel_workers: "32", + max_replication_slots: "32", + max_worker_processes: "32", + max_slot_wal_keep_size: "256MB", + max_wal_size: "512MB", + min_wal_size: "128MB", + shared_memory_type: "mmap", + shared_preload_libraries: "", + ssl_max_protocol_version: "TLSv1.3", + ssl_min_protocol_version: "TLSv1.3", + wal_compression: "on", + wal_keep_size: "128MB", + wal_level: "replica", + wal_log_hints: "on", + wal_receiver_timeout: "5s", + wal_sender_timeout: "5s", + }, + pg_hba: [ + `hostssl all ${options.primaryUser} all cert`, + "hostssl sameuser all all cert", + ], + }, + plugins: [ + { + name: "barman-cloud.cloudnative-pg.io", + isWALArchiver: true, + parameters: { + barmanObjectName: barmanStoreName, + serverName: backupServerName, + }, + }, + ], + bootstrap: { + recovery: { + source: "clusterBackup", + }, + }, + externalClusters: [ + { + name: "clusterBackup", + plugin: { + name: "barman-cloud.cloudnative-pg.io", + parameters: { + barmanObjectName: barmanStoreName, + serverName: backupServerName, + skipWalArchiveCheck: true, + }, + }, + }, + ], + managed: { + services: { + disabledDefaultServices: ["ro", "r"], + additional: [ + { + selectorType: "rw", + serviceTemplate: { + metadata: { + name: "postgres-cluster", + superuser: true, + }, + spec: { + type: "LoadBalancer", + }, + }, + }, + ], + }, + roles: [ + { + name: options.primaryUser, + inRoles: ["postgres"], + inherit: true, + disablePassword: true, + createdb: true, + createrole: true, + login: true, + ensure: "present", + }, + ], + }, + storage: { + size: "10Gi", + storageClass: "longhorn", + }, + walStorage: { + size: "2Gi", + storageClass: "longhorn", + }, + }, + }, + }); + + new Manifest(this, "postgres-backup-job", { + provider, + manifest: { + apiVersion: "postgresql.cnpg.io/v1", + kind: "ScheduledBackup", + metadata: { + name: "postgres-cluster", + namespace: options.namespace, + }, + spec: { + immediate: true, + // weekly midnight on Sunday + schedule: "* 0 0 * * 0", + backupOwnerReference: "self", + method: "plugin", + pluginConfiguration: { + name: "barman-cloud.cloudnative-pg.io", + parameters: { + barmanObjectName: barmanStoreName, + serverName: backupServerName, + }, + }, + cluster: { + name: options.name, + }, + }, + }, + }); + } +} diff --git a/valkey/index.ts b/utility-services/valkey/index.ts similarity index 94% rename from valkey/index.ts rename to utility-services/valkey/index.ts index 9c6d09f..124192a 100644 --- a/valkey/index.ts +++ b/utility-services/valkey/index.ts @@ -2,6 +2,7 @@ import { DeploymentV1 } from "@cdktf/provider-kubernetes/lib/deployment-v1"; import { KubernetesProvider } from "@cdktf/provider-kubernetes/lib/provider"; import { ServiceV1 } from "@cdktf/provider-kubernetes/lib/service-v1"; import { Construct } from "constructs"; +import { OnePasswordSecret } from "../../utils"; type ValkeyClusterOptions = { provider: KubernetesProvider; @@ -17,6 +18,13 @@ export class ValkeyCluster extends Construct { const labels = { app: "valkey" }; const { provider, name, namespace } = options; + new OnePasswordSecret(this, "valkey-secret", { + provider, + name: "valkey", + namespace, + itemPath: "vaults/Lab/items/valkey", + }); + new DeploymentV1(this, "valkey-deployment", { provider, metadata: { diff --git a/utils/1password-secret/index.ts b/utils/1password-secret/index.ts new file mode 100644 index 0000000..a8f43e3 --- /dev/null +++ b/utils/1password-secret/index.ts @@ -0,0 +1,36 @@ +import { Construct } from "constructs"; +import { Manifest } from "@cdktf/provider-kubernetes/lib/manifest"; +import { KubernetesProvider } from "@cdktf/provider-kubernetes/lib/provider"; + +type SecretOptions = { + provider: KubernetesProvider; + namespace: string; + name: string; + itemPath: string; +}; + +export class OnePasswordSecret extends Construct { + constructor(scope: Construct, id: string, options: SecretOptions) { + super(scope, id); + + const { itemPath, name, namespace, provider } = options; + + new Manifest(this, name, { + provider, + manifest: { + apiVersion: "onepassword.com/v1", + kind: "OnePasswordItem", + metadata: { + name, + namespace, + annotations: { + "operator.1password.io/auto-restart": "true", + }, + }, + spec: { + itemPath, + }, + }, + }); + } +} diff --git a/utils/cert-manager/index.ts b/utils/cert-manager/index.ts new file mode 100644 index 0000000..ea2e34b --- /dev/null +++ b/utils/cert-manager/index.ts @@ -0,0 +1,85 @@ +import { Construct } from "constructs"; +import { Manifest } from "@cdktf/provider-kubernetes/lib/manifest"; +import { KubernetesProvider } from "@cdktf/provider-kubernetes/lib/provider"; + +export interface CertificateOptions { + provider: KubernetesProvider; + + /** Namespace to create the Certificate in */ + namespace: string; + + /** Required name of the certificate (and CRD name) */ + name: string; + + /** Secret name for storing the issued TLS cert */ + secretName: string; + + /** One or more DNS names the certificate should cover */ + dnsNames: string[]; + + /** Reference to the cert-manager issuer */ + issuerRef: { + name: string; + kind?: string; // ClusterIssuer or Issuer + }; + + /** Optional duration (default: cert-manager default) */ + duration?: string; + + /** Optional renewBefore (default: cert-manager default) */ + renewBefore?: string; +} + +class Certificate extends Construct { + public readonly manifest: Manifest; + + constructor(scope: Construct, id: string, opts: CertificateOptions) { + super(scope, id); + + const manifest: any = { + apiVersion: "cert-manager.io/v1", + kind: "Certificate", + metadata: { + name: opts.name, + namespace: opts.namespace, + }, + spec: { + secretName: opts.secretName, + dnsNames: opts.dnsNames, + issuerRef: { + name: opts.issuerRef.name, + kind: opts.issuerRef.kind ?? "ClusterIssuer", + }, + }, + }; + + if (opts.duration) { + manifest.spec.duration = opts.duration; + } + + if (opts.renewBefore) { + manifest.spec.renewBefore = opts.renewBefore; + } + + this.manifest = new Manifest(this, id, { + provider: opts.provider, + manifest, + }); + } +} + +export class CloudflareCertificate extends Certificate { + constructor( + scope: Construct, + id: string, + opts: Omit, + ) { + super(scope, id, { + ...opts, + issuerRef: { + name: "cloudflare-issuer", + kind: "ClusterIssuer", + }, + }); + } +} diff --git a/utils/index.ts b/utils/index.ts new file mode 100644 index 0000000..30637ed --- /dev/null +++ b/utils/index.ts @@ -0,0 +1,3 @@ +export { CloudflareCertificate } from "./cert-manager"; +export { OnePasswordSecret } from "./1password-secret"; +export { IngressRoute } from "./traefik"; diff --git a/utils/traefik/index.ts b/utils/traefik/index.ts new file mode 100644 index 0000000..4c6181b --- /dev/null +++ b/utils/traefik/index.ts @@ -0,0 +1,2 @@ +export { IngressRoute } from "./ingress"; +export { IngressRouteTcp } from "./ingress-tcp"; diff --git a/utils/traefik/ingress-tcp.ts b/utils/traefik/ingress-tcp.ts new file mode 100644 index 0000000..ed81c06 --- /dev/null +++ b/utils/traefik/ingress-tcp.ts @@ -0,0 +1,71 @@ +import { Construct } from "constructs"; +import { Manifest } from "@cdktf/provider-kubernetes/lib/manifest"; +import { KubernetesProvider } from "@cdktf/provider-kubernetes/lib/provider"; + +export interface IngressRouteTcpOptions { + provider: KubernetesProvider; + + /** Namespace where the IngressRouteTCP will be created */ + namespace: string; + + /** EntryPoint name (e.g., "ssh", "mc25565", "postgres", etc.) */ + entryPoint: string; + + /** Backend service name */ + serviceName: string; + + /** Backend service port */ + servicePort: number; + + /** + * Match rule. + * Default is `HostSNI(\`*\`)` which is correct for most TCP services. + */ + match?: string; + + /** Name override (CR name) */ + name?: string; +} + +export class IngressRouteTcp extends Construct { + public readonly manifest: Manifest; + + constructor(scope: Construct, id: string, opts: IngressRouteTcpOptions) { + super(scope, id); + + const name = + opts.name ?? + `tcp-${opts.entryPoint}-${opts.serviceName}`.replace( + /[^a-zA-Z0-9-]/g, + "", + ); + + const matchRule = opts.match ?? "HostSNI(`*`)"; + + this.manifest = new Manifest(this, name, { + provider: opts.provider, + manifest: { + apiVersion: "traefik.io/v1alpha1", + kind: "IngressRouteTCP", + metadata: { + name, + namespace: opts.namespace, + }, + spec: { + entryPoints: [opts.entryPoint], + routes: [ + { + match: matchRule, + services: [ + { + name: opts.serviceName, + port: opts.servicePort, + }, + ], + }, + ], + }, + }, + }); + } +} diff --git a/traefik/ingress-route.ts b/utils/traefik/ingress.ts similarity index 92% rename from traefik/ingress-route.ts rename to utils/traefik/ingress.ts index 63de5ee..0f0fb80 100644 --- a/traefik/ingress-route.ts +++ b/utils/traefik/ingress.ts @@ -4,7 +4,7 @@ import { KubernetesProvider } from "@cdktf/provider-kubernetes/lib/provider"; import { CloudflareCertificate } from "../cert-manager"; -export interface TraefikIngressRouteOptions { +export interface IngressRouteOptions { provider: KubernetesProvider; namespace: string; @@ -31,10 +31,10 @@ export interface TraefikIngressRouteOptions { name?: string; } -export class TraefikIngressRoute extends Construct { +export class IngressRoute extends Construct { public readonly manifest: Manifest; - constructor(scope: Construct, id: string, opts: TraefikIngressRouteOptions) { + constructor(scope: Construct, id: string, opts: IngressRouteOptions) { super(scope, id); const name = opts.name ?? `route-${opts.host.replace(/\./g, "-")}`;