Compare commits
2 Commits
feat/authe
...
claude/ci1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
503a225ca6 | ||
|
|
63c3753111 |
@@ -1,18 +0,0 @@
|
|||||||
# Authentik OIDC client registration sweep
|
|
||||||
|
|
||||||
This directory holds the FlowerCore per-service OIDC client secret references
|
|
||||||
for the ADR-093 / ADR-124 Phase 1 step 8 sweep.
|
|
||||||
|
|
||||||
The `clients/*-oidc-client.yaml` manifests are intentionally only
|
|
||||||
`OnePasswordItem` CRDs. The actual 1Password items are created by an operator in
|
|
||||||
the `IAmWorkin` vault with these fields:
|
|
||||||
|
|
||||||
| Field | Purpose |
|
|
||||||
| --- | --- |
|
|
||||||
| `client_id` | Authentik provider client id, default `<slug>` |
|
|
||||||
| `client_secret` | Authentik provider client secret |
|
|
||||||
| `issuer_url` | `https://id.iamworkin.lan/application/o/<slug>/` |
|
|
||||||
|
|
||||||
Run `scripts/authentik-bulk-client-create.py` in dry-run mode first. Live REST
|
|
||||||
mutation requires `--apply`, `AUTHENTIK_TOKEN`, and an operator-provided
|
|
||||||
client-secret JSON file. The script redacts secrets in all normal output.
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
apiVersion: onepassword.com/v1
|
|
||||||
kind: OnePasswordItem
|
|
||||||
metadata:
|
|
||||||
name: aistation-oidc-client
|
|
||||||
namespace: fc-aistation
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
app.kubernetes.io/component: authentik-oidc-client
|
|
||||||
flowercore.io/authentik-client-slug: aistation
|
|
||||||
annotations:
|
|
||||||
flowercore.io/onepassword-item: "IAmWorkin/items/aistation-oidc-client"
|
|
||||||
flowercore.io/expected-fields: "client_id,client_secret,issuer_url"
|
|
||||||
spec:
|
|
||||||
itemPath: "vaults/IAmWorkin/items/aistation-oidc-client"
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
apiVersion: onepassword.com/v1
|
|
||||||
kind: OnePasswordItem
|
|
||||||
metadata:
|
|
||||||
name: audit-oidc-client
|
|
||||||
namespace: fc-audit
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
app.kubernetes.io/component: authentik-oidc-client
|
|
||||||
flowercore.io/authentik-client-slug: audit
|
|
||||||
annotations:
|
|
||||||
flowercore.io/onepassword-item: "IAmWorkin/items/audit-oidc-client"
|
|
||||||
flowercore.io/expected-fields: "client_id,client_secret,issuer_url"
|
|
||||||
spec:
|
|
||||||
itemPath: "vaults/IAmWorkin/items/audit-oidc-client"
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
apiVersion: onepassword.com/v1
|
|
||||||
kind: OnePasswordItem
|
|
||||||
metadata:
|
|
||||||
name: chat-oidc-client
|
|
||||||
namespace: fc-chat
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
app.kubernetes.io/component: authentik-oidc-client
|
|
||||||
flowercore.io/authentik-client-slug: chat
|
|
||||||
annotations:
|
|
||||||
flowercore.io/onepassword-item: "IAmWorkin/items/chat-oidc-client"
|
|
||||||
flowercore.io/expected-fields: "client_id,client_secret,issuer_url"
|
|
||||||
spec:
|
|
||||||
itemPath: "vaults/IAmWorkin/items/chat-oidc-client"
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
apiVersion: onepassword.com/v1
|
|
||||||
kind: OnePasswordItem
|
|
||||||
metadata:
|
|
||||||
name: distribution-oidc-client
|
|
||||||
namespace: fc-distribution
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
app.kubernetes.io/component: authentik-oidc-client
|
|
||||||
flowercore.io/authentik-client-slug: distribution
|
|
||||||
annotations:
|
|
||||||
flowercore.io/onepassword-item: "IAmWorkin/items/distribution-oidc-client"
|
|
||||||
flowercore.io/expected-fields: "client_id,client_secret,issuer_url"
|
|
||||||
spec:
|
|
||||||
itemPath: "vaults/IAmWorkin/items/distribution-oidc-client"
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
apiVersion: onepassword.com/v1
|
|
||||||
kind: OnePasswordItem
|
|
||||||
metadata:
|
|
||||||
name: dms-oidc-client
|
|
||||||
namespace: fc-dms
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
app.kubernetes.io/component: authentik-oidc-client
|
|
||||||
flowercore.io/authentik-client-slug: dms
|
|
||||||
annotations:
|
|
||||||
flowercore.io/onepassword-item: "IAmWorkin/items/dms-oidc-client"
|
|
||||||
flowercore.io/expected-fields: "client_id,client_secret,issuer_url"
|
|
||||||
spec:
|
|
||||||
itemPath: "vaults/IAmWorkin/items/dms-oidc-client"
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
apiVersion: onepassword.com/v1
|
|
||||||
kind: OnePasswordItem
|
|
||||||
metadata:
|
|
||||||
name: dns-oidc-client
|
|
||||||
namespace: fc-dns
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
app.kubernetes.io/component: authentik-oidc-client
|
|
||||||
flowercore.io/authentik-client-slug: dns
|
|
||||||
annotations:
|
|
||||||
flowercore.io/onepassword-item: "IAmWorkin/items/dns-oidc-client"
|
|
||||||
flowercore.io/expected-fields: "client_id,client_secret,issuer_url"
|
|
||||||
spec:
|
|
||||||
itemPath: "vaults/IAmWorkin/items/dns-oidc-client"
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
apiVersion: onepassword.com/v1
|
|
||||||
kind: OnePasswordItem
|
|
||||||
metadata:
|
|
||||||
name: intranet-oidc-client
|
|
||||||
namespace: intranet
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
app.kubernetes.io/component: authentik-oidc-client
|
|
||||||
flowercore.io/authentik-client-slug: intranet
|
|
||||||
annotations:
|
|
||||||
flowercore.io/onepassword-item: "IAmWorkin/items/intranet-oidc-client"
|
|
||||||
flowercore.io/expected-fields: "client_id,client_secret,issuer_url"
|
|
||||||
spec:
|
|
||||||
itemPath: "vaults/IAmWorkin/items/intranet-oidc-client"
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
apiVersion: onepassword.com/v1
|
|
||||||
kind: OnePasswordItem
|
|
||||||
metadata:
|
|
||||||
name: irc-oidc-client
|
|
||||||
namespace: irc
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
app.kubernetes.io/component: authentik-oidc-client
|
|
||||||
flowercore.io/authentik-client-slug: irc
|
|
||||||
annotations:
|
|
||||||
flowercore.io/onepassword-item: "IAmWorkin/items/irc-oidc-client"
|
|
||||||
flowercore.io/expected-fields: "client_id,client_secret,issuer_url"
|
|
||||||
spec:
|
|
||||||
itemPath: "vaults/IAmWorkin/items/irc-oidc-client"
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
apiVersion: onepassword.com/v1
|
|
||||||
kind: OnePasswordItem
|
|
||||||
metadata:
|
|
||||||
name: kiosk-oidc-client
|
|
||||||
namespace: fc-system
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
app.kubernetes.io/component: authentik-oidc-client
|
|
||||||
flowercore.io/authentik-client-slug: kiosk
|
|
||||||
annotations:
|
|
||||||
flowercore.io/onepassword-item: "IAmWorkin/items/kiosk-oidc-client"
|
|
||||||
flowercore.io/expected-fields: "client_id,client_secret,issuer_url"
|
|
||||||
spec:
|
|
||||||
itemPath: "vaults/IAmWorkin/items/kiosk-oidc-client"
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
apiVersion: onepassword.com/v1
|
|
||||||
kind: OnePasswordItem
|
|
||||||
metadata:
|
|
||||||
name: knowledge-oidc-client
|
|
||||||
namespace: knowledge
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
app.kubernetes.io/component: authentik-oidc-client
|
|
||||||
flowercore.io/authentik-client-slug: knowledge
|
|
||||||
annotations:
|
|
||||||
flowercore.io/onepassword-item: "IAmWorkin/items/knowledge-oidc-client"
|
|
||||||
flowercore.io/expected-fields: "client_id,client_secret,issuer_url"
|
|
||||||
spec:
|
|
||||||
itemPath: "vaults/IAmWorkin/items/knowledge-oidc-client"
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
apiVersion: onepassword.com/v1
|
|
||||||
kind: OnePasswordItem
|
|
||||||
metadata:
|
|
||||||
name: library-oidc-client
|
|
||||||
namespace: fc-library
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
app.kubernetes.io/component: authentik-oidc-client
|
|
||||||
flowercore.io/authentik-client-slug: library
|
|
||||||
annotations:
|
|
||||||
flowercore.io/onepassword-item: "IAmWorkin/items/library-oidc-client"
|
|
||||||
flowercore.io/expected-fields: "client_id,client_secret,issuer_url"
|
|
||||||
spec:
|
|
||||||
itemPath: "vaults/IAmWorkin/items/library-oidc-client"
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
apiVersion: onepassword.com/v1
|
|
||||||
kind: OnePasswordItem
|
|
||||||
metadata:
|
|
||||||
name: licensing-oidc-client
|
|
||||||
namespace: fc-licensing
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
app.kubernetes.io/component: authentik-oidc-client
|
|
||||||
flowercore.io/authentik-client-slug: licensing
|
|
||||||
annotations:
|
|
||||||
flowercore.io/onepassword-item: "IAmWorkin/items/licensing-oidc-client"
|
|
||||||
flowercore.io/expected-fields: "client_id,client_secret,issuer_url"
|
|
||||||
spec:
|
|
||||||
itemPath: "vaults/IAmWorkin/items/licensing-oidc-client"
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
apiVersion: onepassword.com/v1
|
|
||||||
kind: OnePasswordItem
|
|
||||||
metadata:
|
|
||||||
name: llmbridge-oidc-client
|
|
||||||
namespace: fc-llm-bridge
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
app.kubernetes.io/component: authentik-oidc-client
|
|
||||||
flowercore.io/authentik-client-slug: llmbridge
|
|
||||||
annotations:
|
|
||||||
flowercore.io/onepassword-item: "IAmWorkin/items/llmbridge-oidc-client"
|
|
||||||
flowercore.io/expected-fields: "client_id,client_secret,issuer_url"
|
|
||||||
spec:
|
|
||||||
itemPath: "vaults/IAmWorkin/items/llmbridge-oidc-client"
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
apiVersion: onepassword.com/v1
|
|
||||||
kind: OnePasswordItem
|
|
||||||
metadata:
|
|
||||||
name: media-oidc-client
|
|
||||||
namespace: fc-media
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
app.kubernetes.io/component: authentik-oidc-client
|
|
||||||
flowercore.io/authentik-client-slug: media
|
|
||||||
annotations:
|
|
||||||
flowercore.io/onepassword-item: "IAmWorkin/items/media-oidc-client"
|
|
||||||
flowercore.io/expected-fields: "client_id,client_secret,issuer_url"
|
|
||||||
spec:
|
|
||||||
itemPath: "vaults/IAmWorkin/items/media-oidc-client"
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
apiVersion: onepassword.com/v1
|
|
||||||
kind: OnePasswordItem
|
|
||||||
metadata:
|
|
||||||
name: menuboard-oidc-client
|
|
||||||
namespace: fc-menuboard
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
app.kubernetes.io/component: authentik-oidc-client
|
|
||||||
flowercore.io/authentik-client-slug: menuboard
|
|
||||||
annotations:
|
|
||||||
flowercore.io/onepassword-item: "IAmWorkin/items/menuboard-oidc-client"
|
|
||||||
flowercore.io/expected-fields: "client_id,client_secret,issuer_url"
|
|
||||||
spec:
|
|
||||||
itemPath: "vaults/IAmWorkin/items/menuboard-oidc-client"
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
apiVersion: onepassword.com/v1
|
|
||||||
kind: OnePasswordItem
|
|
||||||
metadata:
|
|
||||||
name: messageboard-oidc-client
|
|
||||||
namespace: fc-messageboard
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
app.kubernetes.io/component: authentik-oidc-client
|
|
||||||
flowercore.io/authentik-client-slug: messageboard
|
|
||||||
annotations:
|
|
||||||
flowercore.io/onepassword-item: "IAmWorkin/items/messageboard-oidc-client"
|
|
||||||
flowercore.io/expected-fields: "client_id,client_secret,issuer_url"
|
|
||||||
spec:
|
|
||||||
itemPath: "vaults/IAmWorkin/items/messageboard-oidc-client"
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
apiVersion: onepassword.com/v1
|
|
||||||
kind: OnePasswordItem
|
|
||||||
metadata:
|
|
||||||
name: mike-bundle-oidc-client
|
|
||||||
namespace: fc-mike-bundle
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
app.kubernetes.io/component: authentik-oidc-client
|
|
||||||
flowercore.io/authentik-client-slug: mike-bundle
|
|
||||||
annotations:
|
|
||||||
flowercore.io/onepassword-item: "IAmWorkin/items/mike-bundle-oidc-client"
|
|
||||||
flowercore.io/expected-fields: "client_id,client_secret,issuer_url"
|
|
||||||
spec:
|
|
||||||
itemPath: "vaults/IAmWorkin/items/mike-bundle-oidc-client"
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
apiVersion: onepassword.com/v1
|
|
||||||
kind: OnePasswordItem
|
|
||||||
metadata:
|
|
||||||
name: mndot-oidc-client
|
|
||||||
namespace: fc-mndot
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
app.kubernetes.io/component: authentik-oidc-client
|
|
||||||
flowercore.io/authentik-client-slug: mndot
|
|
||||||
annotations:
|
|
||||||
flowercore.io/onepassword-item: "IAmWorkin/items/mndot-oidc-client"
|
|
||||||
flowercore.io/expected-fields: "client_id,client_secret,issuer_url"
|
|
||||||
spec:
|
|
||||||
itemPath: "vaults/IAmWorkin/items/mndot-oidc-client"
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
apiVersion: onepassword.com/v1
|
|
||||||
kind: OnePasswordItem
|
|
||||||
metadata:
|
|
||||||
name: mysql-oidc-client
|
|
||||||
namespace: fc-mysql
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
app.kubernetes.io/component: authentik-oidc-client
|
|
||||||
flowercore.io/authentik-client-slug: mysql
|
|
||||||
annotations:
|
|
||||||
flowercore.io/onepassword-item: "IAmWorkin/items/mysql-oidc-client"
|
|
||||||
flowercore.io/expected-fields: "client_id,client_secret,issuer_url"
|
|
||||||
spec:
|
|
||||||
itemPath: "vaults/IAmWorkin/items/mysql-oidc-client"
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
apiVersion: onepassword.com/v1
|
|
||||||
kind: OnePasswordItem
|
|
||||||
metadata:
|
|
||||||
name: php-oidc-client
|
|
||||||
namespace: fc-php
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
app.kubernetes.io/component: authentik-oidc-client
|
|
||||||
flowercore.io/authentik-client-slug: php
|
|
||||||
annotations:
|
|
||||||
flowercore.io/onepassword-item: "IAmWorkin/items/php-oidc-client"
|
|
||||||
flowercore.io/expected-fields: "client_id,client_secret,issuer_url"
|
|
||||||
spec:
|
|
||||||
itemPath: "vaults/IAmWorkin/items/php-oidc-client"
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
apiVersion: onepassword.com/v1
|
|
||||||
kind: OnePasswordItem
|
|
||||||
metadata:
|
|
||||||
name: pimanager-oidc-client
|
|
||||||
namespace: fc-pimanager
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
app.kubernetes.io/component: authentik-oidc-client
|
|
||||||
flowercore.io/authentik-client-slug: pimanager
|
|
||||||
annotations:
|
|
||||||
flowercore.io/onepassword-item: "IAmWorkin/items/pimanager-oidc-client"
|
|
||||||
flowercore.io/expected-fields: "client_id,client_secret,issuer_url"
|
|
||||||
spec:
|
|
||||||
itemPath: "vaults/IAmWorkin/items/pimanager-oidc-client"
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
apiVersion: onepassword.com/v1
|
|
||||||
kind: OnePasswordItem
|
|
||||||
metadata:
|
|
||||||
name: presentations-oidc-client
|
|
||||||
namespace: fc-presentations
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
app.kubernetes.io/component: authentik-oidc-client
|
|
||||||
flowercore.io/authentik-client-slug: presentations
|
|
||||||
annotations:
|
|
||||||
flowercore.io/onepassword-item: "IAmWorkin/items/presentations-oidc-client"
|
|
||||||
flowercore.io/expected-fields: "client_id,client_secret,issuer_url"
|
|
||||||
spec:
|
|
||||||
itemPath: "vaults/IAmWorkin/items/presentations-oidc-client"
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
apiVersion: onepassword.com/v1
|
|
||||||
kind: OnePasswordItem
|
|
||||||
metadata:
|
|
||||||
name: print-oidc-client
|
|
||||||
namespace: fc-print
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
app.kubernetes.io/component: authentik-oidc-client
|
|
||||||
flowercore.io/authentik-client-slug: print
|
|
||||||
annotations:
|
|
||||||
flowercore.io/onepassword-item: "IAmWorkin/items/print-oidc-client"
|
|
||||||
flowercore.io/expected-fields: "client_id,client_secret,issuer_url"
|
|
||||||
spec:
|
|
||||||
itemPath: "vaults/IAmWorkin/items/print-oidc-client"
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
apiVersion: onepassword.com/v1
|
|
||||||
kind: OnePasswordItem
|
|
||||||
metadata:
|
|
||||||
name: provisioning-oidc-client
|
|
||||||
namespace: fc-provisioning
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
app.kubernetes.io/component: authentik-oidc-client
|
|
||||||
flowercore.io/authentik-client-slug: provisioning
|
|
||||||
annotations:
|
|
||||||
flowercore.io/onepassword-item: "IAmWorkin/items/provisioning-oidc-client"
|
|
||||||
flowercore.io/expected-fields: "client_id,client_secret,issuer_url"
|
|
||||||
spec:
|
|
||||||
itemPath: "vaults/IAmWorkin/items/provisioning-oidc-client"
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
apiVersion: onepassword.com/v1
|
|
||||||
kind: OnePasswordItem
|
|
||||||
metadata:
|
|
||||||
name: remotedesktop-oidc-client
|
|
||||||
namespace: fc-desktop
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
app.kubernetes.io/component: authentik-oidc-client
|
|
||||||
flowercore.io/authentik-client-slug: remotedesktop
|
|
||||||
annotations:
|
|
||||||
flowercore.io/onepassword-item: "IAmWorkin/items/remotedesktop-oidc-client"
|
|
||||||
flowercore.io/expected-fields: "client_id,client_secret,issuer_url"
|
|
||||||
spec:
|
|
||||||
itemPath: "vaults/IAmWorkin/items/remotedesktop-oidc-client"
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
apiVersion: onepassword.com/v1
|
|
||||||
kind: OnePasswordItem
|
|
||||||
metadata:
|
|
||||||
name: retail-oidc-client
|
|
||||||
namespace: fc-retail
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
app.kubernetes.io/component: authentik-oidc-client
|
|
||||||
flowercore.io/authentik-client-slug: retail
|
|
||||||
annotations:
|
|
||||||
flowercore.io/onepassword-item: "IAmWorkin/items/retail-oidc-client"
|
|
||||||
flowercore.io/expected-fields: "client_id,client_secret,issuer_url"
|
|
||||||
spec:
|
|
||||||
itemPath: "vaults/IAmWorkin/items/retail-oidc-client"
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
apiVersion: onepassword.com/v1
|
|
||||||
kind: OnePasswordItem
|
|
||||||
metadata:
|
|
||||||
name: scoreboards-oidc-client
|
|
||||||
namespace: fc-scoreboard
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
app.kubernetes.io/component: authentik-oidc-client
|
|
||||||
flowercore.io/authentik-client-slug: scoreboards
|
|
||||||
annotations:
|
|
||||||
flowercore.io/onepassword-item: "IAmWorkin/items/scoreboards-oidc-client"
|
|
||||||
flowercore.io/expected-fields: "client_id,client_secret,issuer_url"
|
|
||||||
spec:
|
|
||||||
itemPath: "vaults/IAmWorkin/items/scoreboards-oidc-client"
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
apiVersion: onepassword.com/v1
|
|
||||||
kind: OnePasswordItem
|
|
||||||
metadata:
|
|
||||||
name: segmentdisplay-oidc-client
|
|
||||||
namespace: fc-segmentdisplay
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
app.kubernetes.io/component: authentik-oidc-client
|
|
||||||
flowercore.io/authentik-client-slug: segmentdisplay
|
|
||||||
annotations:
|
|
||||||
flowercore.io/onepassword-item: "IAmWorkin/items/segmentdisplay-oidc-client"
|
|
||||||
flowercore.io/expected-fields: "client_id,client_secret,issuer_url"
|
|
||||||
spec:
|
|
||||||
itemPath: "vaults/IAmWorkin/items/segmentdisplay-oidc-client"
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
apiVersion: onepassword.com/v1
|
|
||||||
kind: OnePasswordItem
|
|
||||||
metadata:
|
|
||||||
name: signage-oidc-client
|
|
||||||
namespace: fc-signage
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
app.kubernetes.io/component: authentik-oidc-client
|
|
||||||
flowercore.io/authentik-client-slug: signage
|
|
||||||
annotations:
|
|
||||||
flowercore.io/onepassword-item: "IAmWorkin/items/signage-oidc-client"
|
|
||||||
flowercore.io/expected-fields: "client_id,client_secret,issuer_url"
|
|
||||||
spec:
|
|
||||||
itemPath: "vaults/IAmWorkin/items/signage-oidc-client"
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
apiVersion: onepassword.com/v1
|
|
||||||
kind: OnePasswordItem
|
|
||||||
metadata:
|
|
||||||
name: signalcontrol-oidc-client
|
|
||||||
namespace: fc-signalcontrol
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
app.kubernetes.io/component: authentik-oidc-client
|
|
||||||
flowercore.io/authentik-client-slug: signalcontrol
|
|
||||||
annotations:
|
|
||||||
flowercore.io/onepassword-item: "IAmWorkin/items/signalcontrol-oidc-client"
|
|
||||||
flowercore.io/expected-fields: "client_id,client_secret,issuer_url"
|
|
||||||
spec:
|
|
||||||
itemPath: "vaults/IAmWorkin/items/signalcontrol-oidc-client"
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
apiVersion: onepassword.com/v1
|
|
||||||
kind: OnePasswordItem
|
|
||||||
metadata:
|
|
||||||
name: telephony-oidc-client
|
|
||||||
namespace: telephony
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
app.kubernetes.io/component: authentik-oidc-client
|
|
||||||
flowercore.io/authentik-client-slug: telephony
|
|
||||||
annotations:
|
|
||||||
flowercore.io/onepassword-item: "IAmWorkin/items/telephony-oidc-client"
|
|
||||||
flowercore.io/expected-fields: "client_id,client_secret,issuer_url"
|
|
||||||
spec:
|
|
||||||
itemPath: "vaults/IAmWorkin/items/telephony-oidc-client"
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
apiVersion: onepassword.com/v1
|
|
||||||
kind: OnePasswordItem
|
|
||||||
metadata:
|
|
||||||
name: ttsreader-oidc-client
|
|
||||||
namespace: fc-ttsreader
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
app.kubernetes.io/component: authentik-oidc-client
|
|
||||||
flowercore.io/authentik-client-slug: ttsreader
|
|
||||||
annotations:
|
|
||||||
flowercore.io/onepassword-item: "IAmWorkin/items/ttsreader-oidc-client"
|
|
||||||
flowercore.io/expected-fields: "client_id,client_secret,issuer_url"
|
|
||||||
spec:
|
|
||||||
itemPath: "vaults/IAmWorkin/items/ttsreader-oidc-client"
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
apiVersion: onepassword.com/v1
|
|
||||||
kind: OnePasswordItem
|
|
||||||
metadata:
|
|
||||||
name: worldbuilder-oidc-client
|
|
||||||
namespace: fc-worldbuilder
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
app.kubernetes.io/component: authentik-oidc-client
|
|
||||||
flowercore.io/authentik-client-slug: worldbuilder
|
|
||||||
annotations:
|
|
||||||
flowercore.io/onepassword-item: "IAmWorkin/items/worldbuilder-oidc-client"
|
|
||||||
flowercore.io/expected-fields: "client_id,client_secret,issuer_url"
|
|
||||||
spec:
|
|
||||||
itemPath: "vaults/IAmWorkin/items/worldbuilder-oidc-client"
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
# ArgoCD's bluejay-infra ApplicationSet sees apps/authentik as one app. Keep
|
|
||||||
# an explicit resource list so the client manifests can live under clients/.
|
|
||||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
|
||||||
kind: Kustomization
|
|
||||||
resources:
|
|
||||||
- clients/library-oidc-client.yaml
|
|
||||||
- clients/retail-oidc-client.yaml
|
|
||||||
- clients/telephony-oidc-client.yaml
|
|
||||||
- clients/knowledge-oidc-client.yaml
|
|
||||||
- clients/llmbridge-oidc-client.yaml
|
|
||||||
- clients/mysql-oidc-client.yaml
|
|
||||||
- clients/php-oidc-client.yaml
|
|
||||||
- clients/signage-oidc-client.yaml
|
|
||||||
- clients/media-oidc-client.yaml
|
|
||||||
- clients/dms-oidc-client.yaml
|
|
||||||
- clients/pimanager-oidc-client.yaml
|
|
||||||
- clients/distribution-oidc-client.yaml
|
|
||||||
- clients/dns-oidc-client.yaml
|
|
||||||
- clients/print-oidc-client.yaml
|
|
||||||
- clients/aistation-oidc-client.yaml
|
|
||||||
- clients/irc-oidc-client.yaml
|
|
||||||
- clients/ttsreader-oidc-client.yaml
|
|
||||||
- clients/chat-oidc-client.yaml
|
|
||||||
- clients/intranet-oidc-client.yaml
|
|
||||||
- clients/remotedesktop-oidc-client.yaml
|
|
||||||
- clients/provisioning-oidc-client.yaml
|
|
||||||
- clients/scoreboards-oidc-client.yaml
|
|
||||||
- clients/mndot-oidc-client.yaml
|
|
||||||
- clients/kiosk-oidc-client.yaml
|
|
||||||
- clients/mike-bundle-oidc-client.yaml
|
|
||||||
- clients/messageboard-oidc-client.yaml
|
|
||||||
- clients/menuboard-oidc-client.yaml
|
|
||||||
- clients/presentations-oidc-client.yaml
|
|
||||||
- clients/segmentdisplay-oidc-client.yaml
|
|
||||||
- clients/signalcontrol-oidc-client.yaml
|
|
||||||
- clients/worldbuilder-oidc-client.yaml
|
|
||||||
- clients/audit-oidc-client.yaml
|
|
||||||
- clients/licensing-oidc-client.yaml
|
|
||||||
@@ -1,171 +0,0 @@
|
|||||||
# fc-redis — SignalR backplane for cross-product event bus
|
|
||||||
#
|
|
||||||
# Lands per Q-SO-1 resolution (2026-05-11 PM): SignalR backplane in Phase A,
|
|
||||||
# not Phase C as originally drafted. Operator directive: "Redis can be
|
|
||||||
# deployed just fine as it's another FlowerCore technology we'll want to
|
|
||||||
# manage."
|
|
||||||
#
|
|
||||||
# Phase A scope (this file):
|
|
||||||
# - Single Redis 7.x Alpine pod
|
|
||||||
# - 1Gi Longhorn RWO PVC for AOF persistence
|
|
||||||
# - ClusterIP Service at `redis.fc-redis.svc.cluster.local:6379`
|
|
||||||
# - No AUTH (in-cluster only; not exposed externally)
|
|
||||||
# - No IngressRoute (backplane is server-to-server only)
|
|
||||||
#
|
|
||||||
# Consumers (Phase A IMPL across FC services):
|
|
||||||
# - FlowerCore.Signage.Web (OpsConsoleHub)
|
|
||||||
# - FlowerCore.Scoreboard.Web (ScoreboardHub)
|
|
||||||
# - FlowerCore.SignalControl.Web
|
|
||||||
# - FlowerCore.DMS.Web
|
|
||||||
# - Any other product joining the cross-product event bus
|
|
||||||
#
|
|
||||||
# Each consumer adds:
|
|
||||||
# services.AddSignalR()
|
|
||||||
# .AddStackExchangeRedis(
|
|
||||||
# "redis.fc-redis.svc.cluster.local:6379",
|
|
||||||
# opts => opts.Configuration.ChannelPrefix =
|
|
||||||
# StackExchange.Redis.RedisChannel.Literal("fc-opsconsole"));
|
|
||||||
#
|
|
||||||
# Phase B / C follow-ons (out of scope here):
|
|
||||||
# - Redis Sentinel for HA (3-node)
|
|
||||||
# - AUTH password from 1Password Connect (rotate via /rotate-password)
|
|
||||||
# - redis_exporter sidecar for Prometheus scrape
|
|
||||||
# - Network policies restricting which namespaces can dial 6379
|
|
||||||
#
|
|
||||||
# Design: docs/signage/operations-console-phase-2-design.md §3.5
|
|
||||||
# Decision: Q-SO-1 (RESOLVED 2026-05-11 PM)
|
|
||||||
# Memory: feedback_blooming_ui_pattern_no_iframes
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Namespace
|
|
||||||
metadata:
|
|
||||||
name: fc-redis
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
app.kubernetes.io/managed-by: argocd
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: PersistentVolumeClaim
|
|
||||||
metadata:
|
|
||||||
name: fc-redis-data
|
|
||||||
namespace: fc-redis
|
|
||||||
spec:
|
|
||||||
accessModes:
|
|
||||||
- ReadWriteOnce
|
|
||||||
storageClassName: longhorn
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
storage: 1Gi
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: ConfigMap
|
|
||||||
metadata:
|
|
||||||
name: fc-redis-config
|
|
||||||
namespace: fc-redis
|
|
||||||
data:
|
|
||||||
redis.conf: |
|
|
||||||
# Phase A — minimal config; no AUTH, no replication.
|
|
||||||
bind 0.0.0.0
|
|
||||||
protected-mode no
|
|
||||||
port 6379
|
|
||||||
tcp-backlog 511
|
|
||||||
timeout 0
|
|
||||||
tcp-keepalive 300
|
|
||||||
|
|
||||||
# Persistence: AOF (fsync every second is the standard SignalR-backplane
|
|
||||||
# durability sweet spot — the backplane only needs to survive Redis
|
|
||||||
# restarts, not absolute zero loss).
|
|
||||||
appendonly yes
|
|
||||||
appendfsync everysec
|
|
||||||
auto-aof-rewrite-percentage 100
|
|
||||||
auto-aof-rewrite-min-size 64mb
|
|
||||||
|
|
||||||
# Reasonable defaults — let Redis pick most things.
|
|
||||||
maxmemory-policy allkeys-lru
|
|
||||||
maxmemory 256mb
|
|
||||||
|
|
||||||
# Logging
|
|
||||||
loglevel notice
|
|
||||||
---
|
|
||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: fc-redis
|
|
||||||
namespace: fc-redis
|
|
||||||
labels:
|
|
||||||
app: fc-redis
|
|
||||||
spec:
|
|
||||||
replicas: 1
|
|
||||||
strategy:
|
|
||||||
type: Recreate # RWO PVC; do not do rolling update
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app: fc-redis
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app: fc-redis
|
|
||||||
spec:
|
|
||||||
securityContext:
|
|
||||||
runAsNonRoot: true
|
|
||||||
runAsUser: 999 # redis:7-alpine default uid
|
|
||||||
runAsGroup: 999
|
|
||||||
fsGroup: 999
|
|
||||||
containers:
|
|
||||||
- name: redis
|
|
||||||
image: redis:7-alpine
|
|
||||||
imagePullPolicy: IfNotPresent
|
|
||||||
command: ["redis-server", "/etc/redis/redis.conf"]
|
|
||||||
ports:
|
|
||||||
- name: redis
|
|
||||||
containerPort: 6379
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
cpu: "50m"
|
|
||||||
memory: "128Mi"
|
|
||||||
limits:
|
|
||||||
cpu: "500m"
|
|
||||||
memory: "384Mi"
|
|
||||||
volumeMounts:
|
|
||||||
- name: data
|
|
||||||
mountPath: /data
|
|
||||||
- name: config
|
|
||||||
mountPath: /etc/redis
|
|
||||||
readOnly: true
|
|
||||||
livenessProbe:
|
|
||||||
tcpSocket:
|
|
||||||
port: 6379
|
|
||||||
initialDelaySeconds: 5
|
|
||||||
periodSeconds: 10
|
|
||||||
readinessProbe:
|
|
||||||
exec:
|
|
||||||
command: ["redis-cli", "ping"]
|
|
||||||
initialDelaySeconds: 2
|
|
||||||
periodSeconds: 5
|
|
||||||
securityContext:
|
|
||||||
allowPrivilegeEscalation: false
|
|
||||||
readOnlyRootFilesystem: true
|
|
||||||
capabilities:
|
|
||||||
drop: [ALL]
|
|
||||||
volumes:
|
|
||||||
- name: data
|
|
||||||
persistentVolumeClaim:
|
|
||||||
claimName: fc-redis-data
|
|
||||||
- name: config
|
|
||||||
configMap:
|
|
||||||
name: fc-redis-config
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: redis
|
|
||||||
namespace: fc-redis
|
|
||||||
spec:
|
|
||||||
type: ClusterIP
|
|
||||||
selector:
|
|
||||||
app: fc-redis
|
|
||||||
ports:
|
|
||||||
- name: redis
|
|
||||||
port: 6379
|
|
||||||
targetPort: 6379
|
|
||||||
protocol: TCP
|
|
||||||
@@ -58,7 +58,7 @@ spec:
|
|||||||
nodeName: rke2-server
|
nodeName: rke2-server
|
||||||
containers:
|
containers:
|
||||||
- name: web
|
- name: web
|
||||||
image: localhost/fc-updater-web:v20260509-4162dca-authgate
|
image: localhost/fc-updater-web:v20260507-public-privacy
|
||||||
imagePullPolicy: Never
|
imagePullPolicy: Never
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 8080
|
- containerPort: 8080
|
||||||
|
|||||||
@@ -466,11 +466,11 @@ spec:
|
|||||||
itemPath: vaults/IAmWorkin/items/Guacamole JSON Auth
|
itemPath: vaults/IAmWorkin/items/Guacamole JSON Auth
|
||||||
---
|
---
|
||||||
---
|
---
|
||||||
# 1Password-backed credentials for Mac mini VNC access (Phase 1 <EFBFBD> 2026-04-28)
|
# 1Password-backed credentials for Mac mini VNC access (Phase 1 — 2026-04-28)
|
||||||
# The operator mints Secret 'macmini-vnc-creds' with keys: username, password, VNC Password
|
# The operator mints Secret 'macmini-vnc-creds' with keys: username, password, VNC Password
|
||||||
# Note: '1Password' field label 'VNC Password' -> K8s Secret key 'VNC Password' (space retained)
|
# Note: '1Password' field label 'VNC Password' -> K8s Secret key 'VNC Password' (space retained)
|
||||||
# Guacamole VNC connection password is sourced from the 'VNC Password' field.
|
# Guacamole VNC connection password is sourced from the 'VNC Password' field.
|
||||||
# Actual IP is 10.0.56.115 (INFRA VLAN) <EFBFBD> the 1P item 'IP' field is kept as backup reference.
|
# Actual IP is 10.0.56.115 (INFRA VLAN) — the 1P item 'IP' field is kept as backup reference.
|
||||||
apiVersion: onepassword.com/v1
|
apiVersion: onepassword.com/v1
|
||||||
kind: OnePasswordItem
|
kind: OnePasswordItem
|
||||||
metadata:
|
metadata:
|
||||||
@@ -481,7 +481,6 @@ metadata:
|
|||||||
app.kubernetes.io/part-of: flowercore
|
app.kubernetes.io/part-of: flowercore
|
||||||
spec:
|
spec:
|
||||||
itemPath: vaults/IAmWorkin/items/Mac Mini
|
itemPath: vaults/IAmWorkin/items/Mac Mini
|
||||||
---
|
|
||||||
# Blue Jay Branding Extension (CSS + translations)
|
# Blue Jay Branding Extension (CSS + translations)
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: ConfigMap
|
kind: ConfigMap
|
||||||
|
|||||||
@@ -1,9 +1,43 @@
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
# ci1 - Windows Server 2025 KubeVirt VM (GitHub Actions Self-Hosted Runner)
|
# ci1 — Windows Server 2025 KubeVirt VM (GitHub Actions Self-Hosted Runner)
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Boots from the sysprepped containerDisk template built by the Windows VM
|
# Purpose: dedicated CI runner for FlowerCore.Updater Sandbox E2E nightly +
|
||||||
# sysprep pipeline. See docs/infrastructure/windows-vm-sysprep-pipeline.md.
|
# future fleet WPF AAT lanes. Replaces the never-registered
|
||||||
# Path A/B/C install history is preserved in git log only.
|
# `bluejay-ws-sandbox-1` runner placeholder. Andrew explicitly does NOT want
|
||||||
|
# BLUEJAY-WS registered as a runner (workstation has personal/operator state).
|
||||||
|
#
|
||||||
|
# Status (2026-05-08): LIVE — Phase 1 prereqs satisfied:
|
||||||
|
# * Multus CNI v4.2.2 thick-plugin DaemonSet running on all 3 RKE2 nodes
|
||||||
|
# (apps/multus/multus.yaml; ApplicationSet `infra-multus` Synced/Healthy)
|
||||||
|
# * CDI v1.65.0 operator + CR Deployed (apps/cdi/; ApplicationSet
|
||||||
|
# `infra-cdi` Synced/Healthy; uploadproxy reachable via kubectl port-forward)
|
||||||
|
# * Windows Server 2025 ISO uploaded via CDI virtctl image-upload to
|
||||||
|
# PVC windows-server-2025-iso (7.7 GiB → 10Gi PVC, Bound, Upload Complete)
|
||||||
|
# * Local Administrator password generated, stored in 1Password vault
|
||||||
|
# IAmWorkin (qaphopopkryhbg353ukzhhuqoq) item id h3ix4mgfk65gmkcmvh6ly3d3hu
|
||||||
|
# * NetworkAttachmentDefinition prod-vlan57 registered (apps/kubevirt-vms/
|
||||||
|
# prod-vlan57-nad.yaml). VM still uses pod-network masquerade until Phase 1.5
|
||||||
|
# host bridge work lands (Puppet br-prod + enp86s0.57); switching is a
|
||||||
|
# one-line YAML edit + git push.
|
||||||
|
#
|
||||||
|
# See docs/infrastructure/windows-server-build-runner-plan.md "Phase 1 readiness gate".
|
||||||
|
#
|
||||||
|
# Network choice in this draft: **pod-network fallback** (Calico default).
|
||||||
|
# Outbound-only is fine for the Updater Sandbox E2E runner workload (the runner
|
||||||
|
# polls GitHub Actions over HTTPS; no inbound listener needed). Switch to a
|
||||||
|
# Multus PROD VLAN NetworkAttachmentDefinition once Multus is installed and the
|
||||||
|
# operator wants L2 access from `ci1` to other PROD VLAN services.
|
||||||
|
#
|
||||||
|
# Sizing: 8 vCPU / 16 GB RAM / 200 GB disk on Longhorn (default storageClass).
|
||||||
|
# Capacity check 2026-05-08: each RKE2 node has 16 vCPU / ~64Gi allocatable;
|
||||||
|
# 8 vCPU is ~17% of one node's allocatable, fits comfortably.
|
||||||
|
#
|
||||||
|
# Apply (after operator approval + ISO loaded):
|
||||||
|
# kubectl --kubeconfig $env:USERPROFILE\.kube\rke2.yaml apply -f apps/kubevirt-vms/ci1.yaml
|
||||||
|
#
|
||||||
|
# Connect to console for Windows install:
|
||||||
|
# virtctl --kubeconfig $env:USERPROFILE\.kube\rke2.yaml vnc ci1 -n kubevirt-vms
|
||||||
|
# (Or via Guacamole once a connection profile is added.)
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
@@ -15,6 +49,230 @@ metadata:
|
|||||||
pod-security.kubernetes.io/enforce: privileged
|
pod-security.kubernetes.io/enforce: privileged
|
||||||
|
|
||||||
---
|
---
|
||||||
|
# ISO PVC — populated via CDI virtctl image-upload (CDI is now installed).
|
||||||
|
# Population workflow (LIVE 2026-05-08):
|
||||||
|
# 1. virtctl --kubeconfig $env:USERPROFILE\.kube\rke2.yaml image-upload pvc \
|
||||||
|
# windows-server-2025-iso -n kubevirt-vms \
|
||||||
|
# --image-path "$env:USERPROFILE\Downloads\en-us_windows_server_2025_updated_march_2026_x64_dvd_8e06425a.iso" \
|
||||||
|
# --size 10Gi --storage-class longhorn --access-mode ReadWriteOnce \
|
||||||
|
# --uploadproxy-url https://cdi-uploadproxy.cdi.svc:443 --insecure
|
||||||
|
# (--uploadproxy-url uses port-forward in practice: see plan doc Phase 1.5.)
|
||||||
|
#
|
||||||
|
# Note: CDI's PVC creation hooks add cdi.kubevirt.io/storage.* annotations
|
||||||
|
# automatically. The ISO source file is 7.7GB → request 10Gi for headroom.
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: windows-server-2025-iso
|
||||||
|
namespace: kubevirt-vms
|
||||||
|
labels:
|
||||||
|
app: ci-runner
|
||||||
|
flowercore.io/managed-by: bluejay-infra
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce # Bump to ReadOnlyMany after population for multi-VM use
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 10Gi # Bumped from 6Gi (Server 2025 ISO is 7.7GB)
|
||||||
|
storageClassName: longhorn
|
||||||
|
|
||||||
|
---
|
||||||
|
# Root disk PVC — empty 200Gi volume that Windows installs into.
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: ci1-rootdisk
|
||||||
|
namespace: kubevirt-vms
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 200Gi
|
||||||
|
storageClassName: longhorn
|
||||||
|
|
||||||
|
---
|
||||||
|
# Sysprep ConfigMap — autounattend.xml for hands-off Windows install.
|
||||||
|
# Sets local Administrator password (REPLACE the placeholder), enables RDP,
|
||||||
|
# enables WinRM, sets hostname, and configures static-ish networking via DHCP.
|
||||||
|
# The ISO + VirtIO drivers handle the rest.
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: ci1-autounattend
|
||||||
|
namespace: kubevirt-vms
|
||||||
|
data:
|
||||||
|
autounattend.xml: |
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<unattend xmlns="urn:schemas-microsoft-com:unattend">
|
||||||
|
|
||||||
|
<!-- Pass 1: WindowsPE — Disk setup and VirtIO driver injection -->
|
||||||
|
<settings pass="windowsPE">
|
||||||
|
<component name="Microsoft-Windows-International-Core-WinPE"
|
||||||
|
processorArchitecture="amd64"
|
||||||
|
publicKeyToken="31bf3856ad364e35"
|
||||||
|
language="neutral" versionScope="nonSxS">
|
||||||
|
<SetupUILanguage>
|
||||||
|
<UILanguage>en-US</UILanguage>
|
||||||
|
</SetupUILanguage>
|
||||||
|
<InputLocale>en-US</InputLocale>
|
||||||
|
<SystemLocale>en-US</SystemLocale>
|
||||||
|
<UILanguage>en-US</UILanguage>
|
||||||
|
<UserLocale>en-US</UserLocale>
|
||||||
|
</component>
|
||||||
|
|
||||||
|
<component name="Microsoft-Windows-PnpCustomizationsWinPE"
|
||||||
|
processorArchitecture="amd64"
|
||||||
|
publicKeyToken="31bf3856ad364e35"
|
||||||
|
language="neutral" versionScope="nonSxS">
|
||||||
|
<DriverPaths>
|
||||||
|
<PathAndCredentials wcm:action="add" wcm:keyValue="1">
|
||||||
|
<Path>E:\amd64\2k25</Path>
|
||||||
|
</PathAndCredentials>
|
||||||
|
</DriverPaths>
|
||||||
|
</component>
|
||||||
|
|
||||||
|
<component name="Microsoft-Windows-Setup"
|
||||||
|
processorArchitecture="amd64"
|
||||||
|
publicKeyToken="31bf3856ad364e35"
|
||||||
|
language="neutral" versionScope="nonSxS">
|
||||||
|
<DiskConfiguration>
|
||||||
|
<Disk wcm:action="add">
|
||||||
|
<DiskID>0</DiskID>
|
||||||
|
<WillWipeDisk>true</WillWipeDisk>
|
||||||
|
<CreatePartitions>
|
||||||
|
<CreatePartition wcm:action="add">
|
||||||
|
<Order>1</Order>
|
||||||
|
<Size>260</Size>
|
||||||
|
<Type>EFI</Type>
|
||||||
|
</CreatePartition>
|
||||||
|
<CreatePartition wcm:action="add">
|
||||||
|
<Order>2</Order>
|
||||||
|
<Size>128</Size>
|
||||||
|
<Type>MSR</Type>
|
||||||
|
</CreatePartition>
|
||||||
|
<CreatePartition wcm:action="add">
|
||||||
|
<Order>3</Order>
|
||||||
|
<Extend>true</Extend>
|
||||||
|
<Type>Primary</Type>
|
||||||
|
</CreatePartition>
|
||||||
|
</CreatePartitions>
|
||||||
|
<ModifyPartitions>
|
||||||
|
<ModifyPartition wcm:action="add">
|
||||||
|
<Order>1</Order>
|
||||||
|
<PartitionID>1</PartitionID>
|
||||||
|
<Format>FAT32</Format>
|
||||||
|
<Label>EFI</Label>
|
||||||
|
</ModifyPartition>
|
||||||
|
<ModifyPartition wcm:action="add">
|
||||||
|
<Order>2</Order>
|
||||||
|
<PartitionID>2</PartitionID>
|
||||||
|
</ModifyPartition>
|
||||||
|
<ModifyPartition wcm:action="add">
|
||||||
|
<Order>3</Order>
|
||||||
|
<PartitionID>3</PartitionID>
|
||||||
|
<Format>NTFS</Format>
|
||||||
|
<Label>Windows</Label>
|
||||||
|
</ModifyPartition>
|
||||||
|
</ModifyPartitions>
|
||||||
|
</Disk>
|
||||||
|
</DiskConfiguration>
|
||||||
|
|
||||||
|
<ImageInstall>
|
||||||
|
<OSImage>
|
||||||
|
<InstallTo>
|
||||||
|
<DiskID>0</DiskID>
|
||||||
|
<PartitionID>3</PartitionID>
|
||||||
|
</InstallTo>
|
||||||
|
<!-- Index 2 = Standard Desktop Experience. Use 4 for Datacenter Desktop. -->
|
||||||
|
<InstallFrom>
|
||||||
|
<MetaData wcm:action="add">
|
||||||
|
<Key>/IMAGE/INDEX</Key>
|
||||||
|
<Value>2</Value>
|
||||||
|
</MetaData>
|
||||||
|
</InstallFrom>
|
||||||
|
</OSImage>
|
||||||
|
</ImageInstall>
|
||||||
|
|
||||||
|
<UserData>
|
||||||
|
<AcceptEula>true</AcceptEula>
|
||||||
|
<FullName>FlowerCore CI Runner</FullName>
|
||||||
|
<Organization>FlowerCore</Organization>
|
||||||
|
<!-- Eval install — no product key needed for 180-day evaluation -->
|
||||||
|
</UserData>
|
||||||
|
</component>
|
||||||
|
</settings>
|
||||||
|
|
||||||
|
<!-- Pass 4: Specialize — Hostname, RDP, WinRM -->
|
||||||
|
<settings pass="specialize">
|
||||||
|
<component name="Microsoft-Windows-Shell-Setup"
|
||||||
|
processorArchitecture="amd64"
|
||||||
|
publicKeyToken="31bf3856ad364e35"
|
||||||
|
language="neutral" versionScope="nonSxS">
|
||||||
|
<ComputerName>CI1</ComputerName>
|
||||||
|
<TimeZone>Central Standard Time</TimeZone>
|
||||||
|
</component>
|
||||||
|
|
||||||
|
<component name="Microsoft-Windows-TerminalServices-LocalSessionManager"
|
||||||
|
processorArchitecture="amd64"
|
||||||
|
publicKeyToken="31bf3856ad364e35"
|
||||||
|
language="neutral" versionScope="nonSxS">
|
||||||
|
<fDenyTSConnections>false</fDenyTSConnections>
|
||||||
|
</component>
|
||||||
|
</settings>
|
||||||
|
|
||||||
|
<!-- Pass 7: OOBE — Admin account, RDP firewall, WinRM -->
|
||||||
|
<settings pass="oobeSystem">
|
||||||
|
<component name="Microsoft-Windows-Shell-Setup"
|
||||||
|
processorArchitecture="amd64"
|
||||||
|
publicKeyToken="31bf3856ad364e35"
|
||||||
|
language="neutral" versionScope="nonSxS">
|
||||||
|
<OOBE>
|
||||||
|
<HideEULAPage>true</HideEULAPage>
|
||||||
|
<HideLocalAccountScreen>true</HideLocalAccountScreen>
|
||||||
|
<HideOEMRegistrationScreen>true</HideOEMRegistrationScreen>
|
||||||
|
<HideOnlineAccountScreens>true</HideOnlineAccountScreens>
|
||||||
|
<HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE>
|
||||||
|
<ProtectYourPC>3</ProtectYourPC>
|
||||||
|
</OOBE>
|
||||||
|
<UserAccounts>
|
||||||
|
<AdministratorPassword>
|
||||||
|
<!-- Real password is in 1Password — vault qaphopopkryhbg353ukzhhuqoq,
|
||||||
|
item id h3ix4mgfk65gmkcmvh6ly3d3hu, title:
|
||||||
|
"ci1 Administrator (Windows Server 2025 KubeVirt VM)".
|
||||||
|
Field "autounattend AdministratorPassword Value (UTF-16-LE base64)"
|
||||||
|
matches the Value below.
|
||||||
|
To rotate: regenerate, recompute base64
|
||||||
|
$combined = $pw + "AdministratorPassword"
|
||||||
|
[Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($combined))
|
||||||
|
then update both 1P item AND this Value field, recreate VM. -->
|
||||||
|
<Value>bAA3AGsANABOAHcAcgBMAG4AeQBTAHUAYgBBAHQAaQBzAFUAcAB6AEMAWQAhADkAYQBCAEEAZABtAGkAbgBpAHMAdAByAGEAdABvAHIAUABhAHMAcwB3AG8AcgBkAA==</Value>
|
||||||
|
<PlainText>false</PlainText>
|
||||||
|
</AdministratorPassword>
|
||||||
|
</UserAccounts>
|
||||||
|
<FirstLogonCommands>
|
||||||
|
<SynchronousCommand wcm:action="add">
|
||||||
|
<Order>1</Order>
|
||||||
|
<CommandLine>powershell.exe -ExecutionPolicy Bypass -Command "Set-NetFirewallRule -DisplayGroup 'Remote Desktop' -Enabled True"</CommandLine>
|
||||||
|
<Description>Enable RDP firewall rule</Description>
|
||||||
|
</SynchronousCommand>
|
||||||
|
<SynchronousCommand wcm:action="add">
|
||||||
|
<Order>2</Order>
|
||||||
|
<CommandLine>powershell.exe -ExecutionPolicy Bypass -Command "Enable-PSRemoting -Force; Set-Item WSMan:\localhost\Service\Auth\Basic $true; Set-Item WSMan:\localhost\Service\AllowUnencrypted $true"</CommandLine>
|
||||||
|
<Description>Enable WinRM (Phase 2 will pivot to HTTPS via step-ca cert)</Description>
|
||||||
|
</SynchronousCommand>
|
||||||
|
<SynchronousCommand wcm:action="add">
|
||||||
|
<Order>3</Order>
|
||||||
|
<CommandLine>cmd.exe /c reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" /v EnableLUA /t REG_DWORD /d 0 /f</CommandLine>
|
||||||
|
<Description>Disable UAC (Phase 2 Puppet will re-evaluate)</Description>
|
||||||
|
</SynchronousCommand>
|
||||||
|
</FirstLogonCommands>
|
||||||
|
</component>
|
||||||
|
</settings>
|
||||||
|
</unattend>
|
||||||
|
|
||||||
|
---
|
||||||
|
# VirtualMachine — Windows Server 2025 CI runner.
|
||||||
apiVersion: kubevirt.io/v1
|
apiVersion: kubevirt.io/v1
|
||||||
kind: VirtualMachine
|
kind: VirtualMachine
|
||||||
metadata:
|
metadata:
|
||||||
@@ -25,7 +283,7 @@ metadata:
|
|||||||
role: github-actions-runner
|
role: github-actions-runner
|
||||||
flowercore.io/managed-by: bluejay-infra
|
flowercore.io/managed-by: bluejay-infra
|
||||||
spec:
|
spec:
|
||||||
runStrategy: Always
|
running: true # LIVE — ISO uploaded 2026-05-08, password in 1P
|
||||||
template:
|
template:
|
||||||
metadata:
|
metadata:
|
||||||
labels:
|
labels:
|
||||||
@@ -67,16 +325,31 @@ spec:
|
|||||||
firmware:
|
firmware:
|
||||||
bootloader:
|
bootloader:
|
||||||
efi:
|
efi:
|
||||||
secureBoot: false
|
secureBoot: true
|
||||||
devices:
|
devices:
|
||||||
tpm: {}
|
tpm: {} # Non-persistent vTPM — sufficient for runner; no BitLocker
|
||||||
disks:
|
disks:
|
||||||
- name: rootdisk
|
- name: rootdisk
|
||||||
|
bootOrder: 1
|
||||||
disk:
|
disk:
|
||||||
bus: virtio
|
bus: virtio
|
||||||
|
- name: windows-iso
|
||||||
|
bootOrder: 2
|
||||||
|
cdrom:
|
||||||
|
bus: sata
|
||||||
|
- name: virtio-drivers
|
||||||
|
cdrom:
|
||||||
|
bus: sata
|
||||||
|
- name: sysprep
|
||||||
|
cdrom:
|
||||||
|
bus: sata
|
||||||
interfaces:
|
interfaces:
|
||||||
# Pod-network fallback for CI runner outbound traffic. Switch to
|
# Pod-network fallback for Phase 1. To switch to PROD VLAN once Multus
|
||||||
# prod-vlan57 once the bridge/NAD lane is ready for L2 access.
|
# + the prod-vlan57 NAD exist, replace this block with:
|
||||||
|
# - name: prod-net
|
||||||
|
# bridge: {}
|
||||||
|
# model: virtio
|
||||||
|
# and update the networks: stanza to use multus.networkName: kubevirt-vms/prod-vlan57
|
||||||
- name: default
|
- name: default
|
||||||
masquerade: {}
|
masquerade: {}
|
||||||
model: virtio
|
model: virtio
|
||||||
@@ -87,7 +360,23 @@ spec:
|
|||||||
pod: {}
|
pod: {}
|
||||||
volumes:
|
volumes:
|
||||||
- name: rootdisk
|
- name: rootdisk
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: ci1-rootdisk
|
||||||
|
- name: windows-iso
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: windows-server-2025-iso
|
||||||
|
- name: virtio-drivers
|
||||||
containerDisk:
|
containerDisk:
|
||||||
image: localhost/fc-win-server-2025:v1
|
# Pinned to v1.8.2 (latest stable as of 2026-05-08).
|
||||||
imagePullPolicy: Never
|
# The :latest tag uses Docker manifest v1 schema which containerd
|
||||||
|
# 2.1 (RKE2 v1.34.5) refuses to pull with:
|
||||||
|
# "media type application/vnd.docker.distribution.manifest.v1+prettyjws
|
||||||
|
# is no longer supported since containerd v2.1"
|
||||||
|
# v1.8.2 is rebuilt with manifest v2/OCI and works on containerd 2.1.
|
||||||
|
# Bump available: https://quay.io/repository/kubevirt/virtio-container-disk?tab=tags
|
||||||
|
image: quay.io/kubevirt/virtio-container-disk:v1.8.2
|
||||||
|
- name: sysprep
|
||||||
|
sysprep:
|
||||||
|
configMap:
|
||||||
|
name: ci1-autounattend
|
||||||
terminationGracePeriodSeconds: 3600
|
terminationGracePeriodSeconds: 3600
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
resources:
|
|
||||||
- ci1.yaml
|
|
||||||
- prod-vlan57-nad.yaml
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
# =============================================================================
|
|
||||||
# Windows Server 2025 ISO — Static NFS PV (Path B for SATA-CDROM timeout)
|
|
||||||
# =============================================================================
|
|
||||||
# Purpose: Mount the ISO from Synology NAS via NFS instead of from a Longhorn-
|
|
||||||
# backed Filesystem PVC.
|
|
||||||
#
|
|
||||||
# Why: SATA-CDROM emulation reading from a Longhorn-backed Filesystem PVC is
|
|
||||||
# too slow for OVMF's boot read window — the DVD-ROM enumeration times out
|
|
||||||
# before the bootloader can be read. Symptom on the serial console:
|
|
||||||
# BdsDxe: failed to start Boot0001 "UEFI QEMU DVD-ROM QM00001 " from ...
|
|
||||||
# BdsDxe: failed to start Boot0001 ... Time out
|
|
||||||
# BdsDxe: No bootable option or device was found
|
|
||||||
# Diagnosis confirmed the ISO content is a perfectly valid bootable ISO9660
|
|
||||||
# image — the bug is in the timing path between OVMF and Longhorn-backed
|
|
||||||
# storage, not in the ISO itself.
|
|
||||||
#
|
|
||||||
# Block-mode PVC was tried (`volumeMode: Block` via DataVolume) and would
|
|
||||||
# likely fix the timing, but CDI v1.65.0's upload-target pod cannot open the
|
|
||||||
# block device due to runAsUser:107 + capabilities.drop:[ALL] and we got:
|
|
||||||
# blockdev: cannot open /dev/cdi-block-volume: Permission denied
|
|
||||||
#
|
|
||||||
# NFS-mounted ISO bypasses both issues: no Longhorn slowness, no CDI upload
|
|
||||||
# pod permission concerns. The ISO is read directly from the NAS over a
|
|
||||||
# native NFSv4.1 mount that QEMU's SATA emulator can read at full LAN speed.
|
|
||||||
#
|
|
||||||
# Layout on Synology:
|
|
||||||
# /volume1/ISOs/ (existing export, RKE2 ACL)
|
|
||||||
# en-us_windows_server_2025_updated_march_2026_x64_dvd_8e06425a.iso
|
|
||||||
# win2025-iso-disk/ (new subdir, 2026-05-08)
|
|
||||||
# disk.img -> hardlink to ../en-us_windows_server_2025_..._8e06425a.iso
|
|
||||||
#
|
|
||||||
# KubeVirt's launcher pod expects a PVC mounted at
|
|
||||||
# /var/run/kubevirt-private/vmi-disks/<diskName>/disk.img — by mounting the
|
|
||||||
# `win2025-iso-disk/` subdir as the NFS PV root, `disk.img` lives at the PV's
|
|
||||||
# root and KubeVirt's CDROM emulator finds it without any path manipulation.
|
|
||||||
#
|
|
||||||
# A symlink would NOT work for sub-path NFS mounts (the relative target
|
|
||||||
# `../...iso` falls outside the sub-mount root). A hardlink works because it
|
|
||||||
# references the same inode regardless of mount point.
|
|
||||||
#
|
|
||||||
# Memory references:
|
|
||||||
# - feedback_synology_nfs_volume1_kubernetes_export_scoped (Synology export
|
|
||||||
# scoping pattern — but /volume1/ISOs export, unlike /volume1/kubernetes,
|
|
||||||
# does support sub-path mounts because Synology NFS is configured with
|
|
||||||
# pseudo-fs in NFSv4.1)
|
|
||||||
# - feedback_kubevirt_iso_first_install_bootorder_and_runstrategy (boot
|
|
||||||
# order / runStrategy gotchas, separate from the storage timing issue)
|
|
||||||
#
|
|
||||||
# Validation (2026-05-08, from rke2-server / rke2-agent1 / rke2-agent2):
|
|
||||||
# mount -t nfs -o nfsvers=4.1,ro 10.0.58.3:/volume1/ISOs/win2025-iso-disk /tmp/m
|
|
||||||
# file /tmp/m/disk.img
|
|
||||||
# -> ISO 9660 CD-ROM filesystem data 'SSS_X64FRE_EN-US_DV9' (bootable)
|
|
||||||
# All 3 RKE2 nodes can mount and read.
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
apiVersion: v1
|
|
||||||
kind: PersistentVolume
|
|
||||||
metadata:
|
|
||||||
name: windows-server-2025-iso-nfs
|
|
||||||
labels:
|
|
||||||
flowercore.io/iso: windows-server-2025
|
|
||||||
flowercore.io/managed-by: bluejay-infra
|
|
||||||
spec:
|
|
||||||
capacity:
|
|
||||||
storage: 8Gi
|
|
||||||
accessModes:
|
|
||||||
- ReadOnlyMany
|
|
||||||
volumeMode: Filesystem
|
|
||||||
persistentVolumeReclaimPolicy: Retain
|
|
||||||
storageClassName: "" # static, no provisioner
|
|
||||||
mountOptions:
|
|
||||||
- nfsvers=4.1
|
|
||||||
- ro
|
|
||||||
- hard
|
|
||||||
- timeo=600
|
|
||||||
- retrans=3
|
|
||||||
nfs:
|
|
||||||
server: 10.0.58.3 # BlueJayNAS Synology DS1621+ on HOME VLAN 58
|
|
||||||
path: /volume1/ISOs/win2025-iso-disk
|
|
||||||
readOnly: true
|
|
||||||
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: PersistentVolumeClaim
|
|
||||||
metadata:
|
|
||||||
name: windows-server-2025-iso-nfs
|
|
||||||
namespace: kubevirt-vms
|
|
||||||
labels:
|
|
||||||
app: ci-runner
|
|
||||||
flowercore.io/managed-by: bluejay-infra
|
|
||||||
spec:
|
|
||||||
accessModes:
|
|
||||||
- ReadOnlyMany
|
|
||||||
volumeMode: Filesystem
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
storage: 8Gi
|
|
||||||
storageClassName: ""
|
|
||||||
volumeName: windows-server-2025-iso-nfs
|
|
||||||
@@ -974,39 +974,6 @@ data:
|
|||||||
summary: "Deployment {{ $labels.namespace }}/{{ $labels.deployment }} replica mismatch"
|
summary: "Deployment {{ $labels.namespace }}/{{ $labels.deployment }} replica mismatch"
|
||||||
description: "Spec wants {{ $labels.spec_replicas }} but only {{ $value }} available. Likely a rollout stuck on probe failure, scheduling, or PVC."
|
description: "Spec wants {{ $labels.spec_replicas }} but only {{ $value }} available. Likely a rollout stuck on probe failure, scheduling, or PVC."
|
||||||
|
|
||||||
# Q-MR-3 (2026-05-11): multus memory pressure — catches the next OOM
|
|
||||||
# cascade BEFORE multus is OOM-killed cluster-wide. The 2026-05-10
|
|
||||||
# outage (21h) hit because no alert fired on the rising multus working
|
|
||||||
# set — only downstream blackbox / Traefik / service alerts. With
|
|
||||||
# 1Gi limit (bluejay-infra@eb8693e), 80% = ~800MiB; steady-state
|
|
||||||
# runs ~150-250MiB so this only fires when an avalanche starts.
|
|
||||||
- alert: MultusMemoryPressure
|
|
||||||
expr: |
|
|
||||||
container_memory_working_set_bytes{container="kube-multus"}
|
|
||||||
/ container_spec_memory_limit_bytes{container="kube-multus"} > 0.8
|
|
||||||
for: 5m
|
|
||||||
labels:
|
|
||||||
severity: critical
|
|
||||||
alert_channel: thermal_print
|
|
||||||
annotations:
|
|
||||||
summary: "kube-multus memory >80% of limit on {{ $labels.node }} for 5m"
|
|
||||||
description: "kube-multus working set is {{ $value | humanizePercentage }} of its memory limit on node {{ $labels.node }}. If this keeps climbing, multus will OOM and all new pod networking will halt cluster-wide (precedent: 2026-05-10 outage)."
|
|
||||||
|
|
||||||
# Q-MR-3 (2026-05-11): namespace pending-pod backlog — catches the
|
|
||||||
# operator-leak avalanche pattern BEFORE it cascades into a multus
|
|
||||||
# CNI OOM. Any FC operator (RemoteDesktop / Distribution / WorldBuilder)
|
|
||||||
# emitting pods without ownerReferences will accumulate them when
|
|
||||||
# the operator crashes. >25 pending pods in any namespace for 30m
|
|
||||||
# is the signal to investigate the reconciler.
|
|
||||||
- alert: NamespacePendingPodBacklog
|
|
||||||
expr: sum by (namespace) (kube_pod_status_phase{phase="Pending"}) > 25
|
|
||||||
for: 30m
|
|
||||||
labels:
|
|
||||||
severity: warning
|
|
||||||
annotations:
|
|
||||||
summary: "Namespace {{ $labels.namespace }} has {{ $value }} Pending pods for 30m"
|
|
||||||
description: "Pending pod count in {{ $labels.namespace }} exceeds 25 sustained for 30m. Likely operator-leak avalanche pattern — children emitted without ownerReferences. Risk of multus CNI OOM cascade."
|
|
||||||
|
|
||||||
# Longhorn storage health alerts. Required: longhorn scrape job
|
# Longhorn storage health alerts. Required: longhorn scrape job
|
||||||
# (added 2026-04-26 — see scrape_configs above). The K8s events
|
# (added 2026-04-26 — see scrape_configs above). The K8s events
|
||||||
# for "snapshot becomes not ready to use" are transient lifecycle
|
# for "snapshot becomes not ready to use" are transient lifecycle
|
||||||
|
|||||||
@@ -188,24 +188,13 @@ spec:
|
|||||||
- name: kube-multus
|
- name: kube-multus
|
||||||
image: ghcr.io/k8snetworkplumbingwg/multus-cni:snapshot-thick
|
image: ghcr.io/k8snetworkplumbingwg/multus-cni:snapshot-thick
|
||||||
command: [ "/usr/src/multus-cni/bin/multus-daemon" ]
|
command: [ "/usr/src/multus-cni/bin/multus-daemon" ]
|
||||||
# 2026-05-11: upstream default of 50Mi memory limit OOM-cascades when
|
|
||||||
# an operator-owned namespace accumulates >100 pending pods retrying
|
|
||||||
# CNI ADD. RemoteDesktop emitted 219 orphan rd-browser-only pods
|
|
||||||
# (missing OwnerReferences), kubelet's CNI ADD avalanche pushed multus
|
|
||||||
# over 50Mi, OOMKilled, restarted with even bigger backlog → loop.
|
|
||||||
# 21h cluster outage. See FlowerCore.Notes:
|
|
||||||
# feedback_multus_50mi_limit_oom_orphan_pod_avalanche.md
|
|
||||||
# 1Gi limit / 512Mi request comfortably handles a 200+ pod CNI
|
|
||||||
# catchup burst on 64GB nodes (nodes are <25% used in steady-state).
|
|
||||||
# Drop back toward 256Mi only after MultusMemoryPressure alert
|
|
||||||
# proves steady-state working set sits well below 200Mi.
|
|
||||||
resources:
|
resources:
|
||||||
requests:
|
requests:
|
||||||
cpu: "100m"
|
cpu: "100m"
|
||||||
memory: "512Mi"
|
memory: "50Mi"
|
||||||
limits:
|
limits:
|
||||||
cpu: "100m"
|
cpu: "100m"
|
||||||
memory: "1Gi"
|
memory: "50Mi"
|
||||||
securityContext:
|
securityContext:
|
||||||
privileged: true
|
privileged: true
|
||||||
terminationMessagePolicy: FallbackToLogsOnError
|
terminationMessagePolicy: FallbackToLogsOnError
|
||||||
|
|||||||
@@ -127,13 +127,10 @@ spec:
|
|||||||
initContainers:
|
initContainers:
|
||||||
- name: fix-data-perms
|
- name: fix-data-perms
|
||||||
image: busybox:latest
|
image: busybox:latest
|
||||||
# Must run as root to chown the hostPath /tmp/tts-audio that may be
|
# Also chown /shared-tts (hostPath /tmp/tts-audio) so the non-root
|
||||||
# root-owned after node reboot. Pod-level runAsNonRoot:true would
|
# app user (uid 1654) can write Piper .sln16 files that Asterisk
|
||||||
# otherwise inherit and chown would fail with EPERM (see Notes memory
|
# reads at /var/lib/asterisk/sounds/tts. World-readable (755) is
|
||||||
# feedback_hostpath_initcontainer_chown_perms).
|
# fine — Asterisk runs as a different uid in the other pod.
|
||||||
securityContext:
|
|
||||||
runAsUser: 0
|
|
||||||
runAsNonRoot: false
|
|
||||||
command: ["sh", "-c", "chown -R 1654:1654 /data && chown 1654:1654 /shared-tts && chmod 0755 /shared-tts"]
|
command: ["sh", "-c", "chown -R 1654:1654 /data && chown 1654:1654 /shared-tts && chmod 0755 /shared-tts"]
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- name: telephony-data
|
- name: telephony-data
|
||||||
|
|||||||
@@ -1,416 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Generate and optionally apply FlowerCore Authentik OIDC client assets.
|
|
||||||
|
|
||||||
Dry-run is the default. Live Authentik mutations require --apply plus an
|
|
||||||
AUTHENTIK_TOKEN bearer token and an operator-provided client secret JSON file.
|
|
||||||
The script never prints client_secret values.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import urllib.error
|
|
||||||
import urllib.parse
|
|
||||||
import urllib.request
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
|
|
||||||
CLAIMS = (
|
|
||||||
"fc:roles",
|
|
||||||
"fc:tenant",
|
|
||||||
"fc:svc",
|
|
||||||
"fc:scope",
|
|
||||||
"fc:mfa",
|
|
||||||
"flowercore_actor_id",
|
|
||||||
)
|
|
||||||
|
|
||||||
BUILTIN_SCOPE_MAPPINGS = (
|
|
||||||
"goauthentik.io/providers/oauth2/scope-openid",
|
|
||||||
"goauthentik.io/providers/oauth2/scope-profile",
|
|
||||||
"goauthentik.io/providers/oauth2/scope-email",
|
|
||||||
"goauthentik.io/providers/oauth2/scope-offline_access",
|
|
||||||
)
|
|
||||||
|
|
||||||
FLOW_CONTRACT = {
|
|
||||||
"response_type": "code",
|
|
||||||
"grant_types": ["authorization_code", "refresh_token"],
|
|
||||||
"offline_access_required": True,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class ServiceSpec:
|
|
||||||
slug: str
|
|
||||||
namespace: str
|
|
||||||
display_name: str
|
|
||||||
host: str
|
|
||||||
|
|
||||||
@property
|
|
||||||
def client_id(self) -> str:
|
|
||||||
return self.slug
|
|
||||||
|
|
||||||
@property
|
|
||||||
def provider_name(self) -> str:
|
|
||||||
return f"FlowerCore {self.display_name} OIDC"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def application_name(self) -> str:
|
|
||||||
return f"FlowerCore {self.display_name}"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def issuer_url(self) -> str:
|
|
||||||
return f"https://id.iamworkin.lan/application/o/{self.slug}/"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def onepassword_item_path(self) -> str:
|
|
||||||
return f"IAmWorkin/items/{self.slug}-oidc-client"
|
|
||||||
|
|
||||||
|
|
||||||
SERVICE_SPECS = (
|
|
||||||
ServiceSpec("library", "fc-library", "Library", "library.iamworkin.lan"),
|
|
||||||
ServiceSpec("retail", "fc-retail", "Retail", "retail.iamworkin.lan"),
|
|
||||||
ServiceSpec("telephony", "telephony", "Telephony", "telephony.iamworkin.lan"),
|
|
||||||
ServiceSpec("knowledge", "knowledge", "Knowledge", "knowledge.iamworkin.lan"),
|
|
||||||
ServiceSpec("llmbridge", "fc-llm-bridge", "LlmBridge", "fc-llm-bridge.iamworkin.lan"),
|
|
||||||
ServiceSpec("mysql", "fc-mysql", "MySQL", "mysql.iamworkin.lan"),
|
|
||||||
ServiceSpec("php", "fc-php", "PHP", "php.iamworkin.lan"),
|
|
||||||
ServiceSpec("signage", "fc-signage", "Signage", "signage.iamworkin.lan"),
|
|
||||||
ServiceSpec("media", "fc-media", "Media", "media.iamworkin.lan"),
|
|
||||||
ServiceSpec("dms", "fc-dms", "DMS", "dms.iamworkin.lan"),
|
|
||||||
ServiceSpec("pimanager", "fc-pimanager", "PiManager", "pimanager.iamworkin.lan"),
|
|
||||||
ServiceSpec("distribution", "fc-distribution", "Distribution", "distribution.iamworkin.lan"),
|
|
||||||
ServiceSpec("dns", "fc-dns", "DNS", "dns.iamworkin.lan"),
|
|
||||||
ServiceSpec("print", "fc-print", "Print", "print.iamworkin.lan"),
|
|
||||||
ServiceSpec("aistation", "fc-aistation", "AiStation", "aistation.iamworkin.lan"),
|
|
||||||
ServiceSpec("irc", "irc", "IRC", "irc.iamworkin.lan"),
|
|
||||||
ServiceSpec("ttsreader", "fc-ttsreader", "TtsReader", "ttsreader.iamworkin.lan"),
|
|
||||||
ServiceSpec("chat", "fc-chat", "Chat", "chat.iamworkin.lan"),
|
|
||||||
ServiceSpec("intranet", "intranet", "Intranet", "intranet.iamworkin.lan"),
|
|
||||||
ServiceSpec("remotedesktop", "fc-desktop", "RemoteDesktop", "remotedesktop.iamworkin.lan"),
|
|
||||||
ServiceSpec("provisioning", "fc-provisioning", "Provisioning", "provisioning.iamworkin.lan"),
|
|
||||||
ServiceSpec("scoreboards", "fc-scoreboard", "Scoreboards", "scoreboards.iamworkin.lan"),
|
|
||||||
ServiceSpec("mndot", "fc-mndot", "MnDOT", "mndot.iamworkin.lan"),
|
|
||||||
ServiceSpec("kiosk", "fc-system", "Kiosk", "kiosk.iamworkin.lan"),
|
|
||||||
ServiceSpec("mike-bundle", "fc-mike-bundle", "Mike Bundle", "mike-bundle.iamworkin.lan"),
|
|
||||||
ServiceSpec("messageboard", "fc-messageboard", "MessageBoard", "messageboard.iamworkin.lan"),
|
|
||||||
ServiceSpec("menuboard", "fc-menuboard", "MenuBoard", "menuboard.iamworkin.lan"),
|
|
||||||
ServiceSpec("presentations", "fc-presentations", "Presentations", "presentations.iamworkin.lan"),
|
|
||||||
ServiceSpec("segmentdisplay", "fc-segmentdisplay", "SegmentDisplay", "segmentdisplay.iamworkin.lan"),
|
|
||||||
ServiceSpec("signalcontrol", "fc-signalcontrol", "SignalControl", "signalcontrol.iamworkin.lan"),
|
|
||||||
ServiceSpec("worldbuilder", "fc-worldbuilder", "WorldBuilder", "worldbuilder.iamworkin.lan"),
|
|
||||||
ServiceSpec("audit", "fc-audit", "Audit", "audit.iamworkin.lan"),
|
|
||||||
ServiceSpec("licensing", "fc-licensing", "Licensing", "licensing.iamworkin.lan"),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def scope_mapping_payloads(service: ServiceSpec) -> list[dict[str, str]]:
|
|
||||||
managed_prefix = f"flowercore.io/authentik/oidc/{service.slug}"
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
"managed": f"{managed_prefix}/fc-roles",
|
|
||||||
"name": f"FlowerCore {service.slug} fc:roles",
|
|
||||||
"scope_name": "flowercore",
|
|
||||||
"description": "FlowerCore role claim from Authentik group memberships.",
|
|
||||||
"expression": (
|
|
||||||
"groups = [group.name for group in request.user.ak_groups.all()]\n"
|
|
||||||
"return {'fc:roles': ','.join(groups)}"
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"managed": f"{managed_prefix}/fc-tenant",
|
|
||||||
"name": f"FlowerCore {service.slug} fc:tenant",
|
|
||||||
"scope_name": "flowercore",
|
|
||||||
"description": "FlowerCore tenant claim from group attribute fc_tenant_id.",
|
|
||||||
"expression": (
|
|
||||||
"for group in request.user.ak_groups.all():\n"
|
|
||||||
" tenant_id = group.attributes.get('fc_tenant_id')\n"
|
|
||||||
" if tenant_id:\n"
|
|
||||||
" return {'fc:tenant': tenant_id}\n"
|
|
||||||
"return {'fc:tenant': 'default'}"
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"managed": f"{managed_prefix}/fc-svc",
|
|
||||||
"name": f"FlowerCore {service.slug} fc:svc",
|
|
||||||
"scope_name": "flowercore",
|
|
||||||
"description": "FlowerCore service slug claim.",
|
|
||||||
"expression": f"return {{'fc:svc': '{service.slug}'}}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"managed": f"{managed_prefix}/fc-scope",
|
|
||||||
"name": f"FlowerCore {service.slug} fc:scope",
|
|
||||||
"scope_name": "flowercore",
|
|
||||||
"description": "FlowerCore service permission scope claim.",
|
|
||||||
"expression": f"return {{'fc:scope': 'flowercore:{service.slug}'}}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"managed": f"{managed_prefix}/fc-mfa",
|
|
||||||
"name": f"FlowerCore {service.slug} fc:mfa",
|
|
||||||
"scope_name": "flowercore",
|
|
||||||
"description": "FlowerCore MFA satisfied session claim.",
|
|
||||||
"expression": (
|
|
||||||
"mfa_stage = request.session.get('authentik/stages/authenticator_validate')\n"
|
|
||||||
"return {'fc:mfa': bool(mfa_stage)}"
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"managed": f"{managed_prefix}/flowercore-actor-id",
|
|
||||||
"name": f"FlowerCore {service.slug} flowercore_actor_id",
|
|
||||||
"scope_name": "flowercore",
|
|
||||||
"description": "FlowerCore audit actor alias for the Authentik user id.",
|
|
||||||
"expression": "return {'flowercore_actor_id': str(request.user.uid)}",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def provider_payload(
|
|
||||||
service: ServiceSpec,
|
|
||||||
args: argparse.Namespace,
|
|
||||||
mapping_ids: list[str],
|
|
||||||
client_secret: str,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
payload: dict[str, Any] = {
|
|
||||||
"name": service.provider_name,
|
|
||||||
"authorization_flow": args.authorization_flow,
|
|
||||||
"invalidation_flow": args.invalidation_flow,
|
|
||||||
"property_mappings": mapping_ids,
|
|
||||||
"client_type": "confidential",
|
|
||||||
"client_id": service.client_id,
|
|
||||||
"client_secret": client_secret,
|
|
||||||
"access_code_validity": "minutes=1",
|
|
||||||
"access_token_validity": "hours=1",
|
|
||||||
"refresh_token_validity": "days=30",
|
|
||||||
"include_claims_in_id_token": True,
|
|
||||||
"redirect_uris": [
|
|
||||||
{"matching_mode": "strict", "url": f"https://{service.host}/signin-oidc"},
|
|
||||||
{"matching_mode": "strict", "url": f"https://{service.host}/signout-callback-oidc"},
|
|
||||||
],
|
|
||||||
"sub_mode": "hashed_user_id",
|
|
||||||
"issuer_mode": "per_provider",
|
|
||||||
}
|
|
||||||
|
|
||||||
if args.authentication_flow:
|
|
||||||
payload["authentication_flow"] = args.authentication_flow
|
|
||||||
if args.signing_key:
|
|
||||||
payload["signing_key"] = args.signing_key
|
|
||||||
|
|
||||||
return payload
|
|
||||||
|
|
||||||
|
|
||||||
def application_payload(service: ServiceSpec, provider_pk: int | str | None) -> dict[str, Any]:
|
|
||||||
return {
|
|
||||||
"name": service.application_name,
|
|
||||||
"slug": service.slug,
|
|
||||||
"provider": provider_pk,
|
|
||||||
"open_in_new_tab": True,
|
|
||||||
"meta_launch_url": f"https://{service.host}/",
|
|
||||||
"meta_description": f"FlowerCore {service.display_name} OIDC client",
|
|
||||||
"meta_publisher": "FlowerCore",
|
|
||||||
"policy_engine_mode": "all",
|
|
||||||
"group": "FlowerCore",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def load_client_secrets(path: str | None) -> dict[str, str]:
|
|
||||||
if not path:
|
|
||||||
return {}
|
|
||||||
with open(path, "r", encoding="utf-8") as handle:
|
|
||||||
data = json.load(handle)
|
|
||||||
if not isinstance(data, dict):
|
|
||||||
raise ValueError("client secret file must be a JSON object keyed by service slug")
|
|
||||||
return {str(key): str(value) for key, value in data.items()}
|
|
||||||
|
|
||||||
|
|
||||||
def redact(value: Any) -> Any:
|
|
||||||
if isinstance(value, dict):
|
|
||||||
return {
|
|
||||||
key: "<redacted>" if key == "client_secret" else redact(child)
|
|
||||||
for key, child in value.items()
|
|
||||||
}
|
|
||||||
if isinstance(value, list):
|
|
||||||
return [redact(child) for child in value]
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
class AuthentikClient:
|
|
||||||
def __init__(self, base_url: str, token: str) -> None:
|
|
||||||
self.base_url = base_url.rstrip("/")
|
|
||||||
self.token = token
|
|
||||||
|
|
||||||
def request(self, method: str, path: str, payload: dict[str, Any] | None = None) -> Any:
|
|
||||||
url = f"{self.base_url}/api/v3/{path.lstrip('/')}"
|
|
||||||
body = None
|
|
||||||
headers = {
|
|
||||||
"Accept": "application/json",
|
|
||||||
"Authorization": f"Bearer {self.token}",
|
|
||||||
}
|
|
||||||
if payload is not None:
|
|
||||||
headers["Content-Type"] = "application/json"
|
|
||||||
body = json.dumps(payload).encode("utf-8")
|
|
||||||
|
|
||||||
request = urllib.request.Request(url, data=body, headers=headers, method=method)
|
|
||||||
try:
|
|
||||||
with urllib.request.urlopen(request, timeout=30) as response:
|
|
||||||
text = response.read().decode("utf-8")
|
|
||||||
except urllib.error.HTTPError as error:
|
|
||||||
error_text = error.read().decode("utf-8", errors="replace")
|
|
||||||
raise RuntimeError(f"{method} {path} failed with HTTP {error.code}: {error_text}") from error
|
|
||||||
|
|
||||||
if not text:
|
|
||||||
return None
|
|
||||||
return json.loads(text)
|
|
||||||
|
|
||||||
def first_result(self, path: str, **query: str) -> dict[str, Any] | None:
|
|
||||||
query_string = urllib.parse.urlencode(query)
|
|
||||||
response = self.request("GET", f"{path}?{query_string}")
|
|
||||||
results = response.get("results", []) if isinstance(response, dict) else []
|
|
||||||
return results[0] if results else None
|
|
||||||
|
|
||||||
|
|
||||||
def select_services(slugs: list[str]) -> list[ServiceSpec]:
|
|
||||||
if not slugs:
|
|
||||||
return list(SERVICE_SPECS)
|
|
||||||
|
|
||||||
by_slug = {service.slug: service for service in SERVICE_SPECS}
|
|
||||||
unknown = sorted(set(slugs) - set(by_slug))
|
|
||||||
if unknown:
|
|
||||||
raise ValueError(f"unknown service slug(s): {', '.join(unknown)}")
|
|
||||||
return [by_slug[slug] for slug in slugs]
|
|
||||||
|
|
||||||
|
|
||||||
def validate_specs(services: list[ServiceSpec]) -> None:
|
|
||||||
slugs = [service.slug for service in services]
|
|
||||||
if len(slugs) != len(set(slugs)):
|
|
||||||
raise ValueError("duplicate service slug in OIDC roster")
|
|
||||||
for service in services:
|
|
||||||
if not service.namespace:
|
|
||||||
raise ValueError(f"{service.slug} is missing a target namespace")
|
|
||||||
expressions = "\n".join(mapping["expression"] for mapping in scope_mapping_payloads(service))
|
|
||||||
missing_claims = [claim for claim in CLAIMS if claim not in expressions]
|
|
||||||
if missing_claims:
|
|
||||||
raise ValueError(f"{service.slug} mapping payloads miss claims: {', '.join(missing_claims)}")
|
|
||||||
|
|
||||||
|
|
||||||
def dry_run(services: list[ServiceSpec], args: argparse.Namespace) -> int:
|
|
||||||
placeholder_ids = [
|
|
||||||
*[f"<builtin-{managed.rsplit('/', 1)[-1]}-pk>" for managed in BUILTIN_SCOPE_MAPPINGS],
|
|
||||||
*[f"<{claim}-mapping-pk>" for claim in CLAIMS],
|
|
||||||
]
|
|
||||||
documents = []
|
|
||||||
for service in services:
|
|
||||||
provider = provider_payload(service, args, placeholder_ids, "<from-1password-client_secret>")
|
|
||||||
documents.append(
|
|
||||||
{
|
|
||||||
"service": service.slug,
|
|
||||||
"namespace": service.namespace,
|
|
||||||
"onepassword_item": service.onepassword_item_path,
|
|
||||||
"issuer_url": service.issuer_url,
|
|
||||||
"flow_contract": FLOW_CONTRACT,
|
|
||||||
"builtin_scope_mappings": list(BUILTIN_SCOPE_MAPPINGS),
|
|
||||||
"scope_mappings": scope_mapping_payloads(service),
|
|
||||||
"provider": redact(provider),
|
|
||||||
"application": application_payload(service, "<provider-pk>"),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if args.print_json:
|
|
||||||
print(json.dumps(documents, indent=2, sort_keys=True))
|
|
||||||
else:
|
|
||||||
print(
|
|
||||||
"Dry-run only: generated "
|
|
||||||
f"{len(services)} providers, {len(services)} applications, "
|
|
||||||
f"and {len(services) * len(CLAIMS)} scope mappings."
|
|
||||||
)
|
|
||||||
print("Use --print-json to inspect redacted payloads; use --apply for live Authentik mutation.")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
def apply(services: list[ServiceSpec], args: argparse.Namespace) -> int:
|
|
||||||
token = os.environ.get("AUTHENTIK_TOKEN")
|
|
||||||
if not token:
|
|
||||||
raise ValueError("AUTHENTIK_TOKEN is required with --apply")
|
|
||||||
if not args.client_secrets_json:
|
|
||||||
raise ValueError("--client-secrets-json is required with --apply")
|
|
||||||
|
|
||||||
secrets = load_client_secrets(args.client_secrets_json)
|
|
||||||
missing = [service.slug for service in services if not secrets.get(service.slug)]
|
|
||||||
if missing:
|
|
||||||
raise ValueError(f"client secret JSON is missing slug(s): {', '.join(missing)}")
|
|
||||||
|
|
||||||
client = AuthentikClient(args.base_url, token)
|
|
||||||
for service in services:
|
|
||||||
mapping_ids: list[str] = []
|
|
||||||
for managed in BUILTIN_SCOPE_MAPPINGS:
|
|
||||||
existing = client.first_result("/propertymappings/provider/scope/", managed=managed)
|
|
||||||
if not existing:
|
|
||||||
raise ValueError(f"built-in Authentik scope mapping not found: {managed}")
|
|
||||||
mapping_ids.append(existing["pk"])
|
|
||||||
|
|
||||||
for mapping in scope_mapping_payloads(service):
|
|
||||||
existing = client.first_result("/propertymappings/provider/scope/", name=mapping["name"])
|
|
||||||
if existing and not args.update_existing:
|
|
||||||
mapping_ids.append(existing["pk"])
|
|
||||||
continue
|
|
||||||
if existing and args.update_existing:
|
|
||||||
updated = client.request("PATCH", f"/propertymappings/provider/scope/{existing['pk']}/", mapping)
|
|
||||||
mapping_ids.append(updated["pk"])
|
|
||||||
continue
|
|
||||||
created = client.request("POST", "/propertymappings/provider/scope/", mapping)
|
|
||||||
mapping_ids.append(created["pk"])
|
|
||||||
|
|
||||||
existing_provider = client.first_result("/providers/oauth2/", client_id=service.client_id)
|
|
||||||
provider_body = provider_payload(service, args, mapping_ids, secrets[service.slug])
|
|
||||||
if existing_provider and not args.update_existing:
|
|
||||||
provider_pk = existing_provider["pk"]
|
|
||||||
elif existing_provider:
|
|
||||||
updated_provider = client.request("PATCH", f"/providers/oauth2/{existing_provider['pk']}/", provider_body)
|
|
||||||
provider_pk = updated_provider["pk"]
|
|
||||||
else:
|
|
||||||
provider_pk = client.request("POST", "/providers/oauth2/", provider_body)["pk"]
|
|
||||||
|
|
||||||
app_body = application_payload(service, provider_pk)
|
|
||||||
existing_app = client.first_result("/core/applications/", slug=service.slug)
|
|
||||||
if existing_app and args.update_existing:
|
|
||||||
client.request("PATCH", f"/core/applications/{existing_app['slug']}/", app_body)
|
|
||||||
elif not existing_app:
|
|
||||||
client.request("POST", "/core/applications/", app_body)
|
|
||||||
|
|
||||||
print(f"applied {service.slug}: provider/app present, secret redacted")
|
|
||||||
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
def parse_args() -> argparse.Namespace:
|
|
||||||
parser = argparse.ArgumentParser(description=__doc__)
|
|
||||||
parser.add_argument("--apply", action="store_true", help="perform live Authentik REST mutations")
|
|
||||||
parser.add_argument("--update-existing", action="store_true", help="patch existing mappings/providers/applications")
|
|
||||||
parser.add_argument("--print-json", action="store_true", help="print redacted dry-run payloads")
|
|
||||||
parser.add_argument("--service", action="append", default=[], help="limit to one service slug; repeatable")
|
|
||||||
parser.add_argument("--base-url", default="http://localhost:9000", help="Authentik base URL")
|
|
||||||
parser.add_argument("--client-secrets-json", help="operator-provided JSON object of slug to client_secret")
|
|
||||||
parser.add_argument("--authorization-flow", default="<authorization-flow-uuid>")
|
|
||||||
parser.add_argument("--invalidation-flow", default="<invalidation-flow-uuid>")
|
|
||||||
parser.add_argument("--authentication-flow")
|
|
||||||
parser.add_argument("--signing-key", help="shared Authentik signing key UUID")
|
|
||||||
return parser.parse_args()
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
args = parse_args()
|
|
||||||
try:
|
|
||||||
services = select_services(args.service)
|
|
||||||
validate_specs(services)
|
|
||||||
if args.apply:
|
|
||||||
return apply(services, args)
|
|
||||||
return dry_run(services, args)
|
|
||||||
except Exception as error:
|
|
||||||
print(f"error: {error}", file=sys.stderr)
|
|
||||||
return 2
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
raise SystemExit(main())
|
|
||||||
@@ -1,192 +0,0 @@
|
|||||||
using FluentAssertions;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using Xunit;
|
|
||||||
using YamlDotNet.RepresentationModel;
|
|
||||||
|
|
||||||
namespace BluejayInfraLint.Tests;
|
|
||||||
|
|
||||||
[Trait("Category", "AuthentikOidc")]
|
|
||||||
public sealed class AuthentikOidcClientRegistrationTests
|
|
||||||
{
|
|
||||||
private static readonly IReadOnlyList<ServiceClientExpectation> ExpectedClients =
|
|
||||||
[
|
|
||||||
new("library", "fc-library"),
|
|
||||||
new("retail", "fc-retail"),
|
|
||||||
new("telephony", "telephony"),
|
|
||||||
new("knowledge", "knowledge"),
|
|
||||||
new("llmbridge", "fc-llm-bridge"),
|
|
||||||
new("mysql", "fc-mysql"),
|
|
||||||
new("php", "fc-php"),
|
|
||||||
new("signage", "fc-signage"),
|
|
||||||
new("media", "fc-media"),
|
|
||||||
new("dms", "fc-dms"),
|
|
||||||
new("pimanager", "fc-pimanager"),
|
|
||||||
new("distribution", "fc-distribution"),
|
|
||||||
new("dns", "fc-dns"),
|
|
||||||
new("print", "fc-print"),
|
|
||||||
new("aistation", "fc-aistation"),
|
|
||||||
new("irc", "irc"),
|
|
||||||
new("ttsreader", "fc-ttsreader"),
|
|
||||||
new("chat", "fc-chat"),
|
|
||||||
new("intranet", "intranet"),
|
|
||||||
new("remotedesktop", "fc-desktop"),
|
|
||||||
new("provisioning", "fc-provisioning"),
|
|
||||||
new("scoreboards", "fc-scoreboard"),
|
|
||||||
new("mndot", "fc-mndot"),
|
|
||||||
new("kiosk", "fc-system"),
|
|
||||||
new("mike-bundle", "fc-mike-bundle"),
|
|
||||||
new("messageboard", "fc-messageboard"),
|
|
||||||
new("menuboard", "fc-menuboard"),
|
|
||||||
new("presentations", "fc-presentations"),
|
|
||||||
new("segmentdisplay", "fc-segmentdisplay"),
|
|
||||||
new("signalcontrol", "fc-signalcontrol"),
|
|
||||||
new("worldbuilder", "fc-worldbuilder"),
|
|
||||||
new("audit", "fc-audit"),
|
|
||||||
new("licensing", "fc-licensing"),
|
|
||||||
];
|
|
||||||
|
|
||||||
public static TheoryData<string, string> ExpectedClientRows()
|
|
||||||
{
|
|
||||||
var data = new TheoryData<string, string>();
|
|
||||||
foreach (var client in ExpectedClients)
|
|
||||||
{
|
|
||||||
data.Add(client.Slug, client.Namespace);
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[MemberData(nameof(ExpectedClientRows))]
|
|
||||||
public void OidcClientManifest_MatchesOnePasswordOperatorContract(string slug, string targetNamespace)
|
|
||||||
{
|
|
||||||
var manifest = LoadClientManifest(slug);
|
|
||||||
manifest.Scalar("apiVersion").Should().Be("onepassword.com/v1");
|
|
||||||
manifest.Scalar("kind").Should().Be("OnePasswordItem");
|
|
||||||
manifest.Scalar("metadata", "name").Should().Be($"{slug}-oidc-client");
|
|
||||||
manifest.Scalar("metadata", "namespace").Should().Be(targetNamespace);
|
|
||||||
manifest.Scalar("metadata", "labels", "app.kubernetes.io/component")
|
|
||||||
.Should().Be("authentik-oidc-client");
|
|
||||||
manifest.Scalar("metadata", "labels", "flowercore.io/authentik-client-slug")
|
|
||||||
.Should().Be(slug);
|
|
||||||
manifest.Scalar("metadata", "annotations", "flowercore.io/expected-fields")
|
|
||||||
.Should().Be("client_id,client_secret,issuer_url");
|
|
||||||
manifest.Scalar("spec", "itemPath")
|
|
||||||
.Should().Be($"vaults/IAmWorkin/items/{slug}-oidc-client");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void AuthentikKustomization_ReferencesEveryClientManifest()
|
|
||||||
{
|
|
||||||
var kustomizationPath = Path.Combine(BluejayRoot(), "apps", "authentik", "kustomization.yaml");
|
|
||||||
var text = File.ReadAllText(kustomizationPath);
|
|
||||||
|
|
||||||
foreach (var client in ExpectedClients)
|
|
||||||
{
|
|
||||||
text.Should().Contain($"clients/{client.Slug}-oidc-client.yaml");
|
|
||||||
}
|
|
||||||
|
|
||||||
Regex.Matches(text, @"clients/[-a-z0-9]+-oidc-client\.yaml")
|
|
||||||
.Select(match => match.Value)
|
|
||||||
.Distinct(StringComparer.Ordinal)
|
|
||||||
.Should()
|
|
||||||
.HaveCount(ExpectedClients.Count);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void BulkClientScript_HasDryRunDefaultAndRequiredClaimPayloads()
|
|
||||||
{
|
|
||||||
var scriptPath = Path.Combine(BluejayRoot(), "scripts", "authentik-bulk-client-create.py");
|
|
||||||
var script = File.ReadAllText(scriptPath);
|
|
||||||
|
|
||||||
script.Should().Contain("--apply");
|
|
||||||
script.Should().Contain("Dry-run only");
|
|
||||||
script.Should().Contain("AUTHENTIK_TOKEN");
|
|
||||||
script.Should().Contain("client_secrets_json");
|
|
||||||
script.Should().Contain("scope-offline_access");
|
|
||||||
script.Should().Contain("authorization_code");
|
|
||||||
script.Should().Contain("refresh_token");
|
|
||||||
|
|
||||||
foreach (var claim in new[] { "fc:roles", "fc:tenant", "fc:svc", "fc:scope", "fc:mfa", "flowercore_actor_id" })
|
|
||||||
{
|
|
||||||
script.Should().Contain(claim);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void BulkClientScript_DryRunGeneratesAllServicesWithoutSecrets()
|
|
||||||
{
|
|
||||||
var scriptPath = Path.Combine(BluejayRoot(), "scripts", "authentik-bulk-client-create.py");
|
|
||||||
var startInfo = new ProcessStartInfo
|
|
||||||
{
|
|
||||||
FileName = "python",
|
|
||||||
WorkingDirectory = BluejayRoot(),
|
|
||||||
RedirectStandardOutput = true,
|
|
||||||
RedirectStandardError = true,
|
|
||||||
};
|
|
||||||
startInfo.ArgumentList.Add(scriptPath);
|
|
||||||
startInfo.ArgumentList.Add("--print-json");
|
|
||||||
|
|
||||||
using var process = Process.Start(startInfo)
|
|
||||||
?? throw new InvalidOperationException("Could not start python dry-run.");
|
|
||||||
var stdout = process.StandardOutput.ReadToEnd();
|
|
||||||
var stderr = process.StandardError.ReadToEnd();
|
|
||||||
process.WaitForExit(15000).Should().BeTrue(stderr);
|
|
||||||
process.ExitCode.Should().Be(0, stderr);
|
|
||||||
|
|
||||||
foreach (var client in ExpectedClients)
|
|
||||||
{
|
|
||||||
stdout.Should().Contain($"\"service\": \"{client.Slug}\"");
|
|
||||||
stdout.Should().Contain($"\"namespace\": \"{client.Namespace}\"");
|
|
||||||
}
|
|
||||||
|
|
||||||
stdout.Should().Contain("\"client_secret\": \"<redacted>\"");
|
|
||||||
stdout.Should().NotMatchRegex("\"client_secret\"\\s*:\\s*\"(?!<redacted>)[^\"]+\"");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void ClientManifests_DoNotContainInlineSecretMaterial()
|
|
||||||
{
|
|
||||||
foreach (var client in ExpectedClients)
|
|
||||||
{
|
|
||||||
var path = ClientManifestPath(client.Slug);
|
|
||||||
var text = File.ReadAllText(path);
|
|
||||||
|
|
||||||
text.Should().NotContain("client_secret:");
|
|
||||||
text.Should().NotContain("password:");
|
|
||||||
text.Should().NotContain("secret:");
|
|
||||||
text.Should().Contain($"IAmWorkin/items/{client.Slug}-oidc-client");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static YamlMappingNode LoadClientManifest(string slug)
|
|
||||||
{
|
|
||||||
using var reader = File.OpenText(ClientManifestPath(slug));
|
|
||||||
var stream = new YamlStream();
|
|
||||||
stream.Load(reader);
|
|
||||||
return stream.Documents[0].RootNode.Should().BeOfType<YamlMappingNode>().Subject;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string ClientManifestPath(string slug) =>
|
|
||||||
Path.Combine(BluejayRoot(), "apps", "authentik", "clients", $"{slug}-oidc-client.yaml");
|
|
||||||
|
|
||||||
private static string BluejayRoot()
|
|
||||||
{
|
|
||||||
var current = new DirectoryInfo(AppContext.BaseDirectory);
|
|
||||||
while (current is not null)
|
|
||||||
{
|
|
||||||
if (Directory.Exists(Path.Combine(current.FullName, "apps"))
|
|
||||||
&& File.Exists(Path.Combine(current.FullName, "README.md")))
|
|
||||||
{
|
|
||||||
return current.FullName;
|
|
||||||
}
|
|
||||||
|
|
||||||
current = current.Parent;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new DirectoryNotFoundException("Could not find bluejay-infra root.");
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed record ServiceClientExpectation(string Slug, string Namespace);
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user