diff --git a/deploy/deploy.json b/deploy/deploy.json index db4a286d..6f7dd4cc 100644 --- a/deploy/deploy.json +++ b/deploy/deploy.json @@ -155,6 +155,9 @@ "HWLAB_BOOTSTRAP_ADMIN_USERNAME": "admin", "HWLAB_BOOTSTRAP_ADMIN_DISPLAY_NAME": "HWLAB v0.2 Admin", "HWLAB_BOOTSTRAP_ADMIN_PASSWORD_HASH": "secretRef:hwlab-v02-bootstrap-admin/password-hash", + "HWLAB_KEYCLOAK_ISSUER": "https://auth.74-48-78-17.nip.io/realms/hwlab", + "HWLAB_KEYCLOAK_CLIENT_ID": "hwlab-cloud-web", + "HWLAB_KEYCLOAK_CLIENT_SECRET": "secretRef:hwlab-cloud-web-client/client-secret", "HWLAB_CODE_AGENT_ADAPTER": "agentrun-v01", "AGENTRUN_MGR_URL": "http://agentrun-mgr.agentrun-v01.svc.cluster.local:8080", "HWLAB_CODE_AGENT_AGENTRUN_PROVIDER_ID": "G14", diff --git a/deploy/k8s/keycloak/keycloak-bootstrap-job.yaml b/deploy/k8s/keycloak/keycloak-bootstrap-job.yaml index e64ac4d7..27056e70 100644 --- a/deploy/k8s/keycloak/keycloak-bootstrap-job.yaml +++ b/deploy/k8s/keycloak/keycloak-bootstrap-job.yaml @@ -1,15 +1,3 @@ -apiVersion: v1 -kind: Secret -metadata: - name: hwlab-cloud-web-client - namespace: keycloak - labels: - app.kubernetes.io/name: hwlab-cloud-web-client - hwlab.pikastech.local/component: identity -type: Opaque -stringData: - client-secret: kc-client-secret-placeholder ---- apiVersion: batch/v1 kind: Job metadata: @@ -19,14 +7,15 @@ metadata: app.kubernetes.io/name: keycloak-bootstrap-realm hwlab.pikastech.local/component: identity spec: - backoffLimit: 3 + backoffLimit: 1 + activeDeadlineSeconds: 600 template: metadata: labels: app.kubernetes.io/name: keycloak-bootstrap-realm hwlab.pikastech.local/component: identity spec: - restartPolicy: OnFailure + restartPolicy: Never initContainers: - name: wait-for-keycloak image: curlimages/curl:8.10.1 @@ -50,7 +39,7 @@ spec: exit 1 containers: - name: bootstrap - image: curlimages/curl:8.10.1 + image: quay.io/keycloak/keycloak:25.0 env: - name: PASSWORD valueFrom: @@ -62,41 +51,101 @@ spec: secretKeyRef: name: hwlab-cloud-web-client key: client-secret + - name: KEYCLOAK_BASE_URL + value: http://keycloak.keycloak.svc.cluster.local:8080 + - name: REALM + value: hwlab + - name: CLIENT_ID + value: hwlab-cloud-web + - name: REDIRECT_URI_DEV + value: http://74.48.78.17:19667/auth/oidc/callback + - name: WEB_ORIGIN_DEV + value: http://74.48.78.17:19666 + - name: ACCESS_TOKEN_LIFESPAN + value: "1440" + - name: SSO_SESSION_MAX_LIFESPAN + value: "86400" command: - /bin/sh - -c - | set -eu - KC=http://keycloak.keycloak.svc.cluster.local:8080 - REALM=hwlab - echo fetching-admin-token - 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=$PASSWORD" \ - --data-urlencode 'grant_type=password' \ - --data-urlencode 'client_id=admin-cli' > /tmp/token.json - TOKEN=$(grep -oE '"access_token":"[^"]+' /tmp/token.json | head -1 | cut -d'"' -f4) - if [ -z "$TOKEN" ]; then - echo no-token - cat /tmp/token.json - exit 1 - fi - echo token-ok - code=$(curl -s -o /tmp/realm.json -w '%{http_code}' -H "Authorization: Bearer $TOKEN" $KC/admin/realms/$REALM) - if [ "$code" = "200" ]; then - echo realm-already-exists + KC="$KEYCLOAK_BASE_URL" + KCADM=/opt/keycloak/bin/kcadm.sh + CONFIG=/tmp/kcadm.config + echo "fetching-admin-token" + "$KCADM" config credentials \ + --config "$CONFIG" \ + --server "$KC" \ + --realm master \ + --user admin \ + --password "$PASSWORD" \ + --client admin-cli >/tmp/kcadm-login.log + echo "token-ok" + if "$KCADM" get "realms/$REALM" --config "$CONFIG" >/tmp/realm.json 2>/tmp/realm.err; then + echo "realm-already-exists" + "$KCADM" update "realms/$REALM" \ + --config "$CONFIG" \ + -s enabled=true \ + -s registrationAllowed=true \ + -s verifyEmail=false \ + -s loginWithEmailAllowed=true \ + -s duplicateEmailsAllowed=false \ + -s resetPasswordAllowed=false \ + -s editUsernameAllowed=false \ + -s bruteForceProtected=false \ + -s accessTokenLifespan="$ACCESS_TOKEN_LIFESPAN" \ + -s ssoSessionIdleTimeout="$SSO_SESSION_MAX_LIFESPAN" \ + -s ssoSessionMaxLifespan="$SSO_SESSION_MAX_LIFESPAN" + echo "realm-updated" else - 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}' - echo realm-created + echo "creating-realm" + "$KCADM" create realms \ + --config "$CONFIG" \ + -s realm="$REALM" \ + -s enabled=true \ + -s registrationAllowed=true \ + -s verifyEmail=false \ + -s loginWithEmailAllowed=true \ + -s duplicateEmailsAllowed=false \ + -s resetPasswordAllowed=false \ + -s editUsernameAllowed=false \ + -s bruteForceProtected=false \ + -s accessTokenLifespan="$ACCESS_TOKEN_LIFESPAN" \ + -s ssoSessionIdleTimeout="$SSO_SESSION_MAX_LIFESPAN" \ + -s ssoSessionMaxLifespan="$SSO_SESSION_MAX_LIFESPAN" + echo "realm-created" fi - client_code=$(curl -s -o /tmp/clients.json -w '%{http_code}' -H "Authorization: Bearer $TOKEN" "$KC/admin/realms/$REALM/clients?clientId=hwlab-cloud-web") - if [ "$client_code" = "200" ] && grep -q '"clientId":"hwlab-cloud-web"' /tmp/clients.json; then - echo client-already-exists + CLIENT_UUID=$("$KCADM" get clients \ + --config "$CONFIG" \ + -r "$REALM" \ + -q clientId="$CLIENT_ID" \ + --fields id \ + --format csv \ + --noquotes | head -1 | tr -d '\r') + if [ -n "$CLIENT_UUID" ]; then + echo "client-already-exists" + "$KCADM" update "clients/$CLIENT_UUID" \ + --config "$CONFIG" \ + -r "$REALM" \ + -s clientId="$CLIENT_ID" \ + -s secret="$CLIENT_SECRET" \ + -s 'redirectUris=["'"$REDIRECT_URI_DEV"'"]' \ + -s 'webOrigins=["'"$WEB_ORIGIN_DEV"'"]' \ + -s standardFlowEnabled=true \ + -s directAccessGrantsEnabled=true + echo "client-updated" else - echo creating-client - 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 + echo "creating-client" + "$KCADM" create clients \ + --config "$CONFIG" \ + -r "$REALM" \ + -s clientId="$CLIENT_ID" \ + -s secret="$CLIENT_SECRET" \ + -s 'redirectUris=["'"$REDIRECT_URI_DEV"'"]' \ + -s 'webOrigins=["'"$WEB_ORIGIN_DEV"'"]' \ + -s standardFlowEnabled=true \ + -s directAccessGrantsEnabled=true + echo "client-created" fi - echo keycloak-bootstrap-complete + echo "keycloak-bootstrap-complete" diff --git a/deploy/k8s/keycloak/keycloak-frpc.yaml b/deploy/k8s/keycloak/keycloak-frpc.yaml new file mode 100644 index 00000000..8895d880 --- /dev/null +++ b/deploy/k8s/keycloak/keycloak-frpc.yaml @@ -0,0 +1,56 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: keycloak-frpc-config + namespace: keycloak + labels: + app.kubernetes.io/name: keycloak-frpc + hwlab.pikastech.local/component: identity +data: + frpc.toml: | + serverAddr = "74.48.78.17" + serverPort = 7000 + loginFailExit = true + + [[proxies]] + name = "keycloak-public-https" + type = "tcp" + localIP = "keycloak.keycloak.svc.cluster.local" + localPort = 8080 + remotePort = 28443 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: keycloak-frpc + namespace: keycloak + labels: + app.kubernetes.io/name: keycloak-frpc + hwlab.pikastech.local/component: identity +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app.kubernetes.io/name: keycloak-frpc + template: + metadata: + labels: + app.kubernetes.io/name: keycloak-frpc + hwlab.pikastech.local/component: identity + spec: + containers: + - name: frpc + image: fatedier/frpc:v0.68.1 + args: + - -c + - /etc/frp/frpc.toml + volumeMounts: + - name: config + mountPath: /etc/frp + readOnly: true + volumes: + - name: config + configMap: + name: keycloak-frpc-config diff --git a/deploy/k8s/keycloak/kustomization.yaml b/deploy/k8s/keycloak/kustomization.yaml index 49a98192..0dc40806 100644 --- a/deploy/k8s/keycloak/kustomization.yaml +++ b/deploy/k8s/keycloak/kustomization.yaml @@ -6,6 +6,7 @@ resources: - postgres.yaml - keycloak.yaml - keycloak-bootstrap-job.yaml + - keycloak-frpc.yaml commonLabels: app.kubernetes.io/part-of: hwlab hwlab.pikastech.local/profile: dev diff --git a/deploy/master/caddy/Caddyfile b/deploy/master/caddy/Caddyfile new file mode 100644 index 00000000..627a31e1 --- /dev/null +++ b/deploy/master/caddy/Caddyfile @@ -0,0 +1,39 @@ +# Caddyfile for auth.74-48-78-17.nip.io (HWLAB Keycloak public HTTPS) +# This Caddyfile runs on the master server and reverse-proxies HTTPS to +# the G14 keycloak Service via master frps (port 28443). +# +# Master-side dependency tree: +# +# client browser +# -> 74.48.78.17:443 (Caddy, this file) +# -> 127.0.0.1:28443 (master frps remotePort, see frps.dev.toml) +# -> G14:/root/hwlab-v02/.worktree/.../deploy/k8s/keycloak/keycloak-frpc.yaml +# -> keycloak.keycloak.svc.cluster.local:8080 +# +# Install: download caddy 2.x from https://github.com/caddyserver/caddy/releases +# Put binary at /usr/local/bin/caddy and Caddyfile at /etc/caddy/Caddyfile. +# The systemd unit caddy.service goes to /etc/systemd/system/caddy.service. +# Run `systemctl daemon-reload && systemctl enable --now caddy`. +# Caddy will auto-request a Let's Encrypt production cert for the first +# domain in the config (auth.74-48-78-17.nip.io). +# To validate the chain end-to-end first, point acme_ca to staging: +# +# acme_ca https://acme-staging-v02.api.letsencrypt.org/directory +# +# Then remove the line to switch to production. + +{ + email ops@pikas.tech +} + +auth.74-48-78-17.nip.io { + encode zstd gzip + reverse_proxy 127.0.0.1:28443 { + header_up Host {host} + header_up X-Real-IP {remote_host} + transport http { + dial_timeout 5s + response_header_timeout 30s + } + } +} diff --git a/deploy/master/caddy/caddy.service b/deploy/master/caddy/caddy.service new file mode 100644 index 00000000..6db1a8c2 --- /dev/null +++ b/deploy/master/caddy/caddy.service @@ -0,0 +1,26 @@ +[Unit] +Description=Caddy (HWLAB Keycloak public HTTPS reverse proxy) +Documentation=https://caddyserver.com/docs/ +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +User=root +ExecStart=/usr/local/bin/caddy run --environ --config /etc/caddy/Caddyfile --adapter caddyfile +ExecReload=/usr/local/bin/caddy reload --config /etc/caddy/Caddyfile --adapter caddyfile +TimeoutStopSec=5s +LimitNOFILE=1048576 +PrivateDevices=yes +PrivateTmp=yes +ProtectSystem=full +ProtectHome=read-only +ReadWritePaths=/var/lib/caddy /var/log/caddy /root/.local/share/caddy +AmbientCapabilities=CAP_NET_BIND_SERVICE +CapabilityBoundingSet=CAP_NET_BIND_SERVICE +NoNewPrivileges=yes +Restart=on-failure +RestartSec=5 + +[Install] +WantedBy=multi-user.target diff --git a/deploy/master/frp/README.txt b/deploy/master/frp/README.txt new file mode 100644 index 00000000..1c2ad9c5 --- /dev/null +++ b/deploy/master/frp/README.txt @@ -0,0 +1,18 @@ +# Master-side frps container launch equivalent. +# The actual running container is `hwlab-frps-dev` (fatedier/frps:v0.68.1) +# started by the existing flow with restart policy `unless-stopped`. +# It bind-mounts /opt/hwlab-frp/frps.dev.toml -> /etc/frp/frps.toml (ro). +# To recreate from scratch: +# +# mkdir -p /opt/hwlab-frp +# cp deploy/master/frp/frps.dev.toml /opt/hwlab-frp/ +# docker run -d --name hwlab-frps-dev --restart unless-stopped \ +# -v /opt/hwlab-frp/frps.dev.toml:/etc/frp/frps.toml:ro \ +# -p 7000:7000 \ +# -p 16666:16666 -p 16667:16667 \ +# -p 17666:17666 -p 17667:17667 \ +# -p 18666:18666 -p 18667:18667 \ +# -p 19666:19666 -p 19667:19667 \ +# -p 7402:7402 \ +# -p 28443:28443 \ +# fatedier/frps:v0.68.1 -c /etc/frp/frps.toml diff --git a/deploy/master/frp/frps.dev.toml b/deploy/master/frp/frps.dev.toml new file mode 100644 index 00000000..b62d01dd --- /dev/null +++ b/deploy/master/frp/frps.dev.toml @@ -0,0 +1,41 @@ +bindPort = 7000 + +[[allowPorts]] +start = 16666 +end = 16666 + +[[allowPorts]] +start = 16667 +end = 16667 + +[[allowPorts]] +start = 17666 +end = 17666 + +[[allowPorts]] +start = 17667 +end = 17667 + +[[allowPorts]] +start = 18666 +end = 18666 + +[[allowPorts]] +start = 18667 +end = 18667 + +[[allowPorts]] +start = 19666 +end = 19666 + +[[allowPorts]] +start = 19667 +end = 19667 + +[[allowPorts]] +start = 7402 +end = 7402 + +[[allowPorts]] +start = 28443 +end = 28443 diff --git a/docs/reference/spec-v02-auth.md b/docs/reference/spec-v02-auth.md index 1a0133cc..01827fd9 100644 --- a/docs/reference/spec-v02-auth.md +++ b/docs/reference/spec-v02-auth.md @@ -45,6 +45,7 @@ https://auth.74-48-78-17.nip.io/realms/hwlab - `hwlab-cloud-api` 只记录 issuer、client id、redirect URI 和 SecretRef 名称,不在日志、issue 或 ConfigMap 中输出 client secret。 - Keycloak 的 `hostname`、proxy header 和 frontend URL 必须与公网 issuer 一致,避免 callback 后 token issuer mismatch。 - `hwlab-cloud-api`、`hwlab-cloud-web` 和 CLI 仍保持 v0.2 原入口 `19666/19667`;Keycloak 只新增 auth 域名,不替代 HWLAB Cloud Web。 +- Keycloak OIDC client 的 redirect URI 必须指向 `hwlab-cloud-api` callback(当前 v0.2 为 `http://74.48.78.17:19667/auth/oidc/callback`)。`https://auth.74-48-78-17.nip.io` 只作为 Keycloak issuer/authorization host,不能配置成 HWLAB callback 目标。 ## 注册策略 @@ -247,3 +248,38 @@ API key 行为: | 默认 API key 自动生成和 Web 可查看 | 目标状态 | 小范围测试允许重复查看明文;生产化再改为 hash-only。 | | `AuthPrincipal` 归一 | 目标状态 | 后续实现必须把 Web session、CLI API key 和 AgentRun `hwpod` API key 都归一成同一用户 actor。 | | `admin/user` 与 device pod grant 授权 | 部分实现 | 现有 cloud-api 已有本地用户、session、grant 和 Code Agent owner 绑定;资源授权继续按 spec-user-access 收敛。 | + +## Keycloak 部署纪律 + +G14 v0.2 固定使用 `quay.io/keycloak/keycloak:25.0`(实际启动版本 Keycloak 25.0.6)。该镜像的初始管理员入口是 `KEYCLOAK_ADMIN` / `KEYCLOAK_ADMIN_PASSWORD`;`KC_BOOTSTRAP_ADMIN_USERNAME/PASSWORD` 属于更新版本文档口径,不作为 v0.2 当前镜像的部署入口。`master` realm 保持 Keycloak 自身初始化结果,不导入或覆盖;业务 realm 使用管理员 token 通过 Admin REST API 创建和更新。 + +`--import-realm` 只用于导入非 `master` 的预制 realm 文件,且启动导入遇到已存在 realm 会跳过。需要覆盖已存在 realm 时按 Keycloak import/export 文档停服务后运行离线 import,不能在运行中的生产 Deployment 上把 `master` realm 当作可覆盖配置文件反复灌入。 + +### 禁止直接修改 keycloak 数据库 + +所有 keycloak admin 操作必须通过 `kc.sh` CLI、Keycloak admin REST API 或 `--import-realm` 启动导入完成。**禁止**对 `keycloak` namespace 里的 Postgres 库直接执行 `INSERT / UPDATE / DELETE` 修改 `user_entity`、`credential`、`realm`、`client`、`client_scope`、`client_scope_attributes`、`keycloak_role`、`user_role_mapping` 等表。直接改库会导致: + +- 破坏 Keycloak 25 的 `pbkdf2-sha256` JSON credential 格式,登录时报 `Unrecognized token 'pbkdf2'` 反序列化失败 +- 绕过 Keycloak 启动时缓存的 `client_scope_attributes`(如 `include.in.token.scope`),pod 重启后修改失效 +- 跳过 Liquibase changelog,未来 keycloak 升级时迁移冲突 +- 在多副本或滚动重启场景下造成各副本行为不一致 + +### 正确做法 + +| 场景 | 推荐做法 | 禁止做法 | +| --- | --- | --- | +| 第一次引导管理员 | `KEYCLOAK_ADMIN=admin` + `KEYCLOAK_ADMIN_PASSWORD` 引用 `keycloak-admin` Secret,让 Keycloak 在首次启动时自行创建 `master` 管理员 | `psql` 直插 `user_entity` + `credential`,或导入自制 `master-realm.json` 覆盖 Keycloak 内置 `master` realm | +| 创建 `hwlab` realm + `hwlab-cloud-web` OIDC client | `keycloak-bootstrap-realm` Job 用 `kcadm.sh` / Admin REST API 幂等 create-or-update realm/client,redirect URI 对齐 `hwlab-cloud-api` callback | `psql` 直插,或依赖运行中的 `--import-realm` 覆盖已有 realm | +| 修改 realm/client/user/role | 优先通过 Keycloak Admin REST API 或 `kcadm.sh`;批量离线导入必须停服务后按 import/export 文档执行,并先确认是否会覆盖目标 realm | `psql` UPDATE 后靠重启碰运气 | +| 调试 admin token 没 `realm_access.roles` | `kubectl -n keycloak exec ... -- /opt/keycloak/bin/kc.sh show-config` 查 attribute 实际值;用 `curl /admin/realms/master/client-scopes/` 走 admin REST 验 attribute 状态 | `psql` UPDATE 然后重启 keycloak pod 试运气 | + +### 历史教训 + +#788 收口过程中曾用 `psql` 直插 `user_entity` / `credential` / `user_role_mapping` 临时绕开 Keycloak 初始化问题,结果: + +1. 直插的 `pbkdf2-sha256` 字符串 hash 在 Keycloak 25 反序列化时抛 `Unrecognized token 'pbkdf2'` +2. SQL UPDATE `client_scope_attributes.include.in.token.scope = 'true'` 后,access_token 仍然不包含 `realm_access.roles`——Keycloak 在启动时缓存 attribute,SQL 修改要重启 pod 才生效 +3. `client_scope_client.default_scope = true` 的 SQL 修改同样需要重启才生效 +4. master realm admin role 复合 19 个子 role,手工插 `user_role_mapping` 后仍然有缺失 + +所有这些都是直接改库的副作用。后续所有 Keycloak 部署必须走镜像支持的初始管理员 env、Admin REST API、`kcadm.sh` 或明确停服的离线 import,**禁止 SQL 直改**。 diff --git a/scripts/g14-gitops-render.mjs b/scripts/g14-gitops-render.mjs index 3ab0de88..8dab248e 100644 --- a/scripts/g14-gitops-render.mjs +++ b/scripts/g14-gitops-render.mjs @@ -875,6 +875,9 @@ function transformWorkloads({ workloads, deploy, catalog, source, sourceBranch = upsertEnv(container.env, "HWLAB_BOOTSTRAP_ADMIN_USERNAME", "admin"); upsertEnv(container.env, "HWLAB_BOOTSTRAP_ADMIN_DISPLAY_NAME", "HWLAB v0.2 Admin"); upsertEnvEntry(container.env, deployEnvEntry("HWLAB_BOOTSTRAP_ADMIN_PASSWORD_HASH", "secretRef:hwlab-v02-bootstrap-admin/password-hash", namespace, profile)); + upsertEnv(container.env, "HWLAB_KEYCLOAK_ISSUER", "https://auth.74-48-78-17.nip.io/realms/hwlab"); + upsertEnv(container.env, "HWLAB_KEYCLOAK_CLIENT_ID", "hwlab-cloud-web"); + upsertEnvEntry(container.env, deployEnvEntry("HWLAB_KEYCLOAK_CLIENT_SECRET", "secretRef:hwlab-cloud-web-client/client-secret", namespace, profile)); upsertEnv(container.env, "HWLAB_CODE_AGENT_ADAPTER", "agentrun-v01"); upsertEnv(container.env, "AGENTRUN_MGR_URL", "http://agentrun-mgr.agentrun-v01.svc.cluster.local:8080"); upsertEnv(container.env, "HWLAB_CODE_AGENT_AGENTRUN_PROVIDER_ID", "G14"); diff --git a/scripts/g14-gitops-render.test.ts b/scripts/g14-gitops-render.test.ts index 0813f53f..68091c3e 100644 --- a/scripts/g14-gitops-render.test.ts +++ b/scripts/g14-gitops-render.test.ts @@ -232,6 +232,9 @@ test("v02 render follows TypeScript runtime checks and does not self-patch boots const cloudApiEnv = cloudApi.spec.template.spec.containers[0].env; assert.ok(cloudApiEnv.some((entry) => entry.name === "HWLAB_CODE_AGENT_AGENTRUN_SOURCE_COMMIT" && entry.value === sourceRevision)); assert.ok(cloudApiEnv.some((entry) => entry.name === "UNIDESK_MAIN_SERVER_IP" && entry.value === "http://74.48.78.17:18081")); + assert.ok(cloudApiEnv.some((entry) => entry.name === "HWLAB_KEYCLOAK_ISSUER" && entry.value === "https://auth.74-48-78-17.nip.io/realms/hwlab")); + assert.ok(cloudApiEnv.some((entry) => entry.name === "HWLAB_KEYCLOAK_CLIENT_ID" && entry.value === "hwlab-cloud-web")); + assert.ok(cloudApiEnv.some((entry) => entry.name === "HWLAB_KEYCLOAK_CLIENT_SECRET" && entry.valueFrom?.secretKeyRef?.name === "hwlab-cloud-web-client" && entry.valueFrom?.secretKeyRef?.key === "client-secret")); const agentWorker = workloadsJson.items.find((item) => item.metadata?.labels?.["hwlab.pikastech.local/service-id"] === "hwlab-agent-worker"); assert.ok(agentWorker, "expected v02 hwlab-agent-worker suspended Job template"); const agentWorkerEnv = agentWorker?.spec?.template?.spec?.containers?.[0]?.env ?? [];