Add Veles deployment IaC

This commit is contained in:
jenkins 2026-06-27 07:35:25 -03:00
parent 87e8146d5f
commit a1139cd3f6
17 changed files with 252 additions and 414 deletions

View File

@ -28,33 +28,3 @@ spec:
- veles-postgres
retain: 8
concurrency: 1
---
apiVersion: longhorn.io/v1beta2
kind: RecurringJob
metadata:
name: veles-artifacts-backup
namespace: longhorn-system
spec:
name: veles-artifacts-backup
cron: "45 5 * * *"
task: backup
groups:
- veles
- veles-artifacts
retain: 7
concurrency: 1
---
apiVersion: longhorn.io/v1beta2
kind: RecurringJob
metadata:
name: veles-artifacts-snapshot
namespace: longhorn-system
spec:
name: veles-artifacts-snapshot
cron: "15 */6 * * *"
task: snapshot
groups:
- veles
- veles-artifacts
retain: 8
concurrency: 1

View File

@ -15,7 +15,6 @@ parameters:
fsType: ext4
replicaAutoBalance: disabled
dataLocality: strict-local
recurringJobSelector: '[{"name":"veles-artifacts-backup","isGroup":false},{"name":"veles-artifacts-snapshot","isGroup":false}]'
reclaimPolicy: Retain
allowVolumeExpansion: true
volumeBindingMode: WaitForFirstConsumer

View File

@ -1,12 +1,13 @@
# services/keycloak/oneoffs/veles-gitea-oidc-secret-ensure-job.yaml
# One-off job for sso/veles-gitea-oidc-secret-ensure-5.
# Purpose: create/update the Veles realm Gitea OIDC client and write the
# matching Gitea auth-source secret to Vault.
# Keep suspended until the Vault policy change has reconciled, then unsuspend once.
# One-off job for sso/veles-gitea-oidc-secret-ensure-1.
# Purpose: create/update the Keycloak veles realm Gitea OIDC client, ensure the
# veles-tester group exists, and write the client secret to Vault for Gitea.
# Run by setting spec.suspend to false, reconcile, then set it back to true.
# Safe to delete the finished Job/pod; it should not run continuously.
apiVersion: batch/v1
kind: Job
metadata:
name: veles-gitea-oidc-secret-ensure-5
name: veles-gitea-oidc-secret-ensure-1
namespace: sso
spec:
suspend: true

View File

@ -1,11 +1,11 @@
# services/keycloak/oneoffs/veles-realm-ensure-job.yaml
# One-off job for sso/veles-realm-ensure-4.
# One-off job for sso/veles-realm-ensure-2.
# Purpose: create the Veles realm, groups, OIDC client, SMTP settings, and Vault client secret.
# Keep suspended until Veles Vault paths/policies have reconciled, then unsuspend once.
apiVersion: batch/v1
kind: Job
metadata:
name: veles-realm-ensure-4
name: veles-realm-ensure-2
namespace: sso
spec:
suspend: true
@ -157,7 +157,7 @@ spec:
create_payload = {
"realm": realm,
"enabled": True,
"registrationAllowed": True,
"registrationAllowed": False,
"resetPasswordAllowed": True,
"verifyEmail": True,
"loginWithEmailAllowed": True,
@ -174,7 +174,7 @@ spec:
realm_rep.update(
{
"enabled": True,
"registrationAllowed": True,
"registrationAllowed": False,
"resetPasswordAllowed": True,
"verifyEmail": True,
"loginWithEmailAllowed": True,
@ -194,77 +194,14 @@ spec:
)
if status != 200:
raise SystemExit(f"Group search failed for {name}: status={status}")
for group in groups or []:
if group.get("name") == name:
return group["id"]
if any(group.get("name") == name for group in groups or []):
return
status, body = request("POST", f"{base_url}/admin/realms/{realm}/groups", token, {"name": name})
if status not in (201, 204, 409):
raise SystemExit(f"Group create failed for {name}: status={status} body={body}")
status, groups = request(
"GET",
f"{base_url}/admin/realms/{realm}/groups?search={urllib.parse.quote(name)}",
token,
)
if status != 200:
raise SystemExit(f"Group lookup failed after create for {name}: status={status}")
for group in groups or []:
if group.get("name") == name:
return group["id"]
raise SystemExit(f"Group {name} not found after create")
def ensure_role(name):
status, role = request("GET", f"{base_url}/admin/realms/{realm}/roles/{urllib.parse.quote(name)}", token)
if status == 404:
status, body = request("POST", f"{base_url}/admin/realms/{realm}/roles", token, {"name": name})
if status not in (201, 204, 409):
raise SystemExit(f"Role create failed for {name}: status={status} body={body}")
status, role = request(
"GET",
f"{base_url}/admin/realms/{realm}/roles/{urllib.parse.quote(name)}",
token,
)
if status != 200 or not isinstance(role, dict):
raise SystemExit(f"Role lookup failed for {name}: status={status}")
return role
def ensure_group_role(group_id, role):
status, mappings = request(
"GET",
f"{base_url}/admin/realms/{realm}/groups/{group_id}/role-mappings/realm",
token,
)
if status != 200:
raise SystemExit(f"Group role mapping lookup failed: status={status}")
if any(mapping.get("name") == role["name"] for mapping in mappings or []):
return
status, body = request(
"POST",
f"{base_url}/admin/realms/{realm}/groups/{group_id}/role-mappings/realm",
token,
[role],
)
if status not in (200, 204):
raise SystemExit(f"Group role mapping failed for {role['name']}: status={status} body={body}")
def ensure_default_group(group_id, name):
status, groups = request("GET", f"{base_url}/admin/realms/{realm}/default-groups", token)
if status != 200:
raise SystemExit(f"Default group lookup failed: status={status}")
for group in groups or []:
if group.get("id") == group_id or group.get("name") == name:
return
status, body = request("PUT", f"{base_url}/admin/realms/{realm}/default-groups/{group_id}", token)
if status not in (200, 204):
raise SystemExit(f"Default group update failed for {name}: status={status} body={body}")
alpha_group_id = ensure_group("alpha")
admin_group_id = ensure_group("admin")
alpha_role = ensure_role("alpha")
admin_role = ensure_role("admin")
ensure_group_role(alpha_group_id, alpha_role)
ensure_group_role(admin_group_id, alpha_role)
ensure_group_role(admin_group_id, admin_role)
ensure_default_group(alpha_group_id, "alpha")
ensure_group("alpha")
ensure_group("admin")
status, clients = request(
"GET",
@ -336,7 +273,6 @@ spec:
raise SystemExit(f"Mapper lookup failed: status={status}")
mapper_id = next((mapper.get("id") for mapper in mappers or [] if mapper.get("name") == "groups"), None)
if mapper_id:
mapper_payload["id"] = mapper_id
status, body = request(
"PUT",
f"{base_url}/admin/realms/{realm}/clients/{client_uuid}/protocol-mappers/models/{mapper_id}",

View File

@ -9,8 +9,7 @@ CLIENT_ID="${KEYCLOAK_CLIENT_ID:-gitea}"
PUBLIC_BASE_URL="${GITEA_PUBLIC_BASE_URL:-https://scm.bstein.dev}"
AUTH_SOURCE_NAME="${GITEA_AUTH_SOURCE_NAME:-veles}"
TESTER_GROUP="${VELES_GITEA_TESTER_GROUP:-veles-tester}"
VAULT_SECRET_PATH="${VAULT_SECRET_PATH:-gitea/gitea-veles-oidc}"
GROUP_TEAM_MAP="${GITEA_GROUP_TEAM_MAP:-{\"veles-tester\":{\"veles-alpha\":[\"testers\"]}}}"
VAULT_SECRET_PATH="${VAULT_SECRET_PATH:-kv/data/atlas/gitea/gitea-veles-oidc}"
ACCESS_TOKEN=""
for attempt in 1 2 3 4 5 6 7 8 9 10; do
@ -20,7 +19,6 @@ for attempt in 1 2 3 4 5 6 7 8 9 10; do
echo "Waiting for Keycloak to be reachable (attempt ${attempt})" >&2
sleep $((attempt * 2))
done
for attempt in 1 2 3 4 5; do
TOKEN_JSON="$(curl -sS -X POST "$KC_URL/realms/master/protocol/openid-connect/token" \
-H 'Content-Type: application/x-www-form-urlencoded' \
@ -44,198 +42,21 @@ ensure_group() {
group_name="$1"
groups="$(curl -sS -H "Authorization: Bearer ${ACCESS_TOKEN}" \
"${KC_URL}/admin/realms/${REALM}/groups?search=$(printf '%s' "${group_name}" | jq -sRr @uri)" || true)"
group_id="$(echo "$groups" | jq -r --arg name "$group_name" '.[]? | select(.name == $name) | .id' | head -n1 || true)"
if [ -n "$group_id" ] && [ "$group_id" != "null" ]; then
printf '%s' "$group_id"
if echo "$groups" | jq -e --arg name "$group_name" '.[]? | select(.name == $name)' >/dev/null 2>&1; then
return
fi
status="$(curl -sS -o /tmp/keycloak-group-create.json -w "%{http_code}" -X POST \
status="$(curl -sS -o /dev/null -w "%{http_code}" -X POST \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H 'Content-Type: application/json' \
-d "$(jq -nc --arg name "$group_name" '{name:$name}')" \
"${KC_URL}/admin/realms/${REALM}/groups")"
if [ "$status" != "201" ] && [ "$status" != "204" ] && [ "$status" != "409" ]; then
echo "Keycloak group create failed for ${group_name} (status ${status})" >&2
cat /tmp/keycloak-group-create.json >&2 || true
exit 1
fi
groups="$(curl -sS -H "Authorization: Bearer ${ACCESS_TOKEN}" \
"${KC_URL}/admin/realms/${REALM}/groups?search=$(printf '%s' "${group_name}" | jq -sRr @uri)" || true)"
group_id="$(echo "$groups" | jq -r --arg name "$group_name" '.[]? | select(.name == $name) | .id' | head -n1 || true)"
if [ -z "$group_id" ] || [ "$group_id" = "null" ]; then
echo "Keycloak group ${group_name} not found after create" >&2
exit 1
fi
printf '%s' "$group_id"
}
ensure_role() {
role_name="$1"
role="$(curl -sS -H "Authorization: Bearer ${ACCESS_TOKEN}" \
"${KC_URL}/admin/realms/${REALM}/roles/$(printf '%s' "${role_name}" | jq -sRr @uri)" || true)"
if echo "$role" | jq -e --arg name "$role_name" '.name == $name' >/dev/null 2>&1; then
printf '%s' "$role"
return
fi
status="$(curl -sS -o /tmp/keycloak-role-create.json -w "%{http_code}" -X POST \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H 'Content-Type: application/json' \
-d "$(jq -nc --arg name "$role_name" '{name:$name}')" \
"${KC_URL}/admin/realms/${REALM}/roles")"
if [ "$status" != "201" ] && [ "$status" != "204" ] && [ "$status" != "409" ]; then
echo "Keycloak role create failed for ${role_name} (status ${status})" >&2
cat /tmp/keycloak-role-create.json >&2 || true
exit 1
fi
role="$(curl -sS -H "Authorization: Bearer ${ACCESS_TOKEN}" \
"${KC_URL}/admin/realms/${REALM}/roles/$(printf '%s' "${role_name}" | jq -sRr @uri)" || true)"
if ! echo "$role" | jq -e --arg name "$role_name" '.name == $name' >/dev/null 2>&1; then
echo "Keycloak role ${role_name} not found after create" >&2
exit 1
fi
printf '%s' "$role"
}
ensure_group_role() {
group_id="$1"
role_json="$2"
role_name="$(echo "$role_json" | jq -r '.name')"
mappings="$(curl -sS -H "Authorization: Bearer ${ACCESS_TOKEN}" \
"${KC_URL}/admin/realms/${REALM}/groups/${group_id}/role-mappings/realm" || true)"
if echo "$mappings" | jq -e --arg name "$role_name" '.[]? | select(.name == $name)' >/dev/null 2>&1; then
return
fi
status="$(curl -sS -o /tmp/keycloak-group-role.json -w "%{http_code}" -X POST \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H 'Content-Type: application/json' \
-d "[$role_json]" \
"${KC_URL}/admin/realms/${REALM}/groups/${group_id}/role-mappings/realm")"
if [ "$status" != "200" ] && [ "$status" != "204" ]; then
echo "Keycloak group role mapping failed for ${role_name} (status ${status})" >&2
cat /tmp/keycloak-group-role.json >&2 || true
exit 1
fi
}
ensure_mapper() {
client_uuid="$1"
mapper_name="$2"
mapper_payload="$3"
mappers="$(curl -sS -H "Authorization: Bearer ${ACCESS_TOKEN}" \
"${KC_URL}/admin/realms/${REALM}/clients/${client_uuid}/protocol-mappers/models" || true)"
mapper_id="$(echo "$mappers" | jq -r --arg name "$mapper_name" '.[]? | select(.name == $name) | .id' | head -n1 || true)"
if [ -n "$mapper_id" ] && [ "$mapper_id" != "null" ]; then
mapper_payload="$(echo "$mapper_payload" | jq --arg id "$mapper_id" '. + {id:$id}')"
status="$(curl -sS -o /tmp/keycloak-mapper.json -w "%{http_code}" -X PUT \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H 'Content-Type: application/json' \
-d "${mapper_payload}" \
"${KC_URL}/admin/realms/${REALM}/clients/${client_uuid}/protocol-mappers/models/${mapper_id}")"
else
status="$(curl -sS -o /tmp/keycloak-mapper.json -w "%{http_code}" -X POST \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H 'Content-Type: application/json' \
-d "${mapper_payload}" \
"${KC_URL}/admin/realms/${REALM}/clients/${client_uuid}/protocol-mappers/models")"
fi
if [ "$status" != "200" ] && [ "$status" != "201" ] && [ "$status" != "204" ]; then
echo "Keycloak mapper ensure failed for ${mapper_name} (status ${status})" >&2
cat /tmp/keycloak-mapper.json >&2 || true
exit 1
fi
}
ensure_client_scope() {
scope_name="$1"
scopes="$(curl -sS -H "Authorization: Bearer ${ACCESS_TOKEN}" \
"${KC_URL}/admin/realms/${REALM}/client-scopes?search=$(printf '%s' "${scope_name}" | jq -sRr @uri)" || true)"
scope_id="$(echo "$scopes" | jq -r --arg name "$scope_name" '.[]? | select(.name == $name) | .id' | head -n1 || true)"
if [ -n "$scope_id" ] && [ "$scope_id" != "null" ]; then
printf '%s' "$scope_id"
return
fi
scope_payload="$(jq -nc --arg name "$scope_name" '{name:$name,protocol:"openid-connect",attributes:{"include.in.token.scope":"true","display.on.consent.screen":"false"}}')"
status="$(curl -sS -o /tmp/keycloak-client-scope-create.json -w "%{http_code}" -X POST \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H 'Content-Type: application/json' \
-d "${scope_payload}" \
"${KC_URL}/admin/realms/${REALM}/client-scopes")"
if [ "$status" != "201" ] && [ "$status" != "204" ] && [ "$status" != "409" ]; then
echo "Keycloak client scope create failed for ${scope_name} (status ${status})" >&2
cat /tmp/keycloak-client-scope-create.json >&2 || true
exit 1
fi
scopes="$(curl -sS -H "Authorization: Bearer ${ACCESS_TOKEN}" \
"${KC_URL}/admin/realms/${REALM}/client-scopes?search=$(printf '%s' "${scope_name}" | jq -sRr @uri)" || true)"
scope_id="$(echo "$scopes" | jq -r --arg name "$scope_name" '.[]? | select(.name == $name) | .id' | head -n1 || true)"
if [ -z "$scope_id" ] || [ "$scope_id" = "null" ]; then
echo "Keycloak client scope ${scope_name} not found after create" >&2
exit 1
fi
printf '%s' "$scope_id"
}
ensure_scope_mapper() {
scope_id="$1"
mapper_name="$2"
mapper_payload="$3"
mappers="$(curl -sS -H "Authorization: Bearer ${ACCESS_TOKEN}" \
"${KC_URL}/admin/realms/${REALM}/client-scopes/${scope_id}/protocol-mappers/models" || true)"
mapper_id="$(echo "$mappers" | jq -r --arg name "$mapper_name" '.[]? | select(.name == $name) | .id' | head -n1 || true)"
if [ -n "$mapper_id" ] && [ "$mapper_id" != "null" ]; then
mapper_payload="$(echo "$mapper_payload" | jq --arg id "$mapper_id" '. + {id:$id}')"
status="$(curl -sS -o /tmp/keycloak-scope-mapper.json -w "%{http_code}" -X PUT \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H 'Content-Type: application/json' \
-d "${mapper_payload}" \
"${KC_URL}/admin/realms/${REALM}/client-scopes/${scope_id}/protocol-mappers/models/${mapper_id}")"
else
status="$(curl -sS -o /tmp/keycloak-scope-mapper.json -w "%{http_code}" -X POST \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H 'Content-Type: application/json' \
-d "${mapper_payload}" \
"${KC_URL}/admin/realms/${REALM}/client-scopes/${scope_id}/protocol-mappers/models")"
fi
if [ "$status" != "200" ] && [ "$status" != "201" ] && [ "$status" != "204" ]; then
echo "Keycloak client-scope mapper ensure failed for ${mapper_name} (status ${status})" >&2
cat /tmp/keycloak-scope-mapper.json >&2 || true
exit 1
fi
}
ensure_client_optional_scope() {
client_uuid="$1"
scope_id="$2"
scope_name="$3"
default_scopes="$(curl -sS -H "Authorization: Bearer ${ACCESS_TOKEN}" \
"${KC_URL}/admin/realms/${REALM}/clients/${client_uuid}/default-client-scopes" || true)"
optional_scopes="$(curl -sS -H "Authorization: Bearer ${ACCESS_TOKEN}" \
"${KC_URL}/admin/realms/${REALM}/clients/${client_uuid}/optional-client-scopes" || true)"
if echo "$default_scopes" | jq -e --arg name "$scope_name" '.[]? | select(.name == $name)' >/dev/null 2>&1 \
|| echo "$optional_scopes" | jq -e --arg name "$scope_name" '.[]? | select(.name == $name)' >/dev/null 2>&1; then
return
fi
status="$(curl -sS -o /dev/null -w "%{http_code}" -X PUT \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
"${KC_URL}/admin/realms/${REALM}/clients/${client_uuid}/optional-client-scopes/${scope_id}")"
if [ "$status" != "200" ] && [ "$status" != "201" ] && [ "$status" != "204" ]; then
status="$(curl -sS -o /dev/null -w "%{http_code}" -X POST \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
"${KC_URL}/admin/realms/${REALM}/clients/${client_uuid}/optional-client-scopes/${scope_id}")"
if [ "$status" != "200" ] && [ "$status" != "201" ] && [ "$status" != "204" ]; then
echo "Failed to attach ${scope_name} client scope to ${CLIENT_ID} (status ${status})" >&2
exit 1
fi
fi
}
TESTER_GROUP_ID="$(ensure_group "${TESTER_GROUP}")"
TESTER_ROLE="$(ensure_role "${TESTER_GROUP}")"
ensure_group_role "${TESTER_GROUP_ID}" "${TESTER_ROLE}"
ensure_group "${TESTER_GROUP}"
CLIENT_QUERY="$(curl -sS -H "Authorization: Bearer ${ACCESS_TOKEN}" \
"${KC_URL}/admin/realms/${REALM}/clients?clientId=$(printf '%s' "${CLIENT_ID}" | jq -sRr @uri)" || true)"
@ -245,17 +66,16 @@ client_payload="$(jq -nc \
--arg client_id "${CLIENT_ID}" \
--arg root_url "${PUBLIC_BASE_URL}" \
--arg callback "${PUBLIC_BASE_URL}/user/oauth2/${AUTH_SOURCE_NAME}/callback" \
'{clientId:$client_id,enabled:true,protocol:"openid-connect",publicClient:false,standardFlowEnabled:true,implicitFlowEnabled:false,directAccessGrantsEnabled:false,serviceAccountsEnabled:false,redirectUris:[$callback],webOrigins:[$root_url],rootUrl:$root_url,baseUrl:"/",attributes:{"pkce.code.challenge.method":"","post.logout.redirect.uris":($root_url + "/*")}}')"
'{clientId:$client_id,enabled:true,protocol:"openid-connect",publicClient:false,standardFlowEnabled:true,implicitFlowEnabled:false,directAccessGrantsEnabled:false,serviceAccountsEnabled:false,redirectUris:[$callback],webOrigins:[$root_url],rootUrl:$root_url,baseUrl:"/",attributes:{"pkce.code.challenge.method":"S256","post.logout.redirect.uris":($root_url + "/*")}}')"
if [ -z "$CLIENT_UUID" ] || [ "$CLIENT_UUID" = "null" ]; then
status="$(curl -sS -o /tmp/keycloak-client-create.json -w "%{http_code}" -X POST \
status="$(curl -sS -o /dev/null -w "%{http_code}" -X POST \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H 'Content-Type: application/json' \
-d "${client_payload}" \
"${KC_URL}/admin/realms/${REALM}/clients")"
if [ "$status" != "201" ] && [ "$status" != "204" ] && [ "$status" != "409" ]; then
echo "Keycloak client create failed for ${CLIENT_ID} (status ${status})" >&2
cat /tmp/keycloak-client-create.json >&2 || true
exit 1
fi
CLIENT_QUERY="$(curl -sS -H "Authorization: Bearer ${ACCESS_TOKEN}" \
@ -268,26 +88,38 @@ if [ -z "$CLIENT_UUID" ] || [ "$CLIENT_UUID" = "null" ]; then
exit 1
fi
status="$(curl -sS -o /tmp/keycloak-client-update.json -w "%{http_code}" -X PUT \
status="$(curl -sS -o /dev/null -w "%{http_code}" -X PUT \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H 'Content-Type: application/json' \
-d "${client_payload}" \
"${KC_URL}/admin/realms/${REALM}/clients/${CLIENT_UUID}")"
if [ "$status" != "200" ] && [ "$status" != "204" ]; then
echo "Keycloak client update failed for ${CLIENT_ID} (status ${status})" >&2
cat /tmp/keycloak-client-update.json >&2 || true
exit 1
fi
groups_mapper_payload="$(jq -nc \
mapper_payload="$(jq -nc \
'{name:"groups",protocol:"openid-connect",protocolMapper:"oidc-group-membership-mapper",consentRequired:false,config:{"full.path":"false","id.token.claim":"true","access.token.claim":"true","userinfo.token.claim":"true","claim.name":"groups","jsonType.label":"String"}}')"
roles_mapper_payload="$(jq -nc \
'{name:"roles",protocol:"openid-connect",protocolMapper:"oidc-usermodel-realm-role-mapper",consentRequired:false,config:{"multivalued":"true","id.token.claim":"true","access.token.claim":"true","userinfo.token.claim":"true","claim.name":"roles","jsonType.label":"String"}}')"
ensure_mapper "${CLIENT_UUID}" groups "${groups_mapper_payload}"
ensure_mapper "${CLIENT_UUID}" roles "${roles_mapper_payload}"
GROUPS_SCOPE_ID="$(ensure_client_scope groups)"
ensure_scope_mapper "${GROUPS_SCOPE_ID}" groups "${groups_mapper_payload}"
ensure_client_optional_scope "${CLIENT_UUID}" "${GROUPS_SCOPE_ID}" groups
mappers="$(curl -sS -H "Authorization: Bearer ${ACCESS_TOKEN}" \
"${KC_URL}/admin/realms/${REALM}/clients/${CLIENT_UUID}/protocol-mappers/models" || true)"
MAPPER_ID="$(echo "$mappers" | jq -r '.[]? | select(.name == "groups") | .id' | head -n1 || true)"
if [ -n "$MAPPER_ID" ] && [ "$MAPPER_ID" != "null" ]; then
status="$(curl -sS -o /dev/null -w "%{http_code}" -X PUT \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H 'Content-Type: application/json' \
-d "${mapper_payload}" \
"${KC_URL}/admin/realms/${REALM}/clients/${CLIENT_UUID}/protocol-mappers/models/${MAPPER_ID}")"
else
status="$(curl -sS -o /dev/null -w "%{http_code}" -X POST \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H 'Content-Type: application/json' \
-d "${mapper_payload}" \
"${KC_URL}/admin/realms/${REALM}/clients/${CLIENT_UUID}/protocol-mappers/models")"
fi
if [ "$status" != "200" ] && [ "$status" != "201" ] && [ "$status" != "204" ]; then
echo "Keycloak groups mapper ensure failed (status ${status})" >&2
exit 1
fi
CLIENT_SECRET="$(curl -sS -H "Authorization: Bearer ${ACCESS_TOKEN}" \
"${KC_URL}/admin/realms/${REALM}/clients/${CLIENT_UUID}/client-secret" | jq -r '.value' 2>/dev/null || true)"
@ -310,21 +142,16 @@ fi
payload="$(jq -nc \
--arg client_id "${CLIENT_ID}" \
--arg client_secret "${CLIENT_SECRET}" \
--arg discovery_url "${PUBLIC_BASE_URL}" \
--arg issuer "https://sso.bstein.dev/realms/${REALM}" \
--arg auto_discovery_url "https://sso.bstein.dev/realms/${REALM}/.well-known/openid-configuration" \
--arg auth_source_name "${AUTH_SOURCE_NAME}" \
--arg tester_group "${TESTER_GROUP}" \
--arg group_team_map "${GROUP_TEAM_MAP}" \
'{data:{client_id:$client_id,client_secret:$client_secret,issuer:$issuer,openid_auto_discovery_url:$auto_discovery_url,auth_source_name:$auth_source_name,required_claim_name:"groups",required_claim_value:$tester_group,group_claim_name:"groups",restricted_group:$tester_group,group_team_map:$group_team_map}}')"
--arg group_team_map "{\"${TESTER_GROUP}\":{\"veles-alpha\":[\"testers\"]}}" \
'{data:{client_id:$client_id,client_secret:$client_secret,issuer:$issuer,openid_auto_discovery_url:$auto_discovery_url,auth_source_name:$auth_source_name,required_claim_name:"groups",required_claim_value:$tester_group,group_claim_name:"groups",restricted_group:$tester_group,group_team_map:$group_team_map,gitea_base_url:$discovery_url}}')"
write_status="$(curl -sS -o /tmp/veles-gitea-oidc-write.json -w "%{http_code}" -X POST \
-H "X-Vault-Token: ${vault_token}" \
curl -sS -X POST -H "X-Vault-Token: ${vault_token}" \
-H 'Content-Type: application/json' \
-d "${payload}" "${vault_addr}/v1/kv/data/atlas/${VAULT_SECRET_PATH}")"
if [ "${write_status}" != "200" ] && [ "${write_status}" != "204" ]; then
echo "Vault write failed for ${VAULT_SECRET_PATH} (status ${write_status})" >&2
cat /tmp/veles-gitea-oidc-write.json >&2 || true
exit 1
fi
-d "${payload}" "${vault_addr}/v1/${VAULT_SECRET_PATH}" >/dev/null
echo "Veles Gitea OIDC client ready in Keycloak and Vault"
echo "Veles realm Gitea OIDC client ready"

View File

@ -7,11 +7,26 @@ metadata:
labels:
app.kubernetes.io/name: veles
app.kubernetes.io/component: artifacts
veles.bstein.dev/backup: longhorn
spec:
accessModes:
- ReadWriteOnce
storageClassName: veles-oceanus-artifacts
resources:
requests:
storage: 200Gi
storage: 500Gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: veles-engine-cache
namespace: veles
labels:
app.kubernetes.io/name: veles
app.kubernetes.io/component: engine-cache
spec:
accessModes:
- ReadWriteOnce
storageClassName: veles-oceanus-artifacts
resources:
requests:
storage: 100Gi

View File

@ -16,11 +16,30 @@ spec:
metadata:
labels:
app: veles-backend
annotations:
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/agent-pre-populate-only: "true"
vault.hashicorp.com/role: "veles"
vault.hashicorp.com/agent-inject-secret-veles-env.sh: "kv/data/atlas/veles/veles-db"
vault.hashicorp.com/agent-inject-template-veles-env.sh: |
{{- with secret "kv/data/atlas/veles/veles-db" }}
export DATABASE_URL="{{ .Data.data.DATABASE_URL }}"
export VELES_DATABASE_USER="{{ .Data.data.POSTGRES_USER }}"
export VELES_DATABASE_PASSWORD="{{ .Data.data.POSTGRES_PASSWORD }}"
{{- end }}
{{- with secret "kv/data/atlas/veles/veles-oidc" }}
export VELES_OIDC_CLIENT_SECRET="{{ .Data.data.client_secret }}"
{{- end }}
{{- with secret "kv/data/atlas/veles/app-secrets" }}
export VELES_SESSION_SECRET="{{ .Data.data.VELES_SESSION_SECRET }}"
export VELES_BYOK_ENCRYPTION_KEY="{{ .Data.data.VELES_BYOK_ENCRYPTION_KEY }}"
{{- end }}
spec:
serviceAccountName: veles-backend
priorityClassName: veles-core
nodeSelector:
veles.bstein.dev/node-pool: oceanus
kubernetes.io/arch: amd64
tolerations:
- key: veles.bstein.dev/simulation
operator: Equal
@ -32,41 +51,46 @@ spec:
seccompProfile:
type: RuntimeDefault
initContainers:
- name: migrate-db
image: registry.bstein.dev/veles/veles-backend:0.3.15 # {"$imagepolicy": "veles:veles-backend"}
- name: database-init
image: registry.bstein.dev/veles/veles-backend:0.3.16 # {"$imagepolicy": "veles:veles-backend"}
imagePullPolicy: IfNotPresent
command: ["veles-db"]
args: ["--init", "--json"]
command: ["/bin/sh", "-c"]
args:
- |
set -eu
if [ -f /vault/secrets/veles-env.sh ]; then
. /vault/secrets/veles-env.sh
fi
exec veles-db --init --wait-seconds 120 --bootstrap-meta --content-root /opt/veles-content
envFrom:
- configMapRef:
name: veles-app-config
- secretRef:
name: veles-runtime-secrets
name: veles-config
resources:
requests:
cpu: 100m
memory: 256Mi
cpu: 50m
memory: 128Mi
limits:
cpu: 500m
cpu: 250m
memory: 512Mi
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
containers:
- name: backend
image: registry.bstein.dev/veles/veles-backend:0.3.15 # {"$imagepolicy": "veles:veles-backend"}
image: registry.bstein.dev/veles/veles-backend:0.3.16 # {"$imagepolicy": "veles:veles-backend"}
imagePullPolicy: IfNotPresent
env:
- name: VELES_SIM_IMAGE
value: registry.bstein.dev/veles/veles-sim-worker:0.3.15 # {"$imagepolicy": "veles:veles-sim-worker"}
command: ["/bin/sh", "-c"]
args:
- |
set -eu
if [ -f /vault/secrets/veles-env.sh ]; then
. /vault/secrets/veles-env.sh
fi
exec veles-web --host 0.0.0.0 --port 8796 --no-resume-queued
ports:
- name: http
containerPort: 8796
protocol: TCP
envFrom:
- configMapRef:
name: veles-config
readinessProbe:
httpGet:
path: /api/v1/ready
@ -79,18 +103,13 @@ spec:
port: http
initialDelaySeconds: 20
periodSeconds: 20
envFrom:
- configMapRef:
name: veles-app-config
- secretRef:
name: veles-runtime-secrets
resources:
requests:
cpu: 250m
memory: 512Mi
limits:
cpu: "1"
memory: 2Gi
memory: 1Gi
securityContext:
runAsNonRoot: true
runAsUser: 10001

View File

@ -2,55 +2,44 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: veles-app-config
name: veles-config
namespace: veles
data:
VELES_ENV: alpha
VELES_PROFILE: cluster
VELES_AUTH_MODE: oidc
VELES_SIM_RUNNER: kubernetes-job
VELES_PUBLIC_URL: https://veles.bstein.dev
VELES_PUBLIC_BASE_URL: https://veles.bstein.dev
VELES_BACKEND_HTTP_PORT: "8796"
VELES_FRONTEND_HTTP_PORT: "8080"
VELES_ALPHA_LABEL: ALPHA
VELES_OIDC_ISSUER_URL: https://sso.bstein.dev/realms/veles
VELES_OIDC_ISSUER: https://sso.bstein.dev/realms/veles
VELES_OIDC_CLIENT_ID: veles-web
VELES_OIDC_ALLOWED_GROUPS: alpha,admin
VELES_OIDC_REQUIRED_GROUPS: alpha,admin
VELES_OIDC_ALLOWED_GROUPS: alpha,veles-tester,admin
VELES_OIDC_REQUIRED_GROUPS: alpha,veles-tester,admin
VELES_OIDC_ADMIN_GROUPS: admin
VELES_OIDC_GROUPS_CLAIM: groups
VELES_OIDC_ROLES_CLAIM: realm_access.roles
VELES_DATABASE_HOST: veles-postgres.veles.svc.cluster.local
VELES_DATABASE_PORT: "5432"
VELES_DATABASE_NAME: veles
VELES_ARTIFACTS_PATH: /data/veles-artifacts
VELES_ARTIFACTS_MODE: rwo-backend-owned
VELES_LOG_ROOT: /data/veles-artifacts/logs
VELES_REPORT_ROOT: /data/veles-artifacts/reports
VELES_ARTIFACT_ROOT: /data/veles-artifacts/artifacts
VELES_RETENTION_DAYS: "30"
VELES_SIM_NAMESPACE: veles
VELES_NAMESPACE: veles
VELES_SIM_IMAGE: registry.bstein.dev/veles/veles-sim-worker:0.3.15 # {"$imagepolicy": "veles:veles-sim-worker"}
VELES_SIM_IMAGE: registry.bstein.dev/veles/veles-sim-worker:0.3.16 # {"$imagepolicy": "veles:veles-sim-worker"}
VELES_SIM_SERVICE_ACCOUNT: veles-sim
VELES_SIM_PRIORITY_CLASS: veles-sim
VELES_SIM_NODE_SELECTOR: veles.bstein.dev/node-pool=oceanus,kubernetes.io/arch=amd64
VELES_SIM_TOLERATIONS: veles.bstein.dev/simulation=true:NoSchedule
VELES_SIM_TOLERATION_KEY: veles.bstein.dev/simulation
VELES_SIM_TOLERATION_VALUE: "true"
VELES_SIM_ACTIVE_DEADLINE_SECONDS: "7200"
VELES_SIM_TTL_SECONDS: "3600"
VELES_SIM_CPU_REQUEST: 500m
VELES_SIM_CPU_LIMIT: "2"
VELES_SIM_MEMORY_REQUEST: 1Gi
VELES_SIM_MEMORY_LIMIT: 4Gi
VELES_SIM_ARTIFACT_PVC: veles-artifacts
VELES_SIM_ARTIFACT_MOUNT_PATH: /data/veles-artifacts
VELES_SIM_FS_GROUP: "10001"
VELES_MAX_ACTIVE_SIMS_PER_USER: "1"
VELES_MAX_ACTIVE_SIMS_GLOBAL: "4"
VELES_RETENTION_NODE_SELECTOR: veles.bstein.dev/simulation=true
VELES_RETENTION_TOLERATION_KEY: veles.bstein.dev/simulation
VELES_RETENTION_TOLERATION_VALUE: "true"
VELES_LOG_RETENTION_DAYS: "30"
VELES_RETENTION_DAYS: "30"
VELES_LOG_ROOT: /data/veles-artifacts/logs
VELES_REPORT_ROOT: /data/veles-artifacts/reports
VELES_ARTIFACT_ROOT: /data/veles-artifacts/artifacts
VELES_SIM_ARTIFACT_PVC: veles-artifacts
VELES_SIM_ARTIFACT_MOUNT_PATH: /data/veles-artifacts
VELES_SIM_ENGINE_PVC: veles-engine-cache
VELES_SIM_ENGINE_MOUNT_PATH: /engine
VELES_SIM_CODEX_CLI_SECRET_PROVIDER_CLASS: veles-codex-cli
VELES_SIM_CODEX_CLI_MOUNT_PATH: /home/veles/.codex
VELES_SIM_CODEX_CLI_HOME: /home/veles/.codex

View File

@ -19,6 +19,7 @@ spec:
spec:
serviceAccountName: veles-frontend
priorityClassName: veles-core
automountServiceAccountToken: false
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
@ -43,12 +44,11 @@ spec:
values: ["rpi5"]
securityContext:
fsGroup: 101
fsGroupChangePolicy: OnRootMismatch
seccompProfile:
type: RuntimeDefault
containers:
- name: frontend
image: registry.bstein.dev/veles/veles-frontend:0.3.15 # {"$imagepolicy": "veles:veles-frontend"}
image: registry.bstein.dev/veles/veles-frontend:0.3.16 # {"$imagepolicy": "veles:veles-frontend"}
imagePullPolicy: IfNotPresent
ports:
- name: http
@ -66,20 +66,15 @@ spec:
port: http
initialDelaySeconds: 20
periodSeconds: 20
envFrom:
- configMapRef:
name: veles-app-config
resources:
requests:
cpu: 100m
memory: 256Mi
cpu: 50m
memory: 64Mi
limits:
cpu: 500m
memory: 512Mi
cpu: 250m
memory: 256Mi
securityContext:
runAsNonRoot: true
runAsUser: 101
runAsGroup: 101
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]

View File

@ -18,12 +18,9 @@ metadata:
spec:
imageRepositoryRef:
name: veles-backend
filterTags:
pattern: '^(?P<version>\d+\.\d+\.\d+)$'
extract: '$version'
policy:
semver:
range: ">=0.1.0"
range: ">=0.1.0-0"
---
apiVersion: image.toolkit.fluxcd.io/v1
kind: ImageRepository
@ -44,12 +41,9 @@ metadata:
spec:
imageRepositoryRef:
name: veles-frontend
filterTags:
pattern: '^(?P<version>\d+\.\d+\.\d+)$'
extract: '$version'
policy:
semver:
range: ">=0.1.0"
range: ">=0.1.0-0"
---
apiVersion: image.toolkit.fluxcd.io/v1
kind: ImageRepository
@ -70,9 +64,6 @@ metadata:
spec:
imageRepositoryRef:
name: veles-sim-worker
filterTags:
pattern: '^(?P<version>\d+\.\d+\.\d+)$'
extract: '$version'
policy:
semver:
range: ">=0.1.0"
range: ">=0.1.0-0"

View File

@ -17,6 +17,7 @@ resources:
- services.yaml
- backend-deployment.yaml
- frontend-deployment.yaml
- retention-cronjob.yaml
- image.yaml
- ingress.yaml
- oneoffs/veles-secrets-ensure-job.yaml

View File

@ -46,7 +46,7 @@ spec:
type: RuntimeDefault
containers:
- name: postgres
image: postgres:15
image: postgres:17
ports:
- name: postgres
containerPort: 5432
@ -62,13 +62,15 @@ spec:
value: veles
resources:
requests:
cpu: "2"
memory: 8Gi
cpu: "1"
memory: 2Gi
limits:
cpu: "4"
memory: 16Gi
memory: 8Gi
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
volumeMounts:
- name: postgres-data
mountPath: /var/lib/postgresql/data
@ -83,4 +85,4 @@ spec:
storageClassName: veles-oceanus-db
resources:
requests:
storage: 100Gi
storage: 200Gi

View File

@ -13,7 +13,7 @@ spec:
pods: "60"
count/jobs.batch: "100"
persistentvolumeclaims: "8"
requests.storage: 300Gi
requests.storage: 1Ti
---
apiVersion: v1
kind: ResourceQuota

View File

@ -0,0 +1,67 @@
# services/veles/retention-cronjob.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
name: veles-retention
namespace: veles
labels:
app: veles-retention
spec:
schedule: "17 4 * * *"
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 1
failedJobsHistoryLimit: 3
jobTemplate:
spec:
template:
metadata:
labels:
app: veles-retention
spec:
restartPolicy: Never
serviceAccountName: veles-retention
automountServiceAccountToken: false
priorityClassName: veles-core
nodeSelector:
veles.bstein.dev/node-pool: oceanus
kubernetes.io/arch: amd64
tolerations:
- key: veles.bstein.dev/simulation
operator: Equal
value: "true"
effect: NoSchedule
securityContext:
fsGroup: 10001
fsGroupChangePolicy: OnRootMismatch
seccompProfile:
type: RuntimeDefault
containers:
- name: retention
image: registry.bstein.dev/veles/veles-backend:0.3.16 # {"$imagepolicy": "veles:veles-backend"}
imagePullPolicy: IfNotPresent
command: ["veles-hygiene"]
args: ["--prune-artifacts"]
envFrom:
- configMapRef:
name: veles-config
volumeMounts:
- name: artifacts
mountPath: /data/veles-artifacts
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
cpu: 250m
memory: 512Mi
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
volumes:
- name: artifacts
persistentVolumeClaim:
claimName: veles-artifacts

View File

@ -13,22 +13,22 @@ spec:
- objectName: "harbor-pull__dockerconfigjson"
secretPath: "kv/data/atlas/shared/harbor-pull"
secretKey: "dockerconfigjson"
- objectName: "veles-db__DATABASE_URL"
- objectName: "veles-db__database-url"
secretPath: "kv/data/atlas/veles/veles-db"
secretKey: "DATABASE_URL"
- objectName: "veles-db__POSTGRES_USER"
- objectName: "veles-db__user"
secretPath: "kv/data/atlas/veles/veles-db"
secretKey: "POSTGRES_USER"
- objectName: "veles-db__POSTGRES_PASSWORD"
- objectName: "veles-db__password"
secretPath: "kv/data/atlas/veles/veles-db"
secretKey: "POSTGRES_PASSWORD"
- objectName: "veles-oidc__client_secret"
- objectName: "veles-oidc__client-secret"
secretPath: "kv/data/atlas/veles/veles-oidc"
secretKey: "client_secret"
- objectName: "veles-app-secrets__VELES_SESSION_SECRET"
- objectName: "veles-app__session-secret"
secretPath: "kv/data/atlas/veles/app-secrets"
secretKey: "VELES_SESSION_SECRET"
- objectName: "veles-app-secrets__VELES_BYOK_ENCRYPTION_KEY"
- objectName: "veles-app__byok-encryption-key"
secretPath: "kv/data/atlas/veles/app-secrets"
secretKey: "VELES_BYOK_ENCRYPTION_KEY"
secretObjects:
@ -40,15 +40,31 @@ spec:
- secretName: veles-runtime-secrets
type: Opaque
data:
- objectName: veles-db__DATABASE_URL
- objectName: veles-db__database-url
key: DATABASE_URL
- objectName: veles-db__POSTGRES_USER
- objectName: veles-db__user
key: VELES_DATABASE_USER
- objectName: veles-db__POSTGRES_PASSWORD
- objectName: veles-db__password
key: VELES_DATABASE_PASSWORD
- objectName: veles-oidc__client_secret
- objectName: veles-oidc__client-secret
key: VELES_OIDC_CLIENT_SECRET
- objectName: veles-app-secrets__VELES_SESSION_SECRET
- objectName: veles-app__session-secret
key: VELES_SESSION_SECRET
- objectName: veles-app-secrets__VELES_BYOK_ENCRYPTION_KEY
- objectName: veles-app__byok-encryption-key
key: VELES_BYOK_ENCRYPTION_KEY
---
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
name: veles-codex-cli
namespace: veles
spec:
provider: vault
parameters:
vaultAddress: "http://vault.vault.svc.cluster.local:8200"
roleName: "veles-sim"
objects: |
- objectName: "auth.json"
secretPath: "kv/data/atlas/veles/codex-cli"
secretKey: "auth.json"
filePermission: "0440"

View File

@ -12,6 +12,7 @@ kind: ServiceAccount
metadata:
name: veles-frontend
namespace: veles
automountServiceAccountToken: false
imagePullSecrets:
- name: harbor-regcred
---
@ -25,6 +26,15 @@ imagePullSecrets:
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: veles-retention
namespace: veles
automountServiceAccountToken: false
imagePullSecrets:
- name: harbor-regcred
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: veles-vault-sync
namespace: veles

View File

@ -11,7 +11,7 @@ spec:
- name: http
port: 80
protocol: TCP
targetPort: http
targetPort: 8796
selector:
app: veles-backend
---
@ -27,6 +27,6 @@ spec:
- name: http
port: 80
protocol: TCP
targetPort: http
targetPort: 8080
selector:
app: veles-frontend