From 53a090d3f5d23887e3786e8e45092c21db637ffd Mon Sep 17 00:00:00 2001 From: jenkins Date: Thu, 13 Aug 2026 19:33:03 -0300 Subject: [PATCH] gitea: add Atlas contributor identities --- .../gitea/atlas-identity-bootstrap-job.yaml | 86 ++++++++ services/gitea/kustomization.yaml | 9 + .../scripts/gitea_atlas_identity_ensure.sh | 200 ++++++++++++++++++ 3 files changed, 295 insertions(+) create mode 100644 services/gitea/atlas-identity-bootstrap-job.yaml create mode 100644 services/gitea/scripts/gitea_atlas_identity_ensure.sh diff --git a/services/gitea/atlas-identity-bootstrap-job.yaml b/services/gitea/atlas-identity-bootstrap-job.yaml new file mode 100644 index 000000000..507774202 --- /dev/null +++ b/services/gitea/atlas-identity-bootstrap-job.yaml @@ -0,0 +1,86 @@ +# services/gitea/atlas-identity-bootstrap-job.yaml +apiVersion: batch/v1 +kind: Job +metadata: + name: gitea-atlas-identity-bootstrap-1 + namespace: gitea + labels: + app.kubernetes.io/name: gitea + app.kubernetes.io/component: identity-bootstrap +spec: + backoffLimit: 4 + template: + metadata: + labels: + app.kubernetes.io/name: gitea + app.kubernetes.io/component: identity-bootstrap + spec: + restartPolicy: Never + serviceAccountName: gitea-vault + securityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + seccompProfile: + type: RuntimeDefault + affinity: + podAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchLabels: + app: gitea + topologyKey: kubernetes.io/hostname + nodeSelector: + node-role.kubernetes.io/worker: "true" + hardware: rpi5 + containers: + - name: bootstrap + image: gitea/gitea:1.23 + imagePullPolicy: IfNotPresent + command: + - /bin/sh + - /opt/bootstrap/gitea_atlas_identity_ensure.sh + env: + - name: HOME + value: /data/git + - name: USER + value: git + - name: GITEA_WORK_DIR + value: /data/gitea + - name: GITEA_APP_INI + value: /data/gitea/conf/app.ini + - name: GITEA_API_URL + value: http://gitea.gitea.svc.cluster.local:3000/api/v1 + - name: GITEA_PUBLIC_URL + value: https://scm.bstein.dev + - name: VAULT_ADDR + value: http://vault.vault.svc.cluster.local:8200 + - name: VAULT_ROLE + value: gitea + resources: + requests: + cpu: 25m + memory: 64Mi + limits: + cpu: 250m + memory: 256Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + volumeMounts: + - name: data + mountPath: /data + - name: bootstrap + mountPath: /opt/bootstrap + readOnly: true + volumes: + - name: data + persistentVolumeClaim: + claimName: gitea-data + - name: bootstrap + configMap: + name: gitea-atlas-identity-bootstrap + defaultMode: 0555 diff --git a/services/gitea/kustomization.yaml b/services/gitea/kustomization.yaml index d42793161..8c473a385 100644 --- a/services/gitea/kustomization.yaml +++ b/services/gitea/kustomization.yaml @@ -9,3 +9,12 @@ resources: - deployment.yaml - service.yaml - ingress.yaml + - atlas-identity-bootstrap-job.yaml + +configMapGenerator: + - name: gitea-atlas-identity-bootstrap + files: + - gitea_atlas_identity_ensure.sh=scripts/gitea_atlas_identity_ensure.sh + +generatorOptions: + disableNameSuffixHash: true diff --git a/services/gitea/scripts/gitea_atlas_identity_ensure.sh b/services/gitea/scripts/gitea_atlas_identity_ensure.sh new file mode 100644 index 000000000..554f09c8d --- /dev/null +++ b/services/gitea/scripts/gitea_atlas_identity_ensure.sh @@ -0,0 +1,200 @@ +#!/bin/sh +set -eu + +app_ini="${GITEA_APP_INI:-/data/gitea/conf/app.ini}" +work_dir="${GITEA_WORK_DIR:-/data/gitea}" +api_url="${GITEA_API_URL:-http://gitea.gitea.svc.cluster.local:3000/api/v1}" +public_url="${GITEA_PUBLIC_URL:-https://scm.bstein.dev}" +vault_addr="${VAULT_ADDR:-http://vault.vault.svc.cluster.local:8200}" +vault_role="${VAULT_ROLE:-gitea}" +service_account_token=/var/run/secrets/kubernetes.io/serviceaccount/token + +reconciler_user=atlas-reconciler +reconciler_email=atlas-reconciler@bstein.dev +hermes_user=hermes-automation +hermes_email=hermes-automation@bstein.dev +organization=atlas +owner_user=bstein +contributor_team=Contributors + +die() { + echo "Gitea Atlas identity bootstrap failed: $*" >&2 + exit 1 +} + +gitea_admin() { + /usr/local/bin/gitea --work-path "${work_dir}" --config "${app_ini}" admin "$@" +} + +wait_for_gitea() { + attempt=0 + while [ "${attempt}" -lt 120 ]; do + if [ -r "${app_ini}" ] && curl --fail --silent --show-error "${api_url}/version" >/dev/null 2>&1; then + return 0 + fi + attempt=$((attempt + 1)) + sleep 2 + done + die "Gitea did not become ready" +} + +user_exists() { + wanted_user=$1 + gitea_admin user list 2>/dev/null | awk 'NR > 1 { print $2 }' | grep -Fxq "${wanted_user}" +} + +ensure_user() { + username=$1 + email=$2 + admin_flag=$3 + + if user_exists "${username}"; then + return 0 + fi + + if [ "${admin_flag}" = true ]; then + gitea_admin user create \ + --username "${username}" \ + --email "${email}" \ + --random-password \ + --must-change-password=false \ + --admin >/dev/null + else + gitea_admin user create \ + --username "${username}" \ + --email "${email}" \ + --random-password \ + --must-change-password=false >/dev/null + fi +} + +generate_token() { + username=$1 + token_name="atlas-bootstrap-$(date +%s)-${username}" + gitea_admin user generate-access-token \ + --username "${username}" \ + --token-name "${token_name}" \ + --scopes all \ + --raw 2>/dev/null +} + +api_request() { + method=$1 + path=$2 + data=${3:-} + response_file=$4 + + if [ -n "${data}" ]; then + status=$(curl --silent --show-error \ + --output "${response_file}" \ + --write-out '%{http_code}' \ + --request "${method}" \ + --header "Authorization: token ${reconciler_token}" \ + --header 'Content-Type: application/json' \ + --data "${data}" \ + "${api_url}${path}") + else + status=$(curl --silent --show-error \ + --output "${response_file}" \ + --write-out '%{http_code}' \ + --request "${method}" \ + --header "Authorization: token ${reconciler_token}" \ + "${api_url}${path}") + fi + printf '%s' "${status}" +} + +expect_status() { + actual=$1 + expected=$2 + description=$3 + [ "${actual}" = "${expected}" ] || die "${description} returned HTTP ${actual}" +} + +find_team_id() { + team_name=$1 + teams_file=$(mktemp) + status=$(api_request GET "/orgs/${organization}/teams" '' "${teams_file}") + expect_status "${status}" 200 "list organization teams" + team_id=$(sed 's/},{/}\ +{/g' "${teams_file}" | grep "\"name\":\"${team_name}\"" | head -n 1 | grep -o '"id":[0-9]*' | head -n 1 | cut -d: -f2 || true) + rm -f "${teams_file}" + [ -n "${team_id}" ] || die "team ${team_name} was not found" + printf '%s' "${team_id}" +} + +vault_login() { + [ -r "${service_account_token}" ] || die "Kubernetes service account token is unavailable" + jwt=$(cat "${service_account_token}") + login_file=$(mktemp) + status=$(curl --silent --show-error \ + --output "${login_file}" \ + --write-out '%{http_code}' \ + --request POST \ + --header 'Content-Type: application/json' \ + --data "{\"role\":\"${vault_role}\",\"jwt\":\"${jwt}\"}" \ + "${vault_addr}/v1/auth/kubernetes/login") + expect_status "${status}" 200 "Vault Kubernetes login" + vault_token=$(sed -n 's/.*"client_token":"\([^"]*\)".*/\1/p' "${login_file}") + rm -f "${login_file}" + [ -n "${vault_token}" ] || die "Vault login did not return a client token" +} + +vault_write() { + path=$1 + payload=$2 + response_file=$(mktemp) + status=$(curl --silent --show-error \ + --output "${response_file}" \ + --write-out '%{http_code}' \ + --request POST \ + --header "X-Vault-Token: ${vault_token}" \ + --header 'Content-Type: application/json' \ + --data "${payload}" \ + "${vault_addr}/v1/kv/data/atlas/${path}") + rm -f "${response_file}" + expect_status "${status}" 200 "write Vault path ${path}" +} + +wait_for_gitea +ensure_user "${reconciler_user}" "${reconciler_email}" true +ensure_user "${hermes_user}" "${hermes_email}" false + +reconciler_token=$(generate_token "${reconciler_user}") +hermes_token=$(generate_token "${hermes_user}") +[ -n "${reconciler_token}" ] || die "reconciler token generation failed" +[ -n "${hermes_token}" ] || die "Hermes token generation failed" + +response_file=$(mktemp) +status=$(api_request POST /orgs "{\"username\":\"${organization}\",\"full_name\":\"Atlas\",\"description\":\"Atlas platform services\",\"visibility\":\"private\",\"repo_admin_change_team_access\":true}" "${response_file}") +rm -f "${response_file}" +case "${status}" in + 201|422) ;; + *) die "create organization returned HTTP ${status}" ;; +esac + +owners_id=$(find_team_id Owners) +response_file=$(mktemp) +status=$(api_request PUT "/teams/${owners_id}/members/${owner_user}" '' "${response_file}") +rm -f "${response_file}" +expect_status "${status}" 204 "add ${owner_user} to Atlas owners" + +response_file=$(mktemp) +status=$(api_request POST "/orgs/${organization}/teams" "{\"name\":\"${contributor_team}\",\"description\":\"Atlas automation contributors\",\"permission\":\"write\",\"includes_all_repositories\":true,\"can_create_org_repo\":false}" "${response_file}") +rm -f "${response_file}" +case "${status}" in + 201|422) ;; + *) die "create contributor team returned HTTP ${status}" ;; +esac + +contributors_id=$(find_team_id "${contributor_team}") +response_file=$(mktemp) +status=$(api_request PUT "/teams/${contributors_id}/members/${hermes_user}" '' "${response_file}") +rm -f "${response_file}" +expect_status "${status}" 204 "add Hermes to Atlas contributors" + +vault_login +vault_write gitea/atlas-reconciler "{\"data\":{\"username\":\"${reconciler_user}\",\"token\":\"${reconciler_token}\",\"base_url\":\"${public_url}\"}}" +vault_write hermes/developer-gitea "{\"data\":{\"username\":\"${hermes_user}\",\"email\":\"${hermes_email}\",\"token\":\"${hermes_token}\",\"base_url\":\"${public_url}\"}}" + +echo "Atlas organization and Hermes contributor identity are ready"