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:
parent
cd5c591a2c
commit
3cd88c682e
7 changed files with 125 additions and 1 deletions
|
|
@ -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 }}
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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" \
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
20
test/deploy-workflow-static.test.mjs
Normal file
20
test/deploy-workflow-static.test.mjs
Normal 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/);
|
||||||
|
});
|
||||||
21
test/operator-dashboard-public-ingress-static.test.mjs
Normal file
21
test/operator-dashboard-public-ingress-static.test.mjs
Normal 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/);
|
||||||
|
});
|
||||||
Loading…
Add table
Reference in a new issue