diff --git a/deploy/k8s/keycloak/keycloak-bootstrap-job.yaml b/deploy/k8s/keycloak/keycloak-bootstrap-job.yaml index 63b03911..3134ea12 100644 --- a/deploy/k8s/keycloak/keycloak-bootstrap-job.yaml +++ b/deploy/k8s/keycloak/keycloak-bootstrap-job.yaml @@ -27,12 +27,56 @@ spec: hwlab.pikastech.local/component: identity spec: restartPolicy: OnFailure - containers: - - name: bootstrap + initContainers: + - name: wait-for-keycloak + image: curlimages/curl:8.10.1 + command: + - /bin/sh + - -c + - | + set -eu + KC=http://keycloak.keycloak.svc.cluster.local:8080 + for i in $(seq 1 60); do + if curl -sf $KC/realms/master > /dev/null; then + echo keycloak-ready + exit 0 + fi + echo waiting-for-keycloak + sleep 5 + done + echo keycloak-not-ready + exit 1 + - name: get-admin-token image: curlimages/curl:8.10.1 env: - name: ADMIN_PASSWORD_FILE value: /tmp/admin-password + command: + - /bin/sh + - -c + - | + set -eu + ADMIN_PASSWORD=$(cat $ADMIN_PASSWORD_FILE/password) + curl -sf -X POST http://keycloak.keycloak.svc.cluster.local:8080/realms/master/protocol/openid-connect/token \ + -H 'content-type: application/x-www-form-urlencoded' \ + --data-urlencode 'username=admin' \ + --data-urlencode "password=$ADMIN_PASSWORD" \ + --data-urlencode 'grant_type=password' \ + --data-urlencode 'client_id=admin-cli' > /tmp/token.json + if grep -q access_token /tmp/token.json; then + echo token-ok + else + echo token-failed + cat /tmp/token.json + exit 1 + fi + volumeMounts: + - name: admin-password + mountPath: /tmp/admin-password + readOnly: true + - name: create-realm + image: curlimages/curl:8.10.1 + env: - name: CLIENT_SECRET_FILE value: /tmp/client-secret command: @@ -40,49 +84,36 @@ spec: - -c - | set -eu - KC=http://keycloak.keycloak.svc.cluster.local:8080 - until curl -sf $KC/realms/master > /dev/null; do - echo waiting-for-keycloak - sleep 5 - done - ADMIN_PASSWORD=$(cat /tmp/admin-password/password) - CLIENT_SECRET=$(cat /tmp/client-secret/client-secret) - REALM=hwlab - curl -sf -X POST $KC/realms/master/protocol/openid-connect/token \ - -H 'content-type: application/x-www-form-urlencoded' \ - --data-urlencode 'username=admin' \ - --data-urlencode "password=$ADMIN_PASSWORD" \ - --data-urlencode 'grant_type=password' \ - --data-urlencode 'client_id=admin-cli' \ - > /tmp/token.json - cat /tmp/token.json TOKEN=$(grep -oE 'access_token":"[^"]+' /tmp/token.json | head -1 | cut -d'"' -f3) if [ -z "$TOKEN" ]; then echo no-token exit 1 fi + KC=http://keycloak.keycloak.svc.cluster.local:8080 + REALM=hwlab if curl -sf -H "Authorization: Bearer $TOKEN" $KC/admin/realms/$REALM > /dev/null; then echo realm-already-exists exit 0 fi echo creating-realm - curl -sf -X POST $KC/admin/realms \ - -H "Authorization: Bearer $TOKEN" \ - -H 'content-type: application/json' \ - -d '{"realm":"hwlab","enabled":true,"registrationAllowed":true,"verifyEmail":false,"loginWithEmailAllowed":true,"duplicateEmailsAllowed":false,"resetPasswordAllowed":false,"editUsernameAllowed":false,"bruteForceProtected":false,"accessTokenLifespan":1440,"ssoSessionIdleTimeout":86400,"ssoSessionMaxLifespan":86400}' + curl -sf -X POST $KC/admin/realms -H "Authorization: Bearer $TOKEN" -H 'content-type: application/json' -d '{"realm":"hwlab","enabled":true,"registrationAllowed":true,"verifyEmail":false,"loginWithEmailAllowed":true,"duplicateEmailsAllowed":false,"resetPasswordAllowed":false,"editUsernameAllowed":false,"bruteForceProtected":false,"accessTokenLifespan":1440,"ssoSessionIdleTimeout":86400,"ssoSessionMaxLifespan":86400}' echo realm-created - curl -sf -X POST $KC/admin/realms/$REALM/clients \ - -H "Authorization: Bearer $TOKEN" \ - -H 'content-type: application/json' \ - -d '{"clientId":"hwlab-cloud-web","secret":"'"$CLIENT_SECRET"'","redirectUris":["http://74.48.78.17:19667/auth/oidc/callback","https://auth.74-48-78-17.nip.io/auth/oidc/callback"],"webOrigins":["+"],"standardFlowEnabled":true,"directAccessGrantsEnabled":true}' + CLIENT_SECRET=$(cat $CLIENT_SECRET_FILE/client-secret) + curl -sf -X POST $KC/admin/realms/$REALM/clients -H "Authorization: Bearer $TOKEN" -H 'content-type: application/json' -d "{\"clientId\":\"hwlab-cloud-web\",\"secret\":\"$CLIENT_SECRET\",\"redirectUris\":[\"http://74.48.78.17:19667/auth/oidc/callback\",\"https://auth.74-48-78-17.nip.io/auth/oidc/callback\"],\"webOrigins\":[\"+\"],\"standardFlowEnabled\":true,\"directAccessGrantsEnabled\":true}" echo client-created volumeMounts: - - name: admin-password - mountPath: /tmp/admin-password - readOnly: true - name: client-secret mountPath: /tmp/client-secret readOnly: true + containers: + - name: done + image: curlimages/curl:8.10.1 + command: + - /bin/sh + - -c + - | + echo keycloak-bootstrap-complete + sleep 30 volumes: - name: admin-password secret: diff --git a/tools/src/hwlab-cli-lib.ts b/tools/src/hwlab-cli-lib.ts index fced5787..c6e94763 100644 --- a/tools/src/hwlab-cli-lib.ts +++ b/tools/src/hwlab-cli-lib.ts @@ -151,6 +151,15 @@ async function authCommand(context: any) { const subcommand = context.rest[0] || "session"; if (wantsHelp(context)) return authHelp(); if (subcommand === "login") return authLogin(context); + if (subcommand === "oidc-login") { + const response = await fetch(`${runtimeEndpoint(context.parsed, context.env, "web").baseUrl}/auth/oidc/login?returnTo=/`, { method: "GET", redirect: "manual" }); + const location = response.headers.get("location") ?? ""; + return ok("client.auth.oidc-login", { + baseUrl: runtimeEndpoint(context.parsed, context.env, "web").baseUrl, + authorizeUrl: location, + hint: "open this URL in a browser to complete OIDC login; once the callback returns, run `client auth whoami` to verify the session" + }); + } if (subcommand === "status") return authStatus(context); if (subcommand === "profiles") return authProfiles(context); if (subcommand === "whoami") { @@ -180,6 +189,7 @@ function authHelp() { imagePublished: false, commands: [ "login --username USER --password-env HWLAB_PASSWORD [--profile NAME]", + "oidc-login", "status [--profile NAME]", "whoami", "session [--profile NAME]",