Expose operator dashboard with basic auth

Proof: The rendered Kubernetes manifest now includes a Traefik TLS ingress for operator-dashboard and production basic auth, deploy workflow/bootstrap preserve the dashboard password as a secret, and static plus full node tests pass.

Assumptions: doran.133011.xyz is the intended public host because unrip.doran.133011.xyz and dashboard.doran.133011.xyz do not currently resolve.

Still fake: the public dashboard is not deployed or externally verified yet because the cluster host, Forgejo, and Kubernetes API timed out from this machine during this turn.
This commit is contained in:
philipp 2026-05-13 18:08:27 +02:00
parent cd5c591a2c
commit 3cd88c682e
7 changed files with 125 additions and 1 deletions

View file

@ -80,6 +80,36 @@ jobs:
run: | run: |
kubectl apply -f "$WORKSPACE_DIR/deploy/k8s/base/namespace.yaml" kubectl apply -f "$WORKSPACE_DIR/deploy/k8s/base/namespace.yaml"
- name: Upsert runtime secrets
env:
OPERATOR_DASHBOARD_AUTH_PASSWORD: ${{ secrets.OPERATOR_DASHBOARD_AUTH_PASSWORD }}
run: |
if [ -z "$OPERATOR_DASHBOARD_AUTH_PASSWORD" ]; then
echo "missing required repo action secret OPERATOR_DASHBOARD_AUTH_PASSWORD" >&2
exit 1
fi
patch_file="$(mktemp)"
cleanup() {
rm -f "$patch_file"
}
trap cleanup EXIT
python3 - "$OPERATOR_DASHBOARD_AUTH_PASSWORD" >"$patch_file" <<'PY'
import json
import sys
print(json.dumps({
"stringData": {
"OPERATOR_DASHBOARD_AUTH_PASSWORD": sys.argv[1],
},
}))
PY
kubectl -n "$PROJECT_NAMESPACE" patch secret "${PROJECT_NAME}-secrets" \
--type merge \
--patch-file "$patch_file"
- name: Build and push image in-cluster - name: Build and push image in-cluster
env: env:
REPO_TOKEN: ${{ github.token }} REPO_TOKEN: ${{ github.token }}

View file

@ -89,7 +89,8 @@ data:
OPS_SENTINEL_INVENTORY_STALE_MS: "30000" OPS_SENTINEL_INVENTORY_STALE_MS: "30000"
OPS_SENTINEL_FUNDING_CREDIT_PENDING_MS: "300000" OPS_SENTINEL_FUNDING_CREDIT_PENDING_MS: "300000"
OPS_SENTINEL_FUNDING_STUCK_MS: "3600000" OPS_SENTINEL_FUNDING_STUCK_MS: "3600000"
OPERATOR_DASHBOARD_AUTH_MODE: stub OPERATOR_DASHBOARD_AUTH_MODE: basic
OPERATOR_DASHBOARD_AUTH_USERNAME: admin
OPERATOR_DASHBOARD_QUOTE_LIMIT: "10" OPERATOR_DASHBOARD_QUOTE_LIMIT: "10"
OPERATOR_DASHBOARD_TRADE_PAGE_SIZE: "20" OPERATOR_DASHBOARD_TRADE_PAGE_SIZE: "20"
OPERATOR_DASHBOARD_UPSTREAM_TIMEOUT_MS: "3000" OPERATOR_DASHBOARD_UPSTREAM_TIMEOUT_MS: "3000"
@ -244,6 +245,31 @@ spec:
port: 8090 port: 8090
targetPort: 8090 targetPort: 8090
--- ---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: operator-dashboard
namespace: unrip
annotations:
cert-manager.io/cluster-issuer: letsencrypt-production
spec:
ingressClassName: traefik
tls:
- hosts:
- doran.133011.xyz
secretName: operator-dashboard-tls
rules:
- host: doran.133011.xyz
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: operator-dashboard
port:
number: 8090
---
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:

View file

@ -136,6 +136,7 @@ fi
: "${POSTGRES_URL:=}" : "${POSTGRES_URL:=}"
: "${NEAR_INTENTS_SIGNER_PRIVATE_KEY:=}" : "${NEAR_INTENTS_SIGNER_PRIVATE_KEY:=}"
: "${NOTIFICATION_NTFY_TOKEN:=}" : "${NOTIFICATION_NTFY_TOKEN:=}"
: "${OPERATOR_DASHBOARD_AUTH_PASSWORD:=}"
secret_value() { secret_value() {
local key="$1" local key="$1"
@ -158,6 +159,10 @@ if [[ -z "$NOTIFICATION_NTFY_TOKEN" ]]; then
NOTIFICATION_NTFY_TOKEN="$(secret_value NOTIFICATION_NTFY_TOKEN)" NOTIFICATION_NTFY_TOKEN="$(secret_value NOTIFICATION_NTFY_TOKEN)"
fi fi
if [[ -z "$OPERATOR_DASHBOARD_AUTH_PASSWORD" ]]; then
OPERATOR_DASHBOARD_AUTH_PASSWORD="$(secret_value OPERATOR_DASHBOARD_AUTH_PASSWORD)"
fi
if [[ -z "$POSTGRES_PASSWORD" ]]; then if [[ -z "$POSTGRES_PASSWORD" ]]; then
POSTGRES_PASSWORD="$(python3 - <<'PY' POSTGRES_PASSWORD="$(python3 - <<'PY'
import secrets import secrets
@ -170,6 +175,8 @@ if [[ -z "$POSTGRES_URL" ]]; then
POSTGRES_URL="postgresql://unrip:${POSTGRES_PASSWORD}@postgres:5432/unrip" POSTGRES_URL="postgresql://unrip:${POSTGRES_PASSWORD}@postgres:5432/unrip"
fi fi
: "${OPERATOR_DASHBOARD_AUTH_PASSWORD:?set OPERATOR_DASHBOARD_AUTH_PASSWORD or pre-create OPERATOR_DASHBOARD_AUTH_PASSWORD in $APP_SECRET_NAME}"
echo "bootstrapping namespace $PROJECT_NAMESPACE" echo "bootstrapping namespace $PROJECT_NAMESPACE"
kubectl apply -f "$ROOT_DIR/deploy/k8s/base/namespace.yaml" kubectl apply -f "$ROOT_DIR/deploy/k8s/base/namespace.yaml"
@ -178,6 +185,7 @@ secret_args=(
--from-literal=NEAR_INTENTS_API_KEY="$NEAR_INTENTS_API_KEY" --from-literal=NEAR_INTENTS_API_KEY="$NEAR_INTENTS_API_KEY"
--from-literal=POSTGRES_PASSWORD="$POSTGRES_PASSWORD" --from-literal=POSTGRES_PASSWORD="$POSTGRES_PASSWORD"
--from-literal=POSTGRES_URL="$POSTGRES_URL" --from-literal=POSTGRES_URL="$POSTGRES_URL"
--from-literal=OPERATOR_DASHBOARD_AUTH_PASSWORD="$OPERATOR_DASHBOARD_AUTH_PASSWORD"
) )
if [[ -n "$NEAR_INTENTS_SIGNER_PRIVATE_KEY" ]]; then if [[ -n "$NEAR_INTENTS_SIGNER_PRIVATE_KEY" ]]; then
secret_args+=(--from-literal=NEAR_INTENTS_SIGNER_PRIVATE_KEY="$NEAR_INTENTS_SIGNER_PRIVATE_KEY") secret_args+=(--from-literal=NEAR_INTENTS_SIGNER_PRIVATE_KEY="$NEAR_INTENTS_SIGNER_PRIVATE_KEY")
@ -224,6 +232,9 @@ fi
if [[ -n "${FORGEJO_ADMIN_PASSWORD:-}" ]]; then if [[ -n "${FORGEJO_ADMIN_PASSWORD:-}" ]]; then
forgejo_args+=(--admin-password "$FORGEJO_ADMIN_PASSWORD") forgejo_args+=(--admin-password "$FORGEJO_ADMIN_PASSWORD")
fi fi
if [[ -n "${OPERATOR_DASHBOARD_AUTH_PASSWORD:-}" ]]; then
forgejo_args+=(--operator-dashboard-auth-password "$OPERATOR_DASHBOARD_AUTH_PASSWORD")
fi
python3 "$ROOT_DIR/scripts/deploy/forgejo_repo_bootstrap.py" \ python3 "$ROOT_DIR/scripts/deploy/forgejo_repo_bootstrap.py" \
--forgejo-url "$FORGEJO_URL" \ --forgejo-url "$FORGEJO_URL" \

View file

@ -107,6 +107,7 @@ def main():
parser.add_argument('--project-namespace', required=True) parser.add_argument('--project-namespace', required=True)
parser.add_argument('--project-deployments', required=True) parser.add_argument('--project-deployments', required=True)
parser.add_argument('--project-registry-secret-name', required=True) parser.add_argument('--project-registry-secret-name', required=True)
parser.add_argument('--operator-dashboard-auth-password')
args = parser.parse_args() args = parser.parse_args()
client = ForgejoClient(args.forgejo_url, args.admin_username, args.admin_password, args.token) client = ForgejoClient(args.forgejo_url, args.admin_username, args.admin_password, args.token)
@ -120,6 +121,14 @@ def main():
kubeconfig_b64 = base64.b64encode(Path(args.ci_kubeconfig).read_bytes()).decode() kubeconfig_b64 = base64.b64encode(Path(args.ci_kubeconfig).read_bytes()).decode()
client.upsert_secret(args.repo_owner, args.repo_name, 'KUBECONFIG_B64', kubeconfig_b64) client.upsert_secret(args.repo_owner, args.repo_name, 'KUBECONFIG_B64', kubeconfig_b64)
print('upserted repo action secret KUBECONFIG_B64') print('upserted repo action secret KUBECONFIG_B64')
if args.operator_dashboard_auth_password:
client.upsert_secret(
args.repo_owner,
args.repo_name,
'OPERATOR_DASHBOARD_AUTH_PASSWORD',
args.operator_dashboard_auth_password,
)
print('upserted repo action secret OPERATOR_DASHBOARD_AUTH_PASSWORD')
client.upsert_variable(args.repo_owner, args.repo_name, 'REGISTRY_HOST', args.registry_host) client.upsert_variable(args.repo_owner, args.repo_name, 'REGISTRY_HOST', args.registry_host)
client.upsert_variable(args.repo_owner, args.repo_name, 'PROJECT_NAME', args.project_name) client.upsert_variable(args.repo_owner, args.repo_name, 'PROJECT_NAME', args.project_name)

View file

@ -13,6 +13,13 @@ class BootstrapScriptStaticTest(unittest.TestCase):
self.assertIn('ghcr.io/example/unrip:bootstrap', source) self.assertIn('ghcr.io/example/unrip:bootstrap', source)
self.assertIn('kubectl kustomize "$ROOT_DIR/deploy/k8s/base"', source) self.assertIn('kubectl kustomize "$ROOT_DIR/deploy/k8s/base"', source)
def test_bootstrap_preserves_operator_dashboard_password_secret(self):
source = (ROOT / 'scripts/deploy/bootstrap.sh').read_text()
self.assertIn('OPERATOR_DASHBOARD_AUTH_PASSWORD="$(secret_value OPERATOR_DASHBOARD_AUTH_PASSWORD)"', source)
self.assertIn('OPERATOR_DASHBOARD_AUTH_PASSWORD:?set OPERATOR_DASHBOARD_AUTH_PASSWORD', source)
self.assertIn('--from-literal=OPERATOR_DASHBOARD_AUTH_PASSWORD="$OPERATOR_DASHBOARD_AUTH_PASSWORD"', source)
self.assertIn('--operator-dashboard-auth-password "$OPERATOR_DASHBOARD_AUTH_PASSWORD"', source)
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

View file

@ -0,0 +1,20 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
const workflow = readFileSync(new URL('../.forgejo/workflows/deploy.yml', import.meta.url), 'utf8');
const forgejoBootstrap = readFileSync(new URL('../scripts/deploy/forgejo_repo_bootstrap.py', import.meta.url), 'utf8');
test('deploy workflow upserts dashboard password before applying public dashboard manifest', () => {
assert.match(workflow, /name: Upsert runtime secrets/);
assert.match(workflow, /OPERATOR_DASHBOARD_AUTH_PASSWORD: \$\{\{ secrets\.OPERATOR_DASHBOARD_AUTH_PASSWORD \}\}/);
assert.match(workflow, /missing required repo action secret OPERATOR_DASHBOARD_AUTH_PASSWORD/);
assert.match(workflow, /patch secret "\$\{PROJECT_NAME\}-secrets"/);
assert.match(workflow, /--patch-file "\$patch_file"/);
});
test('Forgejo bootstrap can publish dashboard password as a repo action secret', () => {
assert.match(forgejoBootstrap, /--operator-dashboard-auth-password/);
assert.match(forgejoBootstrap, /OPERATOR_DASHBOARD_AUTH_PASSWORD/);
assert.match(forgejoBootstrap, /upserted repo action secret OPERATOR_DASHBOARD_AUTH_PASSWORD/);
});

View file

@ -0,0 +1,21 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
const manifest = readFileSync(new URL('../deploy/k8s/base/unrip.yaml', import.meta.url), 'utf8');
test('operator dashboard production manifest uses basic auth with password from secret', () => {
assert.match(manifest, /OPERATOR_DASHBOARD_AUTH_MODE:\s+basic/);
assert.match(manifest, /OPERATOR_DASHBOARD_AUTH_USERNAME:\s+admin/);
assert.doesNotMatch(manifest, /OPERATOR_DASHBOARD_AUTH_PASSWORD:/);
assert.match(manifest, /secretRef:\s*\n\s+name: unrip-secrets/);
});
test('operator dashboard has a public Traefik ingress with TLS', () => {
assert.match(manifest, /kind: Ingress\s*\nmetadata:\s*\n\s+name: operator-dashboard/);
assert.match(manifest, /cert-manager\.io\/cluster-issuer: letsencrypt-production/);
assert.match(manifest, /ingressClassName: traefik/);
assert.match(manifest, /host: doran\.133011\.xyz/);
assert.match(manifest, /secretName: operator-dashboard-tls/);
assert.match(manifest, /service:\s*\n\s+name: operator-dashboard\s*\n\s+port:\s*\n\s+number: 8090/);
});