diff --git a/Chart.yaml b/Chart.yaml index aaa7279..a78c640 100644 --- a/Chart.yaml +++ b/Chart.yaml @@ -1,9 +1,11 @@ +--- apiVersion: v2 description: A Helm chart to build and deploy secrets using external-secrets for ansible-edge-gitops +home: https://github.com/validatedpatterns/aap-config-chart.git keywords: - pattern name: aap-config -version: 0.2.0 +version: 0.2.1 dependencies: - name: vp-rbac version: '0.1.*' diff --git a/README.md b/README.md index 2db22c1..14dbe84 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # aap-config -![Version: 0.2.0](https://img.shields.io/badge/Version-0.2.0-informational?style=flat-square) +![Version: 0.2.1](https://img.shields.io/badge/Version-0.2.1-informational?style=flat-square) A Helm chart to build and deploy secrets using external-secrets for ansible-edge-gitops @@ -27,6 +27,51 @@ namespaces from external secrets validation. To use this version, you will also need to update your pattern to use the `openshift-external-secrets-operator` and `openshift-external-secrets` helm chart. +* v0.2.1: Support credential (HTTPS or SSH) injection for git client in AGOF config +jobs. + +### VP-Secrets-v2 + +```yaml +--- +# NEVER COMMIT THESE VALUES TO GIT +version: "2.0" +secrets: + - name: aap-manifest + fields: + - name: b64content + path: 'full pathname of file containing Satellite Manifest for entitling Ansible Automation Platform' + base64: true + + - name: automation-hub-token + fields: + - name: token + value: 'An automation hub token for retrieving Certified and Validated Ansible content' + + # Optional + - name: agof-vault-file + fields: + - name: agof-vault-file + path: 'full pathname of a valid agof_vault file for secrets to overlay the iac config' + base64: true + + # Optional, if git auth is needed + - name: git-auth-secret + fields: + # HTTPS auth + - name: username + value: "Username to authenticate with" + - value: password + value: "Password to authenticate with" + # SSH auth + - name: .git-credentials + value: "git credentials" + - name: ssh-privatekey + value: "An ssh private key" + - name: known_hosts + value: "SSH known hosts for SSH authentication" +``` + ## Requirements | Repository | Name | Version | @@ -42,6 +87,9 @@ To use this version, you will also need to update your pattern to use the | agof.agof_revision | string | `"v2"` | | | agof.automationHubTokenKey | string | `"secret/data/hub/automation-hub-token"` | | | agof.extraPlaybookOpts | string | `""` | | +| agof.gitAuthHttpsStyle | string | `"auto"` | | +| agof.gitAuthSecret | string | `""` | | +| agof.gitAuthVaultKey | string | `""` | | | agof.iac_repo | string | `"https://github.com/validatedpatterns-demos/ansible-edge-gitops-hmi-config-as-code.git"` | | | agof.iac_revision | string | `"main"` | | | agof.vaultFileKey | string | `"secret/data/hub/agof-vault-file"` | | diff --git a/README.md.gotmpl b/README.md.gotmpl index 60b9a6d..d73423f 100644 --- a/README.md.gotmpl +++ b/README.md.gotmpl @@ -28,6 +28,51 @@ namespaces from external secrets validation. To use this version, you will also need to update your pattern to use the `openshift-external-secrets-operator` and `openshift-external-secrets` helm chart. +* v0.2.1: Support credential (HTTPS or SSH) injection for git client in AGOF config +jobs. + +### VP-Secrets-v2 + +```yaml +--- +# NEVER COMMIT THESE VALUES TO GIT +version: "2.0" +secrets: + - name: aap-manifest + fields: + - name: b64content + path: 'full pathname of file containing Satellite Manifest for entitling Ansible Automation Platform' + base64: true + + - name: automation-hub-token + fields: + - name: token + value: 'An automation hub token for retrieving Certified and Validated Ansible content' + + # Optional + - name: agof-vault-file + fields: + - name: agof-vault-file + path: 'full pathname of a valid agof_vault file for secrets to overlay the iac config' + base64: true + + # Optional, if git auth is needed + - name: git-auth-secret + fields: + # HTTPS auth + - name: username + value: "Username to authenticate with" + - value: password + value: "Password to authenticate with" + # SSH auth + - name: .git-credentials + value: "git credentials" + - name: ssh-privatekey + value: "An ssh private key" + - name: known_hosts + value: "SSH known hosts for SSH authentication" +``` + {{ template "chart.homepageLine" . }} {{ template "chart.maintainersSection" . }} diff --git a/templates/_helpers.tpl b/templates/_helpers.tpl index 40de043..b7bbad9 100644 --- a/templates/_helpers.tpl +++ b/templates/_helpers.tpl @@ -8,6 +8,11 @@ volumes: - name: agof-vault-file secret: secretName: agof-vault-file +{{- if $.Values.agof.gitAuthSecret }} + - name: agof-git-auth + secret: + secretName: {{ $.Values.agof.gitAuthSecret | quote }} +{{- end }} initContainers: - name: agof-init image: {{ .Values.configJob.image }} @@ -19,15 +24,119 @@ initContainers: command: - /bin/bash - -c - - > - base64 -d /pattern-home/agof-vault-file/agof-vault-file > ~/agof_vault.yml && - git clone --recurse-submodules --single-branch --branch "{{ .Values.agof.agof_revision }}" - -- "{{ .Values.agof.agof_repo }}" /pattern-home/agof_repo + - | + set -euo pipefail + export GIT_TERMINAL_PROMPT=0 + agof_repo_url={{ $.Values.agof.agof_repo | quote }} + iac_repo_url={{ $.Values.agof.iac_repo | quote }} + base64 -d /pattern-home/agof-vault-file/agof-vault-file > ~/agof_vault.yml +{{- if $.Values.agof.gitAuthSecret }} + GIT_AUTH_DIR=/pattern-home/git-auth + if [[ -f "$GIT_AUTH_DIR/.git-credentials" ]]; then + cp "$GIT_AUTH_DIR/.git-credentials" ~/.git-credentials + chmod 600 ~/.git-credentials + git config --global credential.helper store + elif [[ -f "$GIT_AUTH_DIR/ssh-privatekey" ]]; then + mkdir -p ~/.ssh + cp "$GIT_AUTH_DIR/ssh-privatekey" ~/.ssh/id_rsa + chmod 600 ~/.ssh/id_rsa + if [[ -f "$GIT_AUTH_DIR/known_hosts" ]]; then + cp "$GIT_AUTH_DIR/known_hosts" ~/.ssh/known_hosts + chmod 644 ~/.ssh/known_hosts + export GIT_SSH_COMMAND='ssh -i ~/.ssh/id_rsa -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes' + else + export GIT_SSH_COMMAND='ssh -i ~/.ssh/id_rsa -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new' + fi + else + agof_git_host_from_url() { + local u="$1" + if [[ "$u" =~ ^https?://([^/@]+) ]]; then + echo "${BASH_REMATCH[1]}" + elif [[ "$u" =~ ^https?://[^@]+@([^/]+) ]]; then + echo "${BASH_REMATCH[1]}" + elif [[ "$u" =~ ^git@([^:]+): ]]; then + echo "${BASH_REMATCH[1]}" + elif [[ "$u" =~ ^ssh://[^@]+@([^/:]+) ]]; then + echo "${BASH_REMATCH[1]}" + fi + } + agof_unique_git_hosts() { + local url h + for url in "$@"; do + [[ -z "$url" ]] && continue + h=$(agof_git_host_from_url "$url") || true + [[ -n "$h" ]] && printf '%s\n' "$h" + done | awk '!x[$0]++' + } + agof_https_store_credential() { + local host="$1" user="$2" pass="$3" + git config --global credential.helper store + printf 'protocol=https\nhost=%s\nusername=%s\npassword=%s\n\n' "$host" "$user" "$pass" | git credential approve + } + agof_https_user_for_style() { + local host_lc style + host_lc=$(echo "$1" | tr '[:upper:]' '[:lower:]') + style="$2" + case "$style" in + github) printf '%s' 'git' ;; + gitlab) printf '%s' 'oauth2' ;; + gitea) printf '%s' 'oauth2' ;; + auto) + if [[ "$host_lc" == *"github.com"* ]]; then printf '%s' 'git' + elif [[ "$host_lc" == *"gitlab"* ]]; then printf '%s' 'oauth2' + elif [[ "$host_lc" == *"gitea"* ]] || [[ "$host_lc" == *"forgejo"* ]] || [[ "$host_lc" == *"codeberg"* ]]; then printf '%s' 'oauth2' + else printf '%s' 'git' + fi + ;; + *) printf '%s' 'git' ;; + esac + } + https_style={{ default "auto" $.Values.agof.gitAuthHttpsStyle | quote }} + if [[ -f "$GIT_AUTH_DIR/username" ]] && { [[ -f "$GIT_AUTH_DIR/password" ]] || [[ -f "$GIT_AUTH_DIR/token" ]]; }; then + u=$(cat "$GIT_AUTH_DIR/username") + if [[ -f "$GIT_AUTH_DIR/token" ]]; then + p=$(cat "$GIT_AUTH_DIR/token") + else + p=$(cat "$GIT_AUTH_DIR/password") + fi + host_any="" + while IFS= read -r host; do + [[ -z "$host" ]] && continue + host_any=1 + agof_https_store_credential "$host" "$u" "$p" + done < <(agof_unique_git_hosts "$agof_repo_url" "$iac_repo_url") + if [[ -z "$host_any" ]]; then + echo "agof.gitAuthSecret: could not parse git host from agof_repo or iac_repo for HTTPS credentials" >&2 + exit 1 + fi + elif [[ -f "$GIT_AUTH_DIR/token" ]] && [[ ! -f "$GIT_AUTH_DIR/username" ]]; then + p=$(cat "$GIT_AUTH_DIR/token") + host_any="" + while IFS= read -r host; do + [[ -z "$host" ]] && continue + host_any=1 + u="$(agof_https_user_for_style "$host" "$https_style")" + agof_https_store_credential "$host" "$u" "$p" + done < <(agof_unique_git_hosts "$agof_repo_url" "$iac_repo_url") + if [[ -z "$host_any" ]]; then + echo "agof.gitAuthSecret: could not parse git host from agof_repo or iac_repo for HTTPS token" >&2 + exit 1 + fi + fi + fi +{{- end }} + git clone --recurse-submodules --single-branch --branch "{{ $.Values.agof.agof_revision }}" \ + -- "$agof_repo_url" /pattern-home/agof_repo volumeMounts: - name: agof-scratch-space mountPath: /pattern-home - name: agof-vault-file mountPath: /pattern-home/agof-vault-file +{{- if $.Values.agof.gitAuthSecret }} + - name: agof-git-auth + mountPath: /pattern-home/git-auth + readOnly: true +{{- end }} containers: - name: agof-config image: {{ .Values.configJob.image }} diff --git a/templates/external-secrets-validation-job.yaml b/templates/external-secrets-validation-job.yaml index 931b335..cebe43c 100644 --- a/templates/external-secrets-validation-job.yaml +++ b/templates/external-secrets-validation-job.yaml @@ -32,6 +32,9 @@ spec: "aap-manifest" "agof-vault-file" "automation-hub-token" +{{- if and $.Values.agof.gitAuthSecret $.Values.agof.gitAuthVaultKey }} + "{{ $.Values.agof.gitAuthSecret }}" +{{- end }} ) # Function to check if a secret exists and has data diff --git a/templates/secret-agof-gitauth.yaml b/templates/secret-agof-gitauth.yaml new file mode 100644 index 0000000..aadc26b --- /dev/null +++ b/templates/secret-agof-gitauth.yaml @@ -0,0 +1,22 @@ +{{- if and $.Values.agof.gitAuthVaultKey (not $.Values.agof.gitAuthSecret) -}} +{{- fail "agof.gitAuthVaultKey requires agof.gitAuthSecret (Kubernetes Secret / ExternalSecret name)" -}} +{{- end }} +{{- if and $.Values.agof.gitAuthSecret $.Values.agof.gitAuthVaultKey }} +--- +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: {{ $.Values.agof.gitAuthSecret }} + annotations: + argocd.argoproj.io/sync-wave: "1" +spec: + refreshInterval: 15s + secretStoreRef: + name: {{ $.Values.secretStore.name }} + kind: {{ $.Values.secretStore.kind }} + target: + name: {{ $.Values.agof.gitAuthSecret }} + dataFrom: + - extract: + key: {{ $.Values.agof.gitAuthVaultKey }} +{{- end }} diff --git a/values.yaml b/values.yaml index 0f94b85..734faaa 100644 --- a/values.yaml +++ b/values.yaml @@ -13,6 +13,21 @@ agof: agof_repo: https://github.com/validatedpatterns/agof.git agof_revision: v2 + # Optional: name of an existing Secret (same namespace) whose keys are mounted for + # git in the agof-init container (clone agof_repo) and the agof-config container + # (e.g. clone/fetch iac_repo during make). Use one of: + # - .git-credentials: full git-credential-store line(s) for HTTPS + # - ssh-privatekey (+ optional known_hosts): SSH private key (e.g. kubernetes.io/ssh-auth) + # - HTTPS: username + (password or token), or token-only with gitAuthHttpsStyle below + gitAuthSecret: '' + # Optional Vault path: when set (with gitAuthSecret), an ExternalSecret is created that uses + # dataFrom.extract to copy all fields from that Vault secret into the Kubernetes Secret named + # gitAuthSecret (same pattern as vaultFileKey / agof-vault-file). + gitAuthVaultKey: '' + # When the Secret has only a "token" key: how to pick the HTTPS username (PAT / OAuth). + # auto: from each repo URL host (github.com -> git; gitlab / gitea / forgejo / codeberg -> oauth2) + # github|gitlab|gitea: force that platform's usual clone user (git / oauth2 / oauth2) + gitAuthHttpsStyle: auto iac_repo: https://github.com/validatedpatterns-demos/ansible-edge-gitops-hmi-config-as-code.git iac_revision: main