Compare commits
3 Commits
feat/redis
...
feat/authe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
41c598394e | ||
|
|
5029e209cd | ||
|
|
f298339152 |
18
apps/authentik/README.md
Normal file
18
apps/authentik/README.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# 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.
|
||||||
14
apps/authentik/clients/aistation-oidc-client.yaml
Normal file
14
apps/authentik/clients/aistation-oidc-client.yaml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
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"
|
||||||
14
apps/authentik/clients/audit-oidc-client.yaml
Normal file
14
apps/authentik/clients/audit-oidc-client.yaml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
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"
|
||||||
14
apps/authentik/clients/chat-oidc-client.yaml
Normal file
14
apps/authentik/clients/chat-oidc-client.yaml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
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"
|
||||||
14
apps/authentik/clients/distribution-oidc-client.yaml
Normal file
14
apps/authentik/clients/distribution-oidc-client.yaml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
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"
|
||||||
14
apps/authentik/clients/dms-oidc-client.yaml
Normal file
14
apps/authentik/clients/dms-oidc-client.yaml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
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"
|
||||||
14
apps/authentik/clients/dns-oidc-client.yaml
Normal file
14
apps/authentik/clients/dns-oidc-client.yaml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
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"
|
||||||
14
apps/authentik/clients/intranet-oidc-client.yaml
Normal file
14
apps/authentik/clients/intranet-oidc-client.yaml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
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"
|
||||||
14
apps/authentik/clients/irc-oidc-client.yaml
Normal file
14
apps/authentik/clients/irc-oidc-client.yaml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
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"
|
||||||
14
apps/authentik/clients/kiosk-oidc-client.yaml
Normal file
14
apps/authentik/clients/kiosk-oidc-client.yaml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
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"
|
||||||
14
apps/authentik/clients/knowledge-oidc-client.yaml
Normal file
14
apps/authentik/clients/knowledge-oidc-client.yaml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
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"
|
||||||
14
apps/authentik/clients/library-oidc-client.yaml
Normal file
14
apps/authentik/clients/library-oidc-client.yaml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
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"
|
||||||
14
apps/authentik/clients/licensing-oidc-client.yaml
Normal file
14
apps/authentik/clients/licensing-oidc-client.yaml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
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"
|
||||||
14
apps/authentik/clients/llmbridge-oidc-client.yaml
Normal file
14
apps/authentik/clients/llmbridge-oidc-client.yaml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
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"
|
||||||
14
apps/authentik/clients/media-oidc-client.yaml
Normal file
14
apps/authentik/clients/media-oidc-client.yaml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
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"
|
||||||
14
apps/authentik/clients/menuboard-oidc-client.yaml
Normal file
14
apps/authentik/clients/menuboard-oidc-client.yaml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
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"
|
||||||
14
apps/authentik/clients/messageboard-oidc-client.yaml
Normal file
14
apps/authentik/clients/messageboard-oidc-client.yaml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
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"
|
||||||
14
apps/authentik/clients/mike-bundle-oidc-client.yaml
Normal file
14
apps/authentik/clients/mike-bundle-oidc-client.yaml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
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"
|
||||||
14
apps/authentik/clients/mndot-oidc-client.yaml
Normal file
14
apps/authentik/clients/mndot-oidc-client.yaml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
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"
|
||||||
14
apps/authentik/clients/mysql-oidc-client.yaml
Normal file
14
apps/authentik/clients/mysql-oidc-client.yaml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
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"
|
||||||
14
apps/authentik/clients/php-oidc-client.yaml
Normal file
14
apps/authentik/clients/php-oidc-client.yaml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
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"
|
||||||
14
apps/authentik/clients/pimanager-oidc-client.yaml
Normal file
14
apps/authentik/clients/pimanager-oidc-client.yaml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
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"
|
||||||
14
apps/authentik/clients/presentations-oidc-client.yaml
Normal file
14
apps/authentik/clients/presentations-oidc-client.yaml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
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"
|
||||||
14
apps/authentik/clients/print-oidc-client.yaml
Normal file
14
apps/authentik/clients/print-oidc-client.yaml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
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"
|
||||||
14
apps/authentik/clients/provisioning-oidc-client.yaml
Normal file
14
apps/authentik/clients/provisioning-oidc-client.yaml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
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"
|
||||||
14
apps/authentik/clients/remotedesktop-oidc-client.yaml
Normal file
14
apps/authentik/clients/remotedesktop-oidc-client.yaml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
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"
|
||||||
14
apps/authentik/clients/retail-oidc-client.yaml
Normal file
14
apps/authentik/clients/retail-oidc-client.yaml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
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"
|
||||||
14
apps/authentik/clients/scoreboards-oidc-client.yaml
Normal file
14
apps/authentik/clients/scoreboards-oidc-client.yaml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
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"
|
||||||
14
apps/authentik/clients/segmentdisplay-oidc-client.yaml
Normal file
14
apps/authentik/clients/segmentdisplay-oidc-client.yaml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
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"
|
||||||
14
apps/authentik/clients/signage-oidc-client.yaml
Normal file
14
apps/authentik/clients/signage-oidc-client.yaml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
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"
|
||||||
14
apps/authentik/clients/signalcontrol-oidc-client.yaml
Normal file
14
apps/authentik/clients/signalcontrol-oidc-client.yaml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
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"
|
||||||
14
apps/authentik/clients/telephony-oidc-client.yaml
Normal file
14
apps/authentik/clients/telephony-oidc-client.yaml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
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"
|
||||||
14
apps/authentik/clients/ttsreader-oidc-client.yaml
Normal file
14
apps/authentik/clients/ttsreader-oidc-client.yaml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
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"
|
||||||
14
apps/authentik/clients/worldbuilder-oidc-client.yaml
Normal file
14
apps/authentik/clients/worldbuilder-oidc-client.yaml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
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"
|
||||||
38
apps/authentik/kustomization.yaml
Normal file
38
apps/authentik/kustomization.yaml
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# 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
|
||||||
171
apps/fc-redis/fc-redis.yaml
Normal file
171
apps/fc-redis/fc-redis.yaml
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
# 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
|
||||||
@@ -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 — 2026-04-28)
|
# 1Password-backed credentials for Mac mini VNC access (Phase 1 <EFBFBD> 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) — the 1P item 'IP' field is kept as backup reference.
|
# Actual IP is 10.0.56.115 (INFRA VLAN) <EFBFBD> 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,6 +481,7 @@ 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,51 +1,9 @@
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
# ci1 — Windows Server 2025 KubeVirt VM (GitHub Actions Self-Hosted Runner)
|
# ci1 - Windows Server 2025 KubeVirt VM (GitHub Actions Self-Hosted Runner)
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Purpose: dedicated CI runner for FlowerCore.Updater Sandbox E2E nightly +
|
# Boots from the sysprepped containerDisk template built by the Windows VM
|
||||||
# future fleet WPF AAT lanes. Replaces the never-registered
|
# sysprep pipeline. See docs/infrastructure/windows-vm-sysprep-pipeline.md.
|
||||||
# `bluejay-ws-sandbox-1` runner placeholder. Andrew explicitly does NOT want
|
# Path A/B/C install history is preserved in git log only.
|
||||||
# BLUEJAY-WS registered as a runner (workstation has personal/operator state).
|
|
||||||
#
|
|
||||||
# Storage layout (2026-05-08):
|
|
||||||
# * ISO is now sourced from Synology NFS (Path B) — see
|
|
||||||
# win2025-iso-nfs-pv.yaml. The Longhorn Filesystem PVC
|
|
||||||
# `windows-server-2025-iso` below is RETAINED but UNUSED so the prior
|
|
||||||
# CDI upload state is preserved as a fallback (and so ArgoCD doesn't
|
|
||||||
# prune it on this commit). It can be deleted in a follow-up commit
|
|
||||||
# after the NFS path is proven on a successful Windows install.
|
|
||||||
#
|
|
||||||
# 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
|
||||||
@@ -57,248 +15,6 @@ 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).
|
|
||||||
#
|
|
||||||
# **Volume mode (2026-05-08 status):** Filesystem-mode PVC. A migration to
|
|
||||||
# `volumeMode: Block` via DataVolume was attempted to address an OVMF SATA
|
|
||||||
# CDROM read timeout, but CDI v1.65.0's upload-target pod runs as uid 107
|
|
||||||
# with `capabilities.drop: [ALL]` and cannot open the underlying block
|
|
||||||
# device (`blockdev: cannot open /dev/cdi-block-volume: Permission denied`).
|
|
||||||
# Reverted to Filesystem PVC pending one of:
|
|
||||||
# - CDI deployment override granting CAP_SYS_RAWIO to upload pod
|
|
||||||
# - Pre-populated PVC via privileged init pod that dd's the ISO directly
|
|
||||||
# - Migration to a different storage class that exposes block devices
|
|
||||||
# differently (e.g. iSCSI, where Longhorn's CSI mount path may behave
|
|
||||||
# differently)
|
|
||||||
#
|
|
||||||
# Population workflow (this PVC, Filesystem mode):
|
|
||||||
# 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://localhost:8443 --insecure
|
|
||||||
# (--uploadproxy-url uses port-forward in practice: `kubectl port-forward
|
|
||||||
# -n cdi service/cdi-uploadproxy 8443:443 &` first.)
|
|
||||||
#
|
|
||||||
# **Open boot issue:** even with the ISO at bootOrder:1, OVMF console showed:
|
|
||||||
# BdsDxe: starting Boot0001 "UEFI QEMU DVD-ROM QM00001 " from ... Sata(...)
|
|
||||||
# BdsDxe: failed to start Boot0001 ... Time out
|
|
||||||
# Diagnosis confirmed PVC content IS a valid bootable ISO9660 image — the
|
|
||||||
# timeout is in OVMF reading from the SATA-CDROM-backed-by-filesystem-PVC.
|
|
||||||
# Block mode would likely fix it; see CDI permission issue above.
|
|
||||||
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 # Server 2025 ISO is 7.7GB; 10Gi for headroom
|
|
||||||
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:
|
||||||
@@ -309,33 +25,7 @@ metadata:
|
|||||||
role: github-actions-runner
|
role: github-actions-runner
|
||||||
flowercore.io/managed-by: bluejay-infra
|
flowercore.io/managed-by: bluejay-infra
|
||||||
spec:
|
spec:
|
||||||
# `running: true` is deprecated in favor of `runStrategy`. They are mutually
|
runStrategy: Always
|
||||||
# exclusive — KubeVirt's validating webhook rejects any VM that sets both:
|
|
||||||
# admission webhook "virtualmachine-validator.kubevirt.io" denied the request:
|
|
||||||
# Running and RunStrategy are mutually exclusive.
|
|
||||||
# `Always` keeps a VMI running and restarts it if it crashes/exits — same
|
|
||||||
# semantics as the old `running: true`.
|
|
||||||
#
|
|
||||||
# **2026-05-08 status: VM cannot start due to a stale QEMU flock on the
|
|
||||||
# rootdisk PVC** (qemu reports `Failed to get "write" lock` on
|
|
||||||
# `/var/run/kubevirt-private/vmi-disks/rootdisk/disk.img`). The flock was
|
|
||||||
# left by a previous QEMU process during a force-deleted launcher pod
|
|
||||||
# cycle. Recovery requires either (a) a Longhorn engine restart on
|
|
||||||
# rke2-agent2, (b) a Longhorn volume detach via the longhorn-manager API
|
|
||||||
# (kubectl patch on `volume.longhorn.io/<pvc-name>` does not work — the
|
|
||||||
# spec.nodeID is reconciled back), or (c) a node reboot of rke2-agent2.
|
|
||||||
#
|
|
||||||
# **Confirmed working:** the bootOrder swap (windows-iso=1, rootdisk=2)
|
|
||||||
# and the runStrategy migration (above). The ISO PVC was successfully
|
|
||||||
# repopulated via virtctl image-upload pvc on the Filesystem-mode PVC.
|
|
||||||
#
|
|
||||||
# **Open: SATA CDROM read timeout** — even with bootOrder=1, OVMF reported
|
|
||||||
# `BdsDxe: failed to start Boot0001 ... Time out` reading the SATA CDROM
|
|
||||||
# backed by the Filesystem-mode PVC. A switch to Block-mode DataVolume
|
|
||||||
# was attempted but blocked by a CDI v1.65.0 upload-pod permission issue
|
|
||||||
# (capability drop prevents writing to the underlying block device).
|
|
||||||
# See header docstring on the ISO PVC.
|
|
||||||
runStrategy: Always # LIVE — ISO uploaded 2026-05-08, password in 1P
|
|
||||||
template:
|
template:
|
||||||
metadata:
|
metadata:
|
||||||
labels:
|
labels:
|
||||||
@@ -377,73 +67,16 @@ spec:
|
|||||||
firmware:
|
firmware:
|
||||||
bootloader:
|
bootloader:
|
||||||
efi:
|
efi:
|
||||||
# 2026-05-08: SecureBoot=false during initial install. With SecureBoot
|
|
||||||
# enabled, OVMF's BdsDxe times out reading Boot0001 from the SCSI
|
|
||||||
# CDROM ("BdsDxe: failed to start Boot0001 ... Time out") before the
|
|
||||||
# EFI bootloader signature can verify against the OVMF VARS trust DB.
|
|
||||||
# KubeVirt's `/usr/share/OVMF/OVMF_VARS.secboot.fd` template doesn't
|
|
||||||
# appear to include the Microsoft KEK/DB by default, so signed
|
|
||||||
# Windows EFI bootloaders fail validation. Disabling SecureBoot lets
|
|
||||||
# OVMF skip the chain check and boot directly. This is acceptable for
|
|
||||||
# a CI runner — TPM 2.0 is still emulated (`tpm: {}` below) so
|
|
||||||
# BitLocker / Hyper-V / WSL still work.
|
|
||||||
# When the operator wants SecureBoot back, the path is:
|
|
||||||
# 1. Custom-build OVMF_VARS.fd with Microsoft KEK/DB enrolled
|
|
||||||
# 2. Mount it into the VM via firmware.bootloader.efi.persistent
|
|
||||||
# 3. Set secureBoot: true again
|
|
||||||
# Tracked separately from the install unblock.
|
|
||||||
secureBoot: false
|
secureBoot: false
|
||||||
devices:
|
devices:
|
||||||
tpm: {} # Non-persistent vTPM — sufficient for runner; no BitLocker
|
tpm: {}
|
||||||
disks:
|
disks:
|
||||||
# bootOrder: ISO must be 1 for first-boot install (the rootdisk has no
|
|
||||||
# EFI bootloader yet). After Windows installs, it writes its own UEFI
|
|
||||||
# Boot#### entries pointing at the rootdisk's EFI partition; UEFI then
|
|
||||||
# boots from rootdisk going forward and the ISO at bootOrder:2 acts as
|
|
||||||
# a fallback for re-install scenarios.
|
|
||||||
#
|
|
||||||
# Original (broken) order had rootdisk=1, windows-iso=2 — UEFI tried
|
|
||||||
# the empty virtio disk first, got nothing, fell back to the SATA
|
|
||||||
# CDROM at Boot0001 with a short timeout, and timed out before the
|
|
||||||
# CDROM enumerated. Console showed:
|
|
||||||
# BdsDxe: failed to start Boot0001 ... Time out
|
|
||||||
# BdsDxe: No bootable option or device was found.
|
|
||||||
# Confirmed via debug pod: PVC content IS a real bootable ISO9660
|
|
||||||
# (file: "ISO 9660 CD-ROM filesystem data ... (bootable)"), so the
|
|
||||||
# only bug was boot priority.
|
|
||||||
# 2026-05-08 PM: cdrom bus SCSI + containerDisk delivery. This
|
|
||||||
# combination boots qemu cleanly and reaches OVMF, but OVMF
|
|
||||||
# BdsDxe still hits "starting Boot0001 ... Time out" on the
|
|
||||||
# cdrom — see HANDOFF.md / CODEX-STATUS.md "OPEN — ci1" for the
|
|
||||||
# full diagnostic chain. virtio-blk disk swap was attempted as a
|
|
||||||
# workaround but introduced a separate QEMU rootdisk flock issue
|
|
||||||
# without fixing the underlying OVMF cdrom problem; reverted.
|
|
||||||
# Operator decision needed for next architectural step (OVMF
|
|
||||||
# custom build with extended timeout, KubeVirt version bump,
|
|
||||||
# Hyper-V/VirtualBox-and-export, or BIOS legacy boot). The
|
|
||||||
# containerDisk distribution pipeline (build/save/scp/ctr import)
|
|
||||||
# is proven and ready to reuse for any of those.
|
|
||||||
- name: windows-iso
|
|
||||||
bootOrder: 1
|
|
||||||
cdrom:
|
|
||||||
bus: scsi
|
|
||||||
- name: rootdisk
|
- name: rootdisk
|
||||||
bootOrder: 2
|
|
||||||
disk:
|
disk:
|
||||||
bus: virtio
|
bus: virtio
|
||||||
- name: virtio-drivers
|
|
||||||
cdrom:
|
|
||||||
bus: sata
|
|
||||||
- name: sysprep
|
|
||||||
cdrom:
|
|
||||||
bus: sata
|
|
||||||
interfaces:
|
interfaces:
|
||||||
# Pod-network fallback for Phase 1. To switch to PROD VLAN once Multus
|
# Pod-network fallback for CI runner outbound traffic. Switch to
|
||||||
# + the prod-vlan57 NAD exist, replace this block with:
|
# prod-vlan57 once the bridge/NAD lane is ready for L2 access.
|
||||||
# - 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
|
||||||
@@ -454,55 +87,7 @@ spec:
|
|||||||
pod: {}
|
pod: {}
|
||||||
volumes:
|
volumes:
|
||||||
- name: rootdisk
|
- name: rootdisk
|
||||||
persistentVolumeClaim:
|
|
||||||
claimName: ci1-rootdisk
|
|
||||||
- name: windows-iso
|
|
||||||
# 2026-05-08 PM (Path C, CONTAINERDISK): the ISO is now packaged as
|
|
||||||
# a KubeVirt containerDisk OCI image baked from
|
|
||||||
# `FROM scratch ; ADD --chown=107:107 disk.img /disk/disk.img`.
|
|
||||||
# The qemu user (uid 107) reads the ISO directly from a tmpfs view
|
|
||||||
# of the OCI layer, bypassing both:
|
|
||||||
# - Synology NFS export ACL (Path B failed: uid 107 denied at
|
|
||||||
# directory level even with mode 0777, see memory
|
|
||||||
# feedback_synology_iso_export_root_only_uid_107_denied)
|
|
||||||
# - OVMF cdrom read-window timeout (Path A and Path B's SCSI
|
|
||||||
# retry both hit `BdsDxe: failed to start Boot0001 ... Time out`
|
|
||||||
# when the cdrom was backed by a PVC the storage controller
|
|
||||||
# couldn't satisfy reads from fast enough).
|
|
||||||
#
|
|
||||||
# Image build (one-time, per ISO version):
|
|
||||||
# 1. Copy ISO to disk.img, write Dockerfile
|
|
||||||
# 2. podman build --tag localhost/win-server-2025:1.0 . (on noc1)
|
|
||||||
# 3. podman save -o win-server-2025-1.0.tar localhost/win-server-2025:1.0
|
|
||||||
# 4. SCP tar to all 3 RKE2 nodes (rke2-server, rke2-agent1, rke2-agent2)
|
|
||||||
# 5. sudo /var/lib/rancher/rke2/bin/ctr -a /run/k3s/containerd/containerd.sock \
|
|
||||||
# -n k8s.io images import /tmp/win-server-2025-1.0.tar
|
|
||||||
# Standard FC pattern per `feedback_rke2_localhost_imagepullpolicy`.
|
|
||||||
#
|
|
||||||
# When a new Windows ISO version ships, bump the tag (1.1, 1.2, ...),
|
|
||||||
# rebuild + redistribute, and update the image: line below in a new
|
|
||||||
# commit. KubeVirt picks up the new image via a VM restart.
|
|
||||||
#
|
|
||||||
# The legacy NFS PVC + PV (apps/kubevirt-vms/win2025-iso-nfs-pv.yaml)
|
|
||||||
# and CDI Longhorn PVC (`windows-server-2025-iso`) are RETAINED for
|
|
||||||
# this commit so the prior states are recoverable. Once the
|
|
||||||
# containerDisk path proves on a successful Windows install, both
|
|
||||||
# legacy artifacts can be pruned in a follow-up commit.
|
|
||||||
containerDisk:
|
containerDisk:
|
||||||
image: localhost/win-server-2025:1.0
|
image: localhost/fc-win-server-2025:v1
|
||||||
imagePullPolicy: Never
|
imagePullPolicy: Never
|
||||||
- name: virtio-drivers
|
|
||||||
containerDisk:
|
|
||||||
# Pinned to v1.8.2 (latest stable as of 2026-05-08).
|
|
||||||
# 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
|
||||||
|
|||||||
3
apps/kubevirt-vms/kustomization.yaml
Normal file
3
apps/kubevirt-vms/kustomization.yaml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
resources:
|
||||||
|
- ci1.yaml
|
||||||
|
- prod-vlan57-nad.yaml
|
||||||
416
scripts/authentik-bulk-client-create.py
Normal file
416
scripts/authentik-bulk-client-create.py
Normal file
@@ -0,0 +1,416 @@
|
|||||||
|
#!/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())
|
||||||
192
tests/bluejay-infra-lint/AuthentikOidcClientRegistrationTests.cs
Normal file
192
tests/bluejay-infra-lint/AuthentikOidcClientRegistrationTests.cs
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
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