Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -1,17 +1,29 @@
name: Test

on:
# develop itself, so the state after a merge is tested
push:
branches: [ develop ]
# Pull requests targeting develop. This also covers every push to the branch of an open
# pull request, through the `synchronize` event, so branches without one are not tested.
pull_request:
branches: [ develop ]

# Supersede in-flight runs of the same branch or pull request
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
test:
runs-on: ubuntu-latest

steps:
# On a pull request, check out the branch as it was pushed. The default is the merge
# commit with the base branch, which tests a tree that exists nowhere else.
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}

- uses: actions/setup-python@v5
with:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
"firstBrokerLoginFlowAlias": "first broker login",
"config": {
"syncMode": "IMPORT",
"clientSecret": {{ .app.harness.secrets.github_clientSecret | default "<github_clientSecret>" | quote }},
"clientId": {{ .app.harness.secrets.github_clientId | default "<github_clientId>" | quote }},
"clientSecret": {{ include "deploy_utils.secretValue" (dict "spec" .app.harness.secrets.github_clientSecret) | default "<github_clientSecret>" | quote }},
"clientId": {{ include "deploy_utils.secretValue" (dict "spec" .app.harness.secrets.github_clientId) | default "<github_clientId>" | quote }},
"useJwksUrl": "true"
}
}
Expand All @@ -36,8 +36,8 @@
"firstBrokerLoginFlowAlias": "first broker login",
"config": {
"syncMode": "IMPORT",
"clientSecret": {{ .app.harness.secrets.google_clientSecret | default "<google_clientSecret>" | quote }},
"clientId": {{ .app.harness.secrets.google_clientId | default "<google_clientId>" | quote }},
"clientSecret": {{ include "deploy_utils.secretValue" (dict "spec" .app.harness.secrets.google_clientSecret) | default "<google_clientSecret>" | quote }},
"clientId": {{ include "deploy_utils.secretValue" (dict "spec" .app.harness.secrets.google_clientId) | default "<google_clientId>" | quote }},
"useJwksUrl": "true"
}
}
Expand Down
51 changes: 51 additions & 0 deletions deployment-configuration/compose/templates/_secrets.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{{/*
Secret definition helpers, shared with the helm chart.

Secret managers do not exist in a local docker compose deployment: only the plain value of
a secret definition is resolved here. Keep in sync with the reference implementation in
`deployment-configuration/helm/templates/secrets/_secrets.tpl`, which also documents the rich form.
*/}}

{{/*
Resolve the manager of a secret definition.
Outputs `cloudharness`, `unmanaged` or the manager name.
Usage: {{ include "deploy_utils.secretManager" (dict "spec" $secretDefinition) }}
*/}}
{{- define "deploy_utils.secretManager" -}}
{{- $manager := "cloudharness" -}}
{{- if kindIs "map" .spec -}}
{{- if hasKey .spec "manager" -}}
{{- $declared := get .spec "manager" -}}
{{- if kindIs "invalid" $declared -}}
{{- $manager = "unmanaged" -}}
{{- else -}}
{{- if eq (toString $declared) "" -}}
{{- $manager = "unmanaged" -}}
{{- else -}}
{{- $manager = toString $declared -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- $manager -}}
{{- end -}}

{{/*
Resolve the value of a secret definition: the definition itself in the simple form,
the `default` entry in the rich form. Empty when not defined.
Usage: {{ include "deploy_utils.secretValue" (dict "spec" $secretDefinition) }}
*/}}
{{- define "deploy_utils.secretValue" -}}
{{- if kindIs "map" .spec -}}
{{- if hasKey .spec "default" -}}
{{- $default := get .spec "default" -}}
{{- if not (kindIs "invalid" $default) -}}
{{- $default -}}
{{- end -}}
{{- end -}}
{{- else -}}
{{- if not (kindIs "invalid" .spec) -}}
{{- .spec -}}
{{- end -}}
{{- end -}}
{{- end -}}
14 changes: 10 additions & 4 deletions deployment-configuration/compose/templates/auto-secrets.yaml
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
{{- define "deploy_utils.secret" }}
{{- if .app.harness.secrets }}
{{/* Secret managers are not available locally: every secret is resolved to its literal
value, or to the `default` entry when defined in the rich form. */}}
{{- $resolved := dict }}
{{- range $k, $v := .app.harness.secrets }}
{{- $_ := set $resolved $k (include "deploy_utils.secretValue" (dict "spec" $v)) }}
{{- end }}

{{- $secret_name := printf "%s" .app.harness.deployment.name }}
{{- $secret := (lookup "v1" "Secret" .root.Values.namespace $secret_name) }}
{{- if $secret }}
# secret already exists
{{- if not (compact (values .app.harness.secrets)) }}
{{- if not (compact (values $resolved)) }}
# secret values are null, copy from the existing secret
{{- range $k, $v := $secret.data }}
cloudharness-metadata:
Expand All @@ -17,7 +23,7 @@ data: {{ $v }}
{{- else }}
# there are non default values in values.yaml, use these
stringData:
{{- range $k, $v := .app.harness.secrets }}
{{- range $k, $v := $resolved }}
cloudharness-metadata:
path: resources/generated/auth/{{ $k }}

Expand All @@ -28,7 +34,7 @@ data: {{ $v | default (randAlphaNum 20) }}
{{- else }}
# secret doesn't exist
stringData:
{{- range $k, $v := .app.harness.secrets }}
{{- range $k, $v := $resolved }}
cloudharness-metadata:
path: resources/generated/auth/{{ $k }}
data: {{ $v | default (randAlphaNum 20) }}
Expand All @@ -46,4 +52,4 @@ data: {{ $v | default (randAlphaNum 20) }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
8 changes: 2 additions & 6 deletions deployment-configuration/helm/templates/auto-deployments.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -147,9 +147,7 @@ spec:
{{- range $dep := concat .app.harness.dependencies.hard .app.harness.dependencies.soft }}
{{- $depApp := index $root.Values.apps $dep }}
{{- if $depApp.harness.secrets }}
- name: cloudharness-{{ $dep }}
secret:
secretName: {{ $dep }}
{{- include "deploy_utils.secretsVolume" (dict "root" $root "app" $depApp "name" (printf "cloudharness-%s" $dep) "secretName" $dep) | nindent 8 }}
{{- end }}
{{- end }}
{{- if .app.harness.deployment.volume }}
Expand All @@ -164,9 +162,7 @@ spec:
name: "{{ $app.harness.deployment.name }}-{{ $resource.name }}"
{{- end }}
{{- if .app.harness.secrets }}
- name: secrets
secret:
secretName: {{ .app.harness.deployment.name }}
{{- include "deploy_utils.secretsVolume" (dict "root" .root "app" .app "name" "secrets" "secretName" .app.harness.deployment.name) | nindent 8 }}
{{- end }}
{{- if kindIs "map" .app.harness.database }}
{{- if and (hasKey .app.harness.database "connect_string") .app.harness.database.connect_string }}
Expand Down
223 changes: 223 additions & 0 deletions deployment-configuration/helm/templates/secrets/_secrets.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
{{/* vim: set filetype=mustache: */}}

{{/*
Secret managers framework.

An application secret is defined either in the simple form

harness:
secrets:
mySecret: "a value"

or in the rich form

harness:
secrets:
mySecret:
manager: onepassword
default: "a value"
# ... manager specific fields

The manager is resolved as follows:
* no `manager` key, or `manager: cloudharness` -> `cloudharness` (the built-in behaviour:
the value is written to the application secret, random values are generated when needed)
* `manager:` (explicit null or empty) -> `unmanaged`: CloudHarness renders nothing, the
secret entry is expected to be created out of band (e.g. `kubectl edit secret <app>`)
* any other value -> the named secret manager, which is responsible for materializing a
Kubernetes Secret holding the value.

A secret manager named `X` is implemented by defining two templates:

* `deploy_utils.secretmanager.X.resource`: renders the Kubernetes resources needed to
materialize the secret (e.g. a `OnePasswordItem` or an `ExternalSecret`).
* `deploy_utils.secretmanager.X.ref`: outputs `<kubernetes secret name>/<key>`, telling
CloudHarness where the value ends up so that it can be mounted with the application secrets.

Both are called with the context

(dict "root" $ "app" $app "name" <secret name> "spec" <secret definition> "resourceName" <sanitized name>)

where `spec` is the secret definition, holding the manager specific settings, and
`resourceName` is a name safe to give to the resources materializing the secret.
`deploy_utils.secretManagerSetting` reads a setting from the definition, falling back to
the manager's deployment wide configuration under `secretmanagers.X` in the root values.

The manager is looked up by name at render time, so an unknown manager fails with
`no template "deploy_utils.secretmanager.X.resource"`.

One file per manager lives in `managers/`, each documenting its cluster prerequisites, its
settings and what it renders:

* `managers/onepassword.tpl`: 1Password, through the 1Password Kubernetes Operator
* `managers/aws.tpl`: AWS Secrets Manager, through the External Secrets Operator

Managers can equally be added by any application: templates under
`<application>/deploy/templates` are collected into the same chart and therefore share the
same template namespace.
*/}}

{{/*
Resolve the manager of a secret definition.
Outputs `cloudharness`, `unmanaged` or the manager name.
Usage: {{ include "deploy_utils.secretManager" (dict "spec" $secretDefinition) }}
*/}}
{{- define "deploy_utils.secretManager" -}}
{{- $manager := "cloudharness" -}}
{{- if kindIs "map" .spec -}}
{{- if hasKey .spec "manager" -}}
{{- $declared := get .spec "manager" -}}
{{- if kindIs "invalid" $declared -}}
{{- $manager = "unmanaged" -}}
{{- else -}}
{{- if eq (toString $declared) "" -}}
{{- $manager = "unmanaged" -}}
{{- else -}}
{{- $manager = toString $declared -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- $manager -}}
{{- end -}}

{{/*
Tells whether a secret definition is handled by an external secret manager.
Outputs `true` or the empty string.
Usage: {{ if include "deploy_utils.secretIsExternal" (dict "spec" $secretDefinition) }}
*/}}
{{- define "deploy_utils.secretIsExternal" -}}
{{- if not (has (include "deploy_utils.secretManager" (dict "spec" .spec)) (list "cloudharness" "unmanaged")) -}}
true
{{- end -}}
{{- end -}}

{{/*
Resolve the value of a secret definition: the definition itself in the simple form,
the `default` entry in the rich form. Empty when not defined.
Usage: {{ include "deploy_utils.secretValue" (dict "spec" $secretDefinition) }}
*/}}
{{- define "deploy_utils.secretValue" -}}
{{- if kindIs "map" .spec -}}
{{- if hasKey .spec "default" -}}
{{- $default := get .spec "default" -}}
{{- if not (kindIs "invalid" $default) -}}
{{- $default -}}
{{- end -}}
{{- end -}}
{{- else -}}
{{- if not (kindIs "invalid" .spec) -}}
{{- .spec -}}
{{- end -}}
{{- end -}}
{{- end -}}

{{/*
Tells whether an application has secrets handled by CloudHarness itself, i.e. whether
CloudHarness creates the application secret.
Outputs `true` or the empty string.
Usage: {{ if include "deploy_utils.hasManagedSecrets" (dict "app" $app) }}
*/}}
{{- define "deploy_utils.hasManagedSecrets" -}}
{{- $managed := "" -}}
{{- range $name, $spec := .app.harness.secrets -}}
{{- if eq (include "deploy_utils.secretManager" (dict "spec" $spec)) "cloudharness" -}}
{{- $managed = "true" -}}
{{- end -}}
{{- end -}}
{{- $managed -}}
{{- end -}}

{{/*
Name of the Kubernetes resource materializing an externally managed secret.
Usage: {{ include "deploy_utils.secretResourceName" (dict "app" $app "name" $secretName) }}
*/}}
{{- define "deploy_utils.secretResourceName" -}}
{{- printf "%s-%s" .app.harness.deployment.name .name | lower | replace "_" "-" | replace "." "-" | trunc 63 | trimSuffix "-" -}}
{{- end -}}

{{/*
Look up a manager setting, first in the secret definition, then in the manager global
configuration, falling back to the given default.
Usage: {{ include "deploy_utils.secretManagerSetting" (dict "spec" $spec "conf" $conf "key" "store" "default" "") }}
*/}}
{{- define "deploy_utils.secretManagerSetting" -}}
{{- $value := .default -}}
{{- if kindIs "map" .conf -}}
{{- if hasKey .conf .key -}}
{{- $declared := index .conf .key -}}
{{- if not (kindIs "invalid" $declared) -}}
{{- $value = $declared -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- if kindIs "map" .spec -}}
{{- if hasKey .spec .key -}}
{{- $declared := index .spec .key -}}
{{- if not (kindIs "invalid" $declared) -}}
{{- $value = $declared -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- $value -}}
{{- end -}}

{{/*
Render the volume exposing the secrets of an application as files.
All secrets, whatever their manager, are exposed in the same directory, so that
`cloudharness.utils.secrets.get_secret` finds them by name.
Usage: {{ include "deploy_utils.secretsVolume" (dict "root" $root "app" $app "name" "secrets" "secretName" $app.harness.deployment.name) }}
*/}}
{{- define "deploy_utils.secretsVolume" -}}
{{- $root := .root -}}
{{- $app := .app -}}
{{- $managed := include "deploy_utils.hasManagedSecrets" (dict "app" $app) -}}
{{- $external := list -}}
{{- range $name, $spec := $app.harness.secrets -}}
{{- if include "deploy_utils.secretIsExternal" (dict "spec" $spec) -}}
{{- $manager := include "deploy_utils.secretManager" (dict "spec" $spec) -}}
{{- $context := dict "root" $root "app" $app "name" $name "spec" $spec "resourceName" (include "deploy_utils.secretResourceName" (dict "app" $app "name" $name)) -}}
{{- $ref := splitList "/" (include (printf "deploy_utils.secretmanager.%s.ref" $manager) $context) -}}
{{- $external = append $external (dict "path" $name "secretName" (first $ref) "key" (last $ref)) -}}
{{- end -}}
{{- end -}}
- name: {{ .name }}
{{- if $external }}
projected:
sources:
- secret:
name: {{ .secretName }}
{{- if not $managed }}
optional: true
{{- end }}
{{- range $source := $external }}
- secret:
name: {{ $source.secretName }}
items:
- key: {{ $source.key }}
path: {{ $source.path }}
{{- end }}
{{- else }}
secret:
secretName: {{ .secretName }}
{{- if not $managed }}
optional: true
{{- end }}
{{- end }}
{{- end -}}

{{/*
Render the resources materializing the externally managed secrets of an application.
Usage: {{ include "deploy_utils.secretManagerResources" (dict "root" $root "app" $app) }}
*/}}
{{- define "deploy_utils.secretManagerResources" -}}
{{- $root := .root -}}
{{- $app := .app -}}
{{- range $name, $spec := .app.harness.secrets }}
{{- if include "deploy_utils.secretIsExternal" (dict "spec" $spec) }}
{{- $manager := include "deploy_utils.secretManager" (dict "spec" $spec) }}
{{- $context := dict "root" $root "app" $app "name" $name "spec" $spec "resourceName" (include "deploy_utils.secretResourceName" (dict "app" $app "name" $name)) }}
---
{{ include (printf "deploy_utils.secretmanager.%s.resource" $manager) $context }}
{{- end }}
{{- end }}
{{- end -}}
Loading
Loading