Compare commits
1 Commits
783720cc58
...
feat/authe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
41c598394e |
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
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
# Runtime secrets for FlowerCore.DeviceManagement.
|
|
||||||
#
|
|
||||||
# OnePasswordItem operator syncs this item into a Kubernetes Secret with the
|
|
||||||
# same name. Expected fields:
|
|
||||||
# DB-Password
|
|
||||||
# mtls-ca.pem
|
|
||||||
# mtls-client.crt
|
|
||||||
# mtls-client.key
|
|
||||||
# mtls-chain.pem
|
|
||||||
#
|
|
||||||
# Do not add literal secret values to this repo. Runtime pods consume the
|
|
||||||
# synced Secret through env vars and read-only mounts.
|
|
||||||
apiVersion: onepassword.com/v1
|
|
||||||
kind: OnePasswordItem
|
|
||||||
metadata:
|
|
||||||
name: fc-devicemgmt-runtime
|
|
||||||
namespace: fc-devicemgmt
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: fc-devicemgmt
|
|
||||||
app.kubernetes.io/component: secrets
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
app.kubernetes.io/managed-by: argocd
|
|
||||||
flowercore.io/tenant-id: system
|
|
||||||
flowercore.io/created-by: bluejay-infra
|
|
||||||
spec:
|
|
||||||
itemPath: "vaults/IAmWorkin/items/FlowerCore DeviceManagement Runtime"
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
# Explicit ArgoCD Application shape for bootstrap/review.
|
|
||||||
#
|
|
||||||
# The live bluejay-infra ApplicationSet already discovers apps/* directories
|
|
||||||
# and creates this same Application name (`infra-fc-devicemgmt`) automatically.
|
|
||||||
# Keep repoURL on the internal Gitea ClusterIP URL; ArgoCD does not trust the
|
|
||||||
# external step-ca HTTPS endpoint.
|
|
||||||
apiVersion: argoproj.io/v1alpha1
|
|
||||||
kind: Application
|
|
||||||
metadata:
|
|
||||||
name: infra-fc-devicemgmt
|
|
||||||
namespace: argocd
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: fc-devicemgmt
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
app.kubernetes.io/managed-by: argocd
|
|
||||||
flowercore.io/tenant-id: system
|
|
||||||
flowercore.io/created-by: bluejay-infra
|
|
||||||
spec:
|
|
||||||
project: default
|
|
||||||
source:
|
|
||||||
repoURL: http://gitea-clusterip.gitea.svc.cluster.local:3000/bluejay/bluejay-infra.git
|
|
||||||
targetRevision: main
|
|
||||||
path: apps/fc-devicemgmt
|
|
||||||
destination:
|
|
||||||
server: https://kubernetes.default.svc
|
|
||||||
namespace: fc-devicemgmt
|
|
||||||
syncPolicy:
|
|
||||||
automated:
|
|
||||||
prune: true
|
|
||||||
selfHeal: true
|
|
||||||
syncOptions:
|
|
||||||
- CreateNamespace=true
|
|
||||||
- ServerSideApply=true
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
# Certificate for devices.iamworkin.lan.
|
|
||||||
#
|
|
||||||
# Preflight gate: FlowerCore.DNS / pfSense must contain an explicit A record:
|
|
||||||
# devices.iamworkin.lan -> 10.0.56.200
|
|
||||||
# before this Certificate is synced. step-ca ACME cannot see the CoreDNS
|
|
||||||
# wildcard, so missing pfSense DNS produces cert-manager HTTP-01 backoff
|
|
||||||
# (feedback_pfsense_dns_required_for_acme).
|
|
||||||
apiVersion: cert-manager.io/v1
|
|
||||||
kind: Certificate
|
|
||||||
metadata:
|
|
||||||
name: fc-devicemgmt-web-tls
|
|
||||||
namespace: fc-devicemgmt
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: fc-devicemgmt-web
|
|
||||||
app.kubernetes.io/component: web
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
app.kubernetes.io/managed-by: argocd
|
|
||||||
flowercore.io/tenant-id: system
|
|
||||||
flowercore.io/created-by: bluejay-infra
|
|
||||||
annotations:
|
|
||||||
flowercore.io/dns-preflight: "devices.iamworkin.lan must resolve to 10.0.56.200 before ACME sync"
|
|
||||||
spec:
|
|
||||||
secretName: fc-devicemgmt-web-tls
|
|
||||||
issuerRef:
|
|
||||||
name: step-ca-acme
|
|
||||||
kind: ClusterIssuer
|
|
||||||
dnsNames:
|
|
||||||
- devices.iamworkin.lan
|
|
||||||
duration: 720h
|
|
||||||
renewBefore: 240h
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
apiVersion: rbac.authorization.k8s.io/v1
|
|
||||||
kind: ClusterRole
|
|
||||||
metadata:
|
|
||||||
name: fc-devicemgmt-operator
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: fc-devicemgmt-operator
|
|
||||||
app.kubernetes.io/component: operator
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
app.kubernetes.io/managed-by: argocd
|
|
||||||
flowercore.io/tenant-id: system
|
|
||||||
flowercore.io/created-by: bluejay-infra
|
|
||||||
rules:
|
|
||||||
- apiGroups:
|
|
||||||
- devices.flowercore.io
|
|
||||||
resources:
|
|
||||||
- '*'
|
|
||||||
verbs:
|
|
||||||
- get
|
|
||||||
- list
|
|
||||||
- watch
|
|
||||||
- create
|
|
||||||
- update
|
|
||||||
- patch
|
|
||||||
- delete
|
|
||||||
- apiGroups:
|
|
||||||
- devices.flowercore.io
|
|
||||||
resources:
|
|
||||||
- devices/status
|
|
||||||
- devices/finalizers
|
|
||||||
- devicegroups/status
|
|
||||||
- devicegroups/finalizers
|
|
||||||
- devicepolicies/status
|
|
||||||
- devicepolicies/finalizers
|
|
||||||
- remotecommands/status
|
|
||||||
- remotecommands/finalizers
|
|
||||||
verbs:
|
|
||||||
- get
|
|
||||||
- update
|
|
||||||
- patch
|
|
||||||
- apiGroups:
|
|
||||||
- apps
|
|
||||||
resources:
|
|
||||||
- deployments
|
|
||||||
verbs:
|
|
||||||
- get
|
|
||||||
- apiGroups:
|
|
||||||
- ""
|
|
||||||
resources:
|
|
||||||
- pods
|
|
||||||
- services
|
|
||||||
- configmaps
|
|
||||||
- secrets
|
|
||||||
- events
|
|
||||||
verbs:
|
|
||||||
- get
|
|
||||||
- list
|
|
||||||
- watch
|
|
||||||
- create
|
|
||||||
- update
|
|
||||||
- patch
|
|
||||||
- delete
|
|
||||||
- apiGroups:
|
|
||||||
- batch
|
|
||||||
resources:
|
|
||||||
- jobs
|
|
||||||
verbs:
|
|
||||||
- get
|
|
||||||
- list
|
|
||||||
- watch
|
|
||||||
- create
|
|
||||||
- update
|
|
||||||
- patch
|
|
||||||
- delete
|
|
||||||
- apiGroups:
|
|
||||||
- networking.k8s.io
|
|
||||||
resources:
|
|
||||||
- networkpolicies
|
|
||||||
verbs:
|
|
||||||
- get
|
|
||||||
- list
|
|
||||||
- watch
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
apiVersion: rbac.authorization.k8s.io/v1
|
|
||||||
kind: ClusterRoleBinding
|
|
||||||
metadata:
|
|
||||||
name: fc-devicemgmt-operator
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: fc-devicemgmt-operator
|
|
||||||
app.kubernetes.io/component: operator
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
app.kubernetes.io/managed-by: argocd
|
|
||||||
flowercore.io/tenant-id: system
|
|
||||||
flowercore.io/created-by: bluejay-infra
|
|
||||||
roleRef:
|
|
||||||
apiGroup: rbac.authorization.k8s.io
|
|
||||||
kind: ClusterRole
|
|
||||||
name: fc-devicemgmt-operator
|
|
||||||
subjects:
|
|
||||||
- kind: ServiceAccount
|
|
||||||
name: fc-devicemgmt-operator
|
|
||||||
namespace: fc-devicemgmt
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
# FlowerCore.DeviceManagement Operator.
|
|
||||||
#
|
|
||||||
# KubeOps controller for devices.flowercore.io resources. Operator-created
|
|
||||||
# children must set OwnerReferences + traceability labels/annotations per
|
|
||||||
# k8s-pod-ownership-and-traceability-standard.md. RBAC below grants
|
|
||||||
# apps/deployments/get so the process can resolve its own Deployment UID.
|
|
||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: fc-devicemgmt-operator
|
|
||||||
namespace: fc-devicemgmt
|
|
||||||
labels:
|
|
||||||
app: fc-devicemgmt-operator
|
|
||||||
app.kubernetes.io/name: fc-devicemgmt-operator
|
|
||||||
app.kubernetes.io/component: operator
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
app.kubernetes.io/managed-by: argocd
|
|
||||||
flowercore.io/tenant-id: system
|
|
||||||
flowercore.io/created-by: bluejay-infra
|
|
||||||
annotations:
|
|
||||||
flowercore.io/traceability-standard: k8s-pod-ownership-and-traceability-standard
|
|
||||||
spec:
|
|
||||||
replicas: 1
|
|
||||||
revisionHistoryLimit: 3
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app: fc-devicemgmt-operator
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app: fc-devicemgmt-operator
|
|
||||||
app.kubernetes.io/name: fc-devicemgmt-operator
|
|
||||||
app.kubernetes.io/component: operator
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
app.kubernetes.io/managed-by: argocd
|
|
||||||
flowercore.io/tenant-id: system
|
|
||||||
flowercore.io/created-by: bluejay-infra
|
|
||||||
annotations:
|
|
||||||
prometheus.io/scrape: "true"
|
|
||||||
prometheus.io/port: "8080"
|
|
||||||
prometheus.io/path: "/metrics"
|
|
||||||
flowercore.io/audit-trace-id: "runtime-activity-trace"
|
|
||||||
spec:
|
|
||||||
serviceAccountName: fc-devicemgmt-operator
|
|
||||||
securityContext:
|
|
||||||
fsGroup: 1654
|
|
||||||
fsGroupChangePolicy: OnRootMismatch
|
|
||||||
containers:
|
|
||||||
- name: operator
|
|
||||||
image: localhost/fc-devicemgmt-operator:v20260512-cx5
|
|
||||||
imagePullPolicy: Never
|
|
||||||
ports:
|
|
||||||
- name: metrics
|
|
||||||
containerPort: 8080
|
|
||||||
env:
|
|
||||||
- name: ASPNETCORE_ENVIRONMENT
|
|
||||||
value: "Production"
|
|
||||||
- name: DOTNET_SYSTEM_GLOBALIZATION_INVARIANT
|
|
||||||
value: "false"
|
|
||||||
- name: POD_NAME
|
|
||||||
valueFrom:
|
|
||||||
fieldRef:
|
|
||||||
fieldPath: metadata.name
|
|
||||||
- name: POD_NAMESPACE
|
|
||||||
valueFrom:
|
|
||||||
fieldRef:
|
|
||||||
fieldPath: metadata.namespace
|
|
||||||
- name: FLOWERCORE_KUBERNETES_OWNER_DEPLOYMENT
|
|
||||||
value: "fc-devicemgmt-operator"
|
|
||||||
- name: FlowerCore__Service__Name
|
|
||||||
value: "FlowerCore.DeviceManagement.Operator"
|
|
||||||
- name: FlowerCore__DeviceManagement__DefaultTenantId
|
|
||||||
value: "system"
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
cpu: 50m
|
|
||||||
memory: 128Mi
|
|
||||||
limits:
|
|
||||||
cpu: 500m
|
|
||||||
memory: 512Mi
|
|
||||||
readinessProbe:
|
|
||||||
tcpSocket:
|
|
||||||
port: 8080
|
|
||||||
initialDelaySeconds: 5
|
|
||||||
periodSeconds: 10
|
|
||||||
livenessProbe:
|
|
||||||
tcpSocket:
|
|
||||||
port: 8080
|
|
||||||
initialDelaySeconds: 20
|
|
||||||
periodSeconds: 30
|
|
||||||
securityContext:
|
|
||||||
runAsNonRoot: true
|
|
||||||
runAsUser: 1654
|
|
||||||
runAsGroup: 1654
|
|
||||||
allowPrivilegeEscalation: false
|
|
||||||
readOnlyRootFilesystem: true
|
|
||||||
capabilities:
|
|
||||||
drop:
|
|
||||||
- ALL
|
|
||||||
volumeMounts:
|
|
||||||
- name: tmp
|
|
||||||
mountPath: /tmp
|
|
||||||
- name: logs
|
|
||||||
mountPath: /app/logs
|
|
||||||
volumes:
|
|
||||||
- name: tmp
|
|
||||||
emptyDir: {}
|
|
||||||
- name: logs
|
|
||||||
emptyDir: {}
|
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
# FlowerCore.DeviceManagement Web.
|
|
||||||
#
|
|
||||||
# Source repo is expected to ship FlowerCore.DeviceManagement.Web in a later
|
|
||||||
# Sprint 9+ lane. This manifest is static-valid without requiring the image to
|
|
||||||
# exist yet; import localhost/fc-devicemgmt-web:<tag> to all schedulable RKE2
|
|
||||||
# nodes before letting ArgoCD sync a live rollout.
|
|
||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: fc-devicemgmt-web
|
|
||||||
namespace: fc-devicemgmt
|
|
||||||
labels:
|
|
||||||
app: fc-devicemgmt-web
|
|
||||||
app.kubernetes.io/name: fc-devicemgmt-web
|
|
||||||
app.kubernetes.io/component: web
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
app.kubernetes.io/managed-by: argocd
|
|
||||||
flowercore.io/tenant-id: system
|
|
||||||
flowercore.io/created-by: bluejay-infra
|
|
||||||
annotations:
|
|
||||||
flowercore.io/traceability-standard: k8s-pod-ownership-and-traceability-standard
|
|
||||||
spec:
|
|
||||||
replicas: 2
|
|
||||||
revisionHistoryLimit: 3
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app: fc-devicemgmt-web
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app: fc-devicemgmt-web
|
|
||||||
app.kubernetes.io/name: fc-devicemgmt-web
|
|
||||||
app.kubernetes.io/component: web
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
app.kubernetes.io/managed-by: argocd
|
|
||||||
flowercore.io/tenant-id: system
|
|
||||||
flowercore.io/created-by: bluejay-infra
|
|
||||||
annotations:
|
|
||||||
prometheus.io/scrape: "true"
|
|
||||||
prometheus.io/port: "8080"
|
|
||||||
prometheus.io/path: "/metrics"
|
|
||||||
flowercore.io/audit-trace-id: "runtime-activity-trace"
|
|
||||||
spec:
|
|
||||||
securityContext:
|
|
||||||
fsGroup: 1654
|
|
||||||
fsGroupChangePolicy: OnRootMismatch
|
|
||||||
containers:
|
|
||||||
- name: web
|
|
||||||
image: localhost/fc-devicemgmt-web:v20260512-cx5
|
|
||||||
imagePullPolicy: Never
|
|
||||||
ports:
|
|
||||||
- name: http
|
|
||||||
containerPort: 8080
|
|
||||||
env:
|
|
||||||
- name: ASPNETCORE_URLS
|
|
||||||
value: "http://+:8080"
|
|
||||||
- name: ASPNETCORE_ENVIRONMENT
|
|
||||||
value: "Production"
|
|
||||||
- name: DOTNET_SYSTEM_GLOBALIZATION_INVARIANT
|
|
||||||
value: "false"
|
|
||||||
- name: FlowerCore__Service__Name
|
|
||||||
value: "FlowerCore.DeviceManagement.Web"
|
|
||||||
- name: FlowerCore__DeviceManagement__DefaultTenantId
|
|
||||||
value: "system"
|
|
||||||
- name: FlowerCore__Database__Provider
|
|
||||||
value: "MySql"
|
|
||||||
- name: FlowerCore__Database__Host
|
|
||||||
value: "mysql.fc-mysql.svc"
|
|
||||||
- name: FlowerCore__Database__Database
|
|
||||||
value: "flowercore_devicemgmt"
|
|
||||||
- name: FlowerCore__Database__User
|
|
||||||
value: "fc_devicemgmt"
|
|
||||||
- name: FlowerCore__Database__Password
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: fc-devicemgmt-runtime
|
|
||||||
key: DB-Password
|
|
||||||
- name: FlowerCore__DeviceManagement__AgentMtls__CaPath
|
|
||||||
value: "/secrets/devicemgmt-mtls/mtls-ca.pem"
|
|
||||||
- name: FlowerCore__DeviceManagement__AgentMtls__ClientCertificatePath
|
|
||||||
value: "/secrets/devicemgmt-mtls/mtls-client.crt"
|
|
||||||
- name: FlowerCore__DeviceManagement__AgentMtls__ClientKeyPath
|
|
||||||
value: "/secrets/devicemgmt-mtls/mtls-client.key"
|
|
||||||
- name: FlowerCore__EventBus__Redis__Configuration
|
|
||||||
value: "redis.fc-redis.svc:6379"
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
cpu: 100m
|
|
||||||
memory: 256Mi
|
|
||||||
limits:
|
|
||||||
cpu: 1000m
|
|
||||||
memory: 768Mi
|
|
||||||
startupProbe:
|
|
||||||
tcpSocket:
|
|
||||||
port: 8080
|
|
||||||
initialDelaySeconds: 5
|
|
||||||
periodSeconds: 5
|
|
||||||
failureThreshold: 30
|
|
||||||
readinessProbe:
|
|
||||||
tcpSocket:
|
|
||||||
port: 8080
|
|
||||||
periodSeconds: 10
|
|
||||||
failureThreshold: 3
|
|
||||||
livenessProbe:
|
|
||||||
tcpSocket:
|
|
||||||
port: 8080
|
|
||||||
initialDelaySeconds: 30
|
|
||||||
periodSeconds: 30
|
|
||||||
failureThreshold: 3
|
|
||||||
securityContext:
|
|
||||||
runAsNonRoot: true
|
|
||||||
runAsUser: 1654
|
|
||||||
runAsGroup: 1654
|
|
||||||
allowPrivilegeEscalation: false
|
|
||||||
readOnlyRootFilesystem: true
|
|
||||||
capabilities:
|
|
||||||
drop:
|
|
||||||
- ALL
|
|
||||||
volumeMounts:
|
|
||||||
- name: tmp
|
|
||||||
mountPath: /tmp
|
|
||||||
- name: logs
|
|
||||||
mountPath: /app/logs
|
|
||||||
- name: devicemgmt-mtls
|
|
||||||
mountPath: /secrets/devicemgmt-mtls
|
|
||||||
readOnly: true
|
|
||||||
volumes:
|
|
||||||
- name: tmp
|
|
||||||
emptyDir: {}
|
|
||||||
- name: logs
|
|
||||||
emptyDir: {}
|
|
||||||
- name: devicemgmt-mtls
|
|
||||||
secret:
|
|
||||||
secretName: fc-devicemgmt-runtime
|
|
||||||
defaultMode: 0400
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
# LAN ingress for FlowerCore.DeviceManagement Web.
|
|
||||||
#
|
|
||||||
# RKE2 Traefik has no built-in ACME resolver configured. Keep TLS certificate
|
|
||||||
# ownership in cert-manager Certificate/fc-devicemgmt-web-tls.
|
|
||||||
apiVersion: traefik.io/v1alpha1
|
|
||||||
kind: IngressRoute
|
|
||||||
metadata:
|
|
||||||
name: fc-devicemgmt-web
|
|
||||||
namespace: fc-devicemgmt
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: fc-devicemgmt-web
|
|
||||||
app.kubernetes.io/component: web
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
app.kubernetes.io/managed-by: argocd
|
|
||||||
flowercore.io/tenant-id: system
|
|
||||||
flowercore.io/created-by: bluejay-infra
|
|
||||||
spec:
|
|
||||||
entryPoints:
|
|
||||||
- websecure
|
|
||||||
routes:
|
|
||||||
- match: Host(`devices.iamworkin.lan`)
|
|
||||||
kind: Rule
|
|
||||||
services:
|
|
||||||
- name: fc-devicemgmt-web
|
|
||||||
port: 80
|
|
||||||
tls:
|
|
||||||
secretName: fc-devicemgmt-web-tls
|
|
||||||
|
|
||||||
# Future public agent/update host gate (OFF by default):
|
|
||||||
#
|
|
||||||
# Do not enable `update.flowercore.io` here until Authentik OIDC Q-OIDC-1
|
|
||||||
# resolves the public-device-management auth model and route ownership with
|
|
||||||
# UpdateCenter. When enabled, use a separate public IngressRoute with an
|
|
||||||
# explicit Method allowlist, public-host auth middleware, and public TLS
|
|
||||||
# certificate strategy. Leaving this as comments keeps ArgoCD from stealing
|
|
||||||
# live UpdateCenter traffic.
|
|
||||||
#
|
|
||||||
# apiVersion: traefik.io/v1alpha1
|
|
||||||
# kind: IngressRoute
|
|
||||||
# metadata:
|
|
||||||
# name: fc-devicemgmt-web-public
|
|
||||||
# namespace: fc-devicemgmt
|
|
||||||
# annotations:
|
|
||||||
# flowercore.io/public-host-gate: "disabled-until-Q-OIDC-1"
|
|
||||||
# spec:
|
|
||||||
# entryPoints:
|
|
||||||
# - websecure
|
|
||||||
# routes:
|
|
||||||
# - match: Host(`update.flowercore.io`) && (Method(`GET`) || Method(`HEAD`) || Method(`POST`) || Method(`OPTIONS`))
|
|
||||||
# kind: Rule
|
|
||||||
# services:
|
|
||||||
# - name: fc-devicemgmt-web
|
|
||||||
# port: 80
|
|
||||||
# tls:
|
|
||||||
# secretName: fc-devicemgmt-public-tls
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
# FlowerCore.DeviceManagement namespace.
|
|
||||||
#
|
|
||||||
# ArgoCD discovers this directory as Application `infra-fc-devicemgmt`.
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Namespace
|
|
||||||
metadata:
|
|
||||||
name: fc-devicemgmt
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: fc-devicemgmt
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
app.kubernetes.io/managed-by: argocd
|
|
||||||
flowercore.io/tenant-id: system
|
|
||||||
flowercore.io/created-by: bluejay-infra
|
|
||||||
@@ -1,224 +0,0 @@
|
|||||||
# FlowerCore.DeviceManagement NetworkPolicies.
|
|
||||||
#
|
|
||||||
# NetworkPolicies belong in bluejay-infra so ArgoCD owns rebuild state.
|
|
||||||
# Rules include Traefik post-DNAT backend ports per
|
|
||||||
# feedback_netpol_dnat_backend_port and Synology NFS egress for the requested
|
|
||||||
# cold-tier / future artifact path.
|
|
||||||
---
|
|
||||||
apiVersion: networking.k8s.io/v1
|
|
||||||
kind: NetworkPolicy
|
|
||||||
metadata:
|
|
||||||
name: fc-devicemgmt-web-isolation
|
|
||||||
namespace: fc-devicemgmt
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: fc-devicemgmt-web
|
|
||||||
app.kubernetes.io/component: web
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
app.kubernetes.io/managed-by: argocd
|
|
||||||
flowercore.io/tenant-id: system
|
|
||||||
flowercore.io/created-by: bluejay-infra
|
|
||||||
spec:
|
|
||||||
podSelector:
|
|
||||||
matchLabels:
|
|
||||||
app: fc-devicemgmt-web
|
|
||||||
policyTypes:
|
|
||||||
- Ingress
|
|
||||||
- Egress
|
|
||||||
ingress:
|
|
||||||
# LAN edge: only cluster Traefik should reach the Web pod for
|
|
||||||
# devices.iamworkin.lan.
|
|
||||||
- from:
|
|
||||||
- namespaceSelector:
|
|
||||||
matchLabels:
|
|
||||||
kubernetes.io/metadata.name: traefik-system
|
|
||||||
podSelector:
|
|
||||||
matchLabels:
|
|
||||||
app.kubernetes.io/name: traefik
|
|
||||||
ports:
|
|
||||||
- port: 8080
|
|
||||||
protocol: TCP
|
|
||||||
# Direct LAN diagnostics are allowed only from FlowerCore LAN/VPN ranges.
|
|
||||||
- from:
|
|
||||||
- ipBlock:
|
|
||||||
cidr: 10.0.56.0/24
|
|
||||||
- ipBlock:
|
|
||||||
cidr: 10.0.57.0/24
|
|
||||||
- ipBlock:
|
|
||||||
cidr: 10.0.58.0/24
|
|
||||||
- ipBlock:
|
|
||||||
cidr: 10.0.68.0/27
|
|
||||||
ports:
|
|
||||||
- port: 8080
|
|
||||||
protocol: TCP
|
|
||||||
egress:
|
|
||||||
# CoreDNS.
|
|
||||||
- to:
|
|
||||||
- namespaceSelector:
|
|
||||||
matchLabels:
|
|
||||||
kubernetes.io/metadata.name: kube-system
|
|
||||||
podSelector:
|
|
||||||
matchLabels:
|
|
||||||
k8s-app: kube-dns
|
|
||||||
ports:
|
|
||||||
- port: 53
|
|
||||||
protocol: UDP
|
|
||||||
- port: 53
|
|
||||||
protocol: TCP
|
|
||||||
# Database namespace.
|
|
||||||
- to:
|
|
||||||
- namespaceSelector:
|
|
||||||
matchLabels:
|
|
||||||
kubernetes.io/metadata.name: fc-mysql
|
|
||||||
ports:
|
|
||||||
- port: 3306
|
|
||||||
protocol: TCP
|
|
||||||
# Redis backplane for multi-replica SignalR / live-status fan-out.
|
|
||||||
- to:
|
|
||||||
- namespaceSelector:
|
|
||||||
matchLabels:
|
|
||||||
kubernetes.io/metadata.name: fc-redis
|
|
||||||
ports:
|
|
||||||
- port: 6379
|
|
||||||
protocol: TCP
|
|
||||||
# Traefik VIP / in-cluster Traefik for self-callbacks and public URL
|
|
||||||
# generation tests. Include post-DNAT backend ports 8443 + 8080.
|
|
||||||
- to:
|
|
||||||
- ipBlock:
|
|
||||||
cidr: 10.0.56.200/32
|
|
||||||
- namespaceSelector:
|
|
||||||
matchLabels:
|
|
||||||
kubernetes.io/metadata.name: traefik-system
|
|
||||||
podSelector:
|
|
||||||
matchLabels:
|
|
||||||
app.kubernetes.io/name: traefik
|
|
||||||
ports:
|
|
||||||
- port: 80
|
|
||||||
protocol: TCP
|
|
||||||
- port: 443
|
|
||||||
protocol: TCP
|
|
||||||
- port: 8080
|
|
||||||
protocol: TCP
|
|
||||||
- port: 8443
|
|
||||||
protocol: TCP
|
|
||||||
# Agent egress: LAN/VPN devices may run DM Agent in Generic, Kiosk, Pi,
|
|
||||||
# ThinClient, or Server mode. Keep this private-range only.
|
|
||||||
- to:
|
|
||||||
- ipBlock:
|
|
||||||
cidr: 10.0.56.0/24
|
|
||||||
- ipBlock:
|
|
||||||
cidr: 10.0.57.0/24
|
|
||||||
- ipBlock:
|
|
||||||
cidr: 10.0.58.0/24
|
|
||||||
- ipBlock:
|
|
||||||
cidr: 10.0.68.0/27
|
|
||||||
ports:
|
|
||||||
- port: 80
|
|
||||||
protocol: TCP
|
|
||||||
- port: 443
|
|
||||||
protocol: TCP
|
|
||||||
- port: 8080
|
|
||||||
protocol: TCP
|
|
||||||
- port: 8443
|
|
||||||
protocol: TCP
|
|
||||||
- port: 5000
|
|
||||||
protocol: TCP
|
|
||||||
- port: 5001
|
|
||||||
protocol: TCP
|
|
||||||
# Synology NFS cold-tier / artifact mount allowance.
|
|
||||||
- to:
|
|
||||||
- ipBlock:
|
|
||||||
cidr: 10.0.58.3/32
|
|
||||||
ports:
|
|
||||||
- port: 2049
|
|
||||||
protocol: TCP
|
|
||||||
- port: 2049
|
|
||||||
protocol: UDP
|
|
||||||
- port: 111
|
|
||||||
protocol: TCP
|
|
||||||
- port: 111
|
|
||||||
protocol: UDP
|
|
||||||
---
|
|
||||||
apiVersion: networking.k8s.io/v1
|
|
||||||
kind: NetworkPolicy
|
|
||||||
metadata:
|
|
||||||
name: fc-devicemgmt-operator-isolation
|
|
||||||
namespace: fc-devicemgmt
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: fc-devicemgmt-operator
|
|
||||||
app.kubernetes.io/component: operator
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
app.kubernetes.io/managed-by: argocd
|
|
||||||
flowercore.io/tenant-id: system
|
|
||||||
flowercore.io/created-by: bluejay-infra
|
|
||||||
spec:
|
|
||||||
podSelector:
|
|
||||||
matchLabels:
|
|
||||||
app: fc-devicemgmt-operator
|
|
||||||
policyTypes:
|
|
||||||
- Ingress
|
|
||||||
- Egress
|
|
||||||
ingress:
|
|
||||||
- from:
|
|
||||||
- namespaceSelector:
|
|
||||||
matchLabels:
|
|
||||||
kubernetes.io/metadata.name: monitoring
|
|
||||||
ports:
|
|
||||||
- port: 8080
|
|
||||||
protocol: TCP
|
|
||||||
egress:
|
|
||||||
# CoreDNS.
|
|
||||||
- to:
|
|
||||||
- namespaceSelector:
|
|
||||||
matchLabels:
|
|
||||||
kubernetes.io/metadata.name: kube-system
|
|
||||||
podSelector:
|
|
||||||
matchLabels:
|
|
||||||
k8s-app: kube-dns
|
|
||||||
ports:
|
|
||||||
- port: 53
|
|
||||||
protocol: UDP
|
|
||||||
- port: 53
|
|
||||||
protocol: TCP
|
|
||||||
# Kubernetes API for KubeOps reconciliation and Deployment UID lookup.
|
|
||||||
- to: []
|
|
||||||
ports:
|
|
||||||
- port: 443
|
|
||||||
protocol: TCP
|
|
||||||
- port: 6443
|
|
||||||
protocol: TCP
|
|
||||||
# Agent egress for operator-initiated probes / fallback command dispatch.
|
|
||||||
- to:
|
|
||||||
- ipBlock:
|
|
||||||
cidr: 10.0.56.0/24
|
|
||||||
- ipBlock:
|
|
||||||
cidr: 10.0.57.0/24
|
|
||||||
- ipBlock:
|
|
||||||
cidr: 10.0.58.0/24
|
|
||||||
- ipBlock:
|
|
||||||
cidr: 10.0.68.0/27
|
|
||||||
ports:
|
|
||||||
- port: 80
|
|
||||||
protocol: TCP
|
|
||||||
- port: 443
|
|
||||||
protocol: TCP
|
|
||||||
- port: 8080
|
|
||||||
protocol: TCP
|
|
||||||
- port: 8443
|
|
||||||
protocol: TCP
|
|
||||||
- port: 5000
|
|
||||||
protocol: TCP
|
|
||||||
- port: 5001
|
|
||||||
protocol: TCP
|
|
||||||
# Synology NFS allowance for future cold-tier/audit archival jobs.
|
|
||||||
- to:
|
|
||||||
- ipBlock:
|
|
||||||
cidr: 10.0.58.3/32
|
|
||||||
ports:
|
|
||||||
- port: 2049
|
|
||||||
protocol: TCP
|
|
||||||
- port: 2049
|
|
||||||
protocol: UDP
|
|
||||||
- port: 111
|
|
||||||
protocol: TCP
|
|
||||||
- port: 111
|
|
||||||
protocol: UDP
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: fc-devicemgmt-web
|
|
||||||
namespace: fc-devicemgmt
|
|
||||||
labels:
|
|
||||||
app: fc-devicemgmt-web
|
|
||||||
app.kubernetes.io/name: fc-devicemgmt-web
|
|
||||||
app.kubernetes.io/component: web
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
app.kubernetes.io/managed-by: argocd
|
|
||||||
flowercore.io/tenant-id: system
|
|
||||||
flowercore.io/created-by: bluejay-infra
|
|
||||||
spec:
|
|
||||||
type: ClusterIP
|
|
||||||
selector:
|
|
||||||
app: fc-devicemgmt-web
|
|
||||||
ports:
|
|
||||||
- name: http
|
|
||||||
port: 80
|
|
||||||
targetPort: 8080
|
|
||||||
protocol: TCP
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: ServiceAccount
|
|
||||||
metadata:
|
|
||||||
name: fc-devicemgmt-operator
|
|
||||||
namespace: fc-devicemgmt
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: fc-devicemgmt-operator
|
|
||||||
app.kubernetes.io/component: operator
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
app.kubernetes.io/managed-by: argocd
|
|
||||||
flowercore.io/tenant-id: system
|
|
||||||
flowercore.io/created-by: bluejay-infra
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
# fc-signage-appletv
|
|
||||||
|
|
||||||
Apple TV signage is a sealed appliance running the `FlowerCore.Signage.Agent.AppleTv` tvOS app per ADR-134.
|
|
||||||
|
|
||||||
This ApplicationSet entry is documentation and inventory metadata only. It intentionally creates no `Deployment`, `Service`, or `Pod`.
|
|
||||||
|
|
||||||
The Apple TV app connects outbound to existing FC.Signage.Web surfaces:
|
|
||||||
|
|
||||||
- `https://signage.iamworkin.lan/hub/signage` for SignalR live status.
|
|
||||||
- `GET /api/v1/nodes/{nodeId}/state` for the 30 second polling fallback.
|
|
||||||
- `POST /api/v1/nodes/register` and `POST /api/v1/nodes/{nodeId}/enroll` for pairing and mTLS enrollment.
|
|
||||||
- `POST /api/v1/nodes/{nodeId}/heartbeat` for metrics, current content identity, and local audit excerpts.
|
|
||||||
|
|
||||||
Distribution is via Apple Developer Enterprise Program or TestFlight plus FC.Distribution / UpdateCenter publishing once Apple credentials are available.
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
|
||||||
kind: Kustomization
|
|
||||||
|
|
||||||
resources:
|
|
||||||
- manifest.yaml
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
# Apple TV signage is a sealed tvOS appliance. This ArgoCD app intentionally
|
|
||||||
# carries documentation metadata only; no Deployment, Service, or Pod resources
|
|
||||||
# are created for the player.
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: ConfigMap
|
|
||||||
metadata:
|
|
||||||
name: fc-signage-appletv-docs
|
|
||||||
namespace: fc-signage
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: fc-signage-appletv
|
|
||||||
app.kubernetes.io/part-of: flowercore-signage
|
|
||||||
flowercore.io/manifest-kind: docs-only
|
|
||||||
data:
|
|
||||||
README: |
|
|
||||||
FlowerCore.Signage.Agent.AppleTv is distributed through Apple Developer
|
|
||||||
Enterprise Program or TestFlight, not Kubernetes.
|
|
||||||
|
|
||||||
The app connects outbound to FC.Signage.Web:
|
|
||||||
- SignalR: https://signage.iamworkin.lan/hub/signage
|
|
||||||
- Polling fallback: GET /api/v1/nodes/{nodeId}/state
|
|
||||||
- Enrollment: POST /api/v1/nodes/{nodeId}/enroll
|
|
||||||
- Heartbeat: POST /api/v1/nodes/{nodeId}/heartbeat
|
|
||||||
|
|
||||||
This placeholder gives ArgoCD and inventory dashboards a first-class
|
|
||||||
Apple TV signage app entry without creating runtime pods.
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
# FlowerCore Signage Pi Player
|
|
||||||
|
|
||||||
Phase 1 Raspberry Pi signage player packaging for Chromium kiosk deployments.
|
|
||||||
This bundle is intentionally air-gap friendly: systemd units, shell scripts,
|
|
||||||
udev rules, and Chromium managed policy are all checked into the repo and are
|
|
||||||
installed by `FlowerCore.Puppet`.
|
|
||||||
|
|
||||||
## Scope
|
|
||||||
|
|
||||||
- Bootstrap a stable node identity and mTLS client certificate.
|
|
||||||
- Launch Chromium in kiosk mode against `FC.Signage.Web` player routes.
|
|
||||||
- Restart the kiosk on HDMI hotplug.
|
|
||||||
- Renew mTLS certificates daily when fewer than 30 days remain.
|
|
||||||
- Detect display capabilities at boot, daily, and on HDMI hotplug.
|
|
||||||
|
|
||||||
Phase 2 native Avalonia rendering is documented separately in Notes and remains
|
|
||||||
deferred.
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"AutofillAddressEnabled": false,
|
|
||||||
"AutofillCreditCardEnabled": false,
|
|
||||||
"PasswordManagerEnabled": false,
|
|
||||||
"BrowserSignin": 0,
|
|
||||||
"MetricsReportingEnabled": false,
|
|
||||||
"SafeBrowsingProtectionLevel": 0,
|
|
||||||
"DefaultNotificationsSetting": 2,
|
|
||||||
"DefaultPopupsSetting": 2,
|
|
||||||
"BackgroundModeEnabled": false,
|
|
||||||
"DefaultBrowserSettingEnabled": false,
|
|
||||||
"PromotionalTabsEnabled": false,
|
|
||||||
"CommandLineFlagSecurityWarningsEnabled": false,
|
|
||||||
"ExtensionInstallBlocklist": ["*"]
|
|
||||||
}
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
NODE_JSON="/etc/flowercore/signage-node.json"
|
|
||||||
CERT_DIR="/etc/fc-signage-player"
|
|
||||||
SIGNAGE_URL="${FC_SIGNAGE_URL:-https://signage.iamworkin.lan}"
|
|
||||||
NODE_ID=$(jq -r '.nodeId' "$NODE_JSON")
|
|
||||||
|
|
||||||
CONNECTORS=()
|
|
||||||
for dir in /sys/class/drm/card*-HDMI-A-*; do
|
|
||||||
[[ -e "$dir/status" ]] || continue
|
|
||||||
if [[ "$(cat "$dir/status")" == "connected" ]]; then
|
|
||||||
CONNECTORS+=("$(basename "$dir")")
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if [[ ${#CONNECTORS[@]} -eq 0 ]]; then
|
|
||||||
CAPABILITIES_JSON=$(jq -n --arg id "$NODE_ID" '{
|
|
||||||
nodeId: $id,
|
|
||||||
platform: "linux-arm64-pi",
|
|
||||||
displayConnected: false,
|
|
||||||
detectedAt: (now | todate),
|
|
||||||
note: "No HDMI display detected"
|
|
||||||
}')
|
|
||||||
else
|
|
||||||
PRIMARY="${CONNECTORS[0]}"
|
|
||||||
EDID_PATH="/sys/class/drm/${PRIMARY}/edid"
|
|
||||||
WIDTH=0
|
|
||||||
HEIGHT=0
|
|
||||||
REFRESH=60
|
|
||||||
HDR=false
|
|
||||||
AUDIO_HDMI=false
|
|
||||||
MFG=""
|
|
||||||
MODEL=""
|
|
||||||
PHYSICAL_SIZE=null
|
|
||||||
|
|
||||||
if [[ -s "$EDID_PATH" ]] && command -v edid-decode >/dev/null 2>&1; then
|
|
||||||
EDID_INFO=$(edid-decode < "$EDID_PATH" 2>/dev/null || true)
|
|
||||||
MFG=$(echo "$EDID_INFO" | grep -m1 -oP 'Manufacturer:\s*\K\S+' || true)
|
|
||||||
MODEL=$(echo "$EDID_INFO" | grep -m1 -oP 'Model:\s*\K\S+' || true)
|
|
||||||
PREF=$(echo "$EDID_INFO" | grep -m1 -oP '\d+x\d+\s*@\s*\d+(?:\.\d+)?\s*Hz' || true)
|
|
||||||
if [[ -n "$PREF" ]]; then
|
|
||||||
WIDTH=$(echo "$PREF" | grep -oP '^\d+')
|
|
||||||
HEIGHT=$(echo "$PREF" | grep -oP 'x\K\d+')
|
|
||||||
REFRESH=$(echo "$PREF" | grep -oP '@\s*\K[\d.]+' | cut -d. -f1)
|
|
||||||
fi
|
|
||||||
if echo "$EDID_INFO" | grep -qiE 'HDR (Static|Dynamic) Metadata Block'; then HDR=true; fi
|
|
||||||
if echo "$EDID_INFO" | grep -qiE 'CEA Audio Block|Audio Format Descriptor'; then AUDIO_HDMI=true; fi
|
|
||||||
PH_W=$(echo "$EDID_INFO" | grep -m1 -oP 'Maximum image size:\s*\K\d+\s*cm\s*x\s*\d+' || true)
|
|
||||||
if [[ -n "$PH_W" ]]; then
|
|
||||||
PH_CM_W=$(echo "$PH_W" | grep -oP '^\d+')
|
|
||||||
PH_CM_H=$(echo "$PH_W" | grep -oP 'x\s*\K\d+')
|
|
||||||
if (( PH_CM_W > 0 && PH_CM_H > 0 )); then
|
|
||||||
PHYSICAL_SIZE=$(awk -v w="$PH_CM_W" -v h="$PH_CM_H" 'BEGIN { printf "%.1f", sqrt(w*w + h*h)/2.54 }')
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$WIDTH" == "0" ]] && command -v kmsprint >/dev/null 2>&1; then
|
|
||||||
KMS=$(kmsprint 2>/dev/null | grep -A2 "$PRIMARY" | grep -oP '\d+x\d+' | head -1 || true)
|
|
||||||
if [[ -n "$KMS" ]]; then
|
|
||||||
WIDTH=$(echo "$KMS" | grep -oP '^\d+')
|
|
||||||
HEIGHT=$(echo "$KMS" | grep -oP 'x\K\d+')
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
AUDIO_ALSA=false
|
|
||||||
if aplay -l 2>/dev/null | grep -qi 'card.*HDMI'; then AUDIO_ALSA=true; fi
|
|
||||||
HAS_AUDIO=false
|
|
||||||
if [[ "$AUDIO_HDMI" == "true" && "$AUDIO_ALSA" == "true" ]]; then HAS_AUDIO=true; fi
|
|
||||||
|
|
||||||
CAPABILITIES_JSON=$(jq -n \
|
|
||||||
--arg id "$NODE_ID" \
|
|
||||||
--argjson w "$WIDTH" \
|
|
||||||
--argjson h "$HEIGHT" \
|
|
||||||
--argjson r "$REFRESH" \
|
|
||||||
--argjson hdr "$HDR" \
|
|
||||||
--argjson audio "$HAS_AUDIO" \
|
|
||||||
--arg connector "$PRIMARY" \
|
|
||||||
--arg mfg "$MFG" \
|
|
||||||
--arg model "$MODEL" \
|
|
||||||
--argjson size "$PHYSICAL_SIZE" \
|
|
||||||
'{
|
|
||||||
nodeId: $id,
|
|
||||||
platform: "linux-arm64-pi",
|
|
||||||
displayConnected: true,
|
|
||||||
detectedAt: (now | todate),
|
|
||||||
hardware: {
|
|
||||||
maxResolution: { width: $w, height: $h },
|
|
||||||
nativeResolution: { width: $w, height: $h },
|
|
||||||
refreshRateHz: $r,
|
|
||||||
colorDepth: ($hdr | if . then "Color30Hdr" else "Color24" end),
|
|
||||||
hasAudioOutput: $audio,
|
|
||||||
audioChannelCount: ($audio | if . then 2 else 0 end),
|
|
||||||
physicalSizeInches: $size,
|
|
||||||
connector: $connector,
|
|
||||||
manufacturer: $mfg,
|
|
||||||
modelName: $model
|
|
||||||
},
|
|
||||||
render: { codecs: ["h264", "vp9", "mp4"] }
|
|
||||||
}')
|
|
||||||
fi
|
|
||||||
|
|
||||||
ENDPOINT_CANDIDATES=(
|
|
||||||
"${SIGNAGE_URL}/api/v1/nodes/${NODE_ID}/capabilities"
|
|
||||||
"${SIGNAGE_URL}/api/v1/displays/${NODE_ID}/capability-profile"
|
|
||||||
)
|
|
||||||
|
|
||||||
SUCCESS=false
|
|
||||||
for url in "${ENDPOINT_CANDIDATES[@]}"; do
|
|
||||||
HTTP_STATUS=$(curl -sk -o /tmp/cap-response.json -w "%{http_code}" \
|
|
||||||
--max-time 10 \
|
|
||||||
--cert "$CERT_DIR/client.crt" --key "$CERT_DIR/client.key" \
|
|
||||||
-X POST "$url" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "$CAPABILITIES_JSON" || echo "000")
|
|
||||||
if [[ "$HTTP_STATUS" == "200" || "$HTTP_STATUS" == "201" || "$HTTP_STATUS" == "204" ]]; then
|
|
||||||
SUCCESS=true
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
mkdir -p /var/log/fc-signage-player
|
|
||||||
if [[ "$SUCCESS" != "true" ]]; then
|
|
||||||
echo "[$(date -Is)] capability declare: no endpoint accepted the profile; logging locally" \
|
|
||||||
| tee -a /var/log/fc-signage-player/capabilities.log
|
|
||||||
echo "$CAPABILITIES_JSON" | tee -a /var/log/fc-signage-player/capabilities.log
|
|
||||||
else
|
|
||||||
echo "[$(date -Is)] capability declare: ok ($url)" | tee -a /var/log/fc-signage-player/capabilities.log
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "$CAPABILITIES_JSON"
|
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
NODE_JSON="/etc/flowercore/signage-node.json"
|
|
||||||
CERT_DIR="/etc/fc-signage-player"
|
|
||||||
SIGNAGE_URL="${FC_SIGNAGE_URL:-https://signage.iamworkin.lan}"
|
|
||||||
SETUP_CODE_FILE="/etc/flowercore/signage-setup-code"
|
|
||||||
|
|
||||||
mkdir -p /etc/flowercore "$CERT_DIR" /var/log/fc-signage-player
|
|
||||||
chown fc-signage:fc-signage /etc/flowercore "$CERT_DIR" /var/log/fc-signage-player
|
|
||||||
chmod 0750 "$CERT_DIR"
|
|
||||||
|
|
||||||
if [[ -s "$NODE_JSON" && -s "$CERT_DIR/client.p12" ]]; then
|
|
||||||
ENROLLED=$(jq -r '.enrolledAt // empty' "$NODE_JSON")
|
|
||||||
if [[ -n "$ENROLLED" ]]; then
|
|
||||||
echo "[$(date -Is)] bootstrap: already enrolled at $ENROLLED; skipping"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -s "$NODE_JSON" ]]; then
|
|
||||||
NODE_UUID=$(jq -r '.nodeUuid // empty' "$NODE_JSON")
|
|
||||||
MACHINE_ID=$(jq -r '.machineId // empty' "$NODE_JSON")
|
|
||||||
else
|
|
||||||
NODE_UUID=$(uuidgen)
|
|
||||||
MACHINE_ID=$(echo "$NODE_UUID" | tr -d '-' | cut -c1-16)
|
|
||||||
jq -n --arg uuid "$NODE_UUID" --arg machine "$MACHINE_ID" --arg host "$(hostname -f)" --arg ts "$(date -Is)" \
|
|
||||||
'{nodeUuid: $uuid, machineId: $machine, hostname: $host, platform: "linux-arm64-pi", createdAt: $ts}' \
|
|
||||||
> "$NODE_JSON"
|
|
||||||
chmod 0640 "$NODE_JSON"
|
|
||||||
chown fc-signage:fc-signage "$NODE_JSON"
|
|
||||||
fi
|
|
||||||
|
|
||||||
SETUP_CODE=""
|
|
||||||
if [[ -s "$SETUP_CODE_FILE" ]]; then
|
|
||||||
SETUP_CODE=$(tr -d '\r\n\t ' < "$SETUP_CODE_FILE")
|
|
||||||
fi
|
|
||||||
|
|
||||||
MODEL=$(tr -d '\0' < /sys/firmware/devicetree/base/model 2>/dev/null || echo Unknown)
|
|
||||||
REG_PAYLOAD=$(jq -n \
|
|
||||||
--arg machine "$MACHINE_ID" \
|
|
||||||
--arg name "$(hostname -f)" \
|
|
||||||
--arg setup "$SETUP_CODE" \
|
|
||||||
--arg resolution "1920x1080" \
|
|
||||||
--arg model "$MODEL" \
|
|
||||||
'{
|
|
||||||
machineId: $machine,
|
|
||||||
name: $name,
|
|
||||||
setupCode: ($setup | if . == "" then null else . end),
|
|
||||||
resolution: $resolution,
|
|
||||||
hardwareModel: $model,
|
|
||||||
platform: "linux-arm64-pi"
|
|
||||||
}')
|
|
||||||
|
|
||||||
for attempt in 1 2; do
|
|
||||||
HTTP_STATUS=$(curl -sk -o /tmp/register-response.json -w "%{http_code}" \
|
|
||||||
--max-time 15 \
|
|
||||||
-X POST "${SIGNAGE_URL}/api/v1/nodes/register" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "$REG_PAYLOAD" || echo "000")
|
|
||||||
if [[ "$HTTP_STATUS" == "200" || "$HTTP_STATUS" == "201" ]]; then
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
echo "[$(date -Is)] bootstrap: register attempt $attempt returned $HTTP_STATUS" >&2
|
|
||||||
sleep 5
|
|
||||||
done
|
|
||||||
|
|
||||||
if [[ "$HTTP_STATUS" != "200" && "$HTTP_STATUS" != "201" ]]; then
|
|
||||||
echo "[$(date -Is)] bootstrap: register failed after 2 attempts" >&2
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
|
|
||||||
NODE_ID=$(jq -r '.nodeId // empty' /tmp/register-response.json)
|
|
||||||
if [[ -z "$NODE_ID" ]]; then
|
|
||||||
echo "[$(date -Is)] bootstrap: register response did not include nodeId" >&2
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
jq --arg id "$NODE_ID" '.nodeId = $id' "$NODE_JSON" > "${NODE_JSON}.tmp" && mv "${NODE_JSON}.tmp" "$NODE_JSON"
|
|
||||||
|
|
||||||
if [[ -s "$SETUP_CODE_FILE" ]]; then
|
|
||||||
curl -sk -X POST "${SIGNAGE_URL}/api/v1/nodes/${NODE_ID}/approve-via-setup-code" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "{\"setupCode\":\"${SETUP_CODE}\"}" \
|
|
||||||
-o /dev/null || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
STATUS=""
|
|
||||||
DEADLINE=$(( $(date +%s) + 1800 ))
|
|
||||||
while (( $(date +%s) < DEADLINE )); do
|
|
||||||
STATUS=$(curl -sk --max-time 5 "${SIGNAGE_URL}/api/v1/nodes/${NODE_ID}/status" | jq -r '.status // empty')
|
|
||||||
if [[ "$STATUS" == "Approved" || "$STATUS" == "Enrolled" || "$STATUS" == "Online" ]]; then
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
sleep 15
|
|
||||||
done
|
|
||||||
|
|
||||||
if [[ "$STATUS" != "Approved" && "$STATUS" != "Enrolled" && "$STATUS" != "Online" ]]; then
|
|
||||||
echo "[$(date -Is)] bootstrap: approval not granted within 30min budget" >&2
|
|
||||||
exit 3
|
|
||||||
fi
|
|
||||||
|
|
||||||
KEY_PATH="${CERT_DIR}/client.key"
|
|
||||||
CSR_PATH="${CERT_DIR}/client.csr"
|
|
||||||
openssl ecparam -genkey -name prime256v1 -out "$KEY_PATH"
|
|
||||||
openssl req -new -key "$KEY_PATH" -out "$CSR_PATH" \
|
|
||||||
-subj "/CN=${NODE_ID}/O=FlowerCore/OU=SignagePlayer-Pi"
|
|
||||||
|
|
||||||
ENROLL_PAYLOAD=$(jq -n --arg csr "$(cat "$CSR_PATH")" '{certificateSigningRequest: $csr}')
|
|
||||||
HTTP_STATUS=$(curl -sk -o /tmp/enroll-response.json -w "%{http_code}" \
|
|
||||||
--max-time 15 \
|
|
||||||
-X POST "${SIGNAGE_URL}/api/v1/nodes/${NODE_ID}/enroll" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "$ENROLL_PAYLOAD")
|
|
||||||
|
|
||||||
if [[ "$HTTP_STATUS" != "200" && "$HTTP_STATUS" != "201" ]]; then
|
|
||||||
echo "[$(date -Is)] bootstrap: enroll failed with HTTP $HTTP_STATUS" >&2
|
|
||||||
exit 4
|
|
||||||
fi
|
|
||||||
|
|
||||||
jq -r '.clientCertificatePem // .signedCertificatePem' /tmp/enroll-response.json > "${CERT_DIR}/client.crt"
|
|
||||||
jq -r '.caCertificatePem' /tmp/enroll-response.json > "${CERT_DIR}/ca-chain.pem"
|
|
||||||
P12_PASS=$(openssl rand -hex 24)
|
|
||||||
echo -n "$P12_PASS" > "${CERT_DIR}/client.p12.pass"
|
|
||||||
chmod 0600 "${CERT_DIR}/client.p12.pass"
|
|
||||||
|
|
||||||
openssl pkcs12 -export \
|
|
||||||
-inkey "$KEY_PATH" \
|
|
||||||
-in "${CERT_DIR}/client.crt" \
|
|
||||||
-certfile "${CERT_DIR}/ca-chain.pem" \
|
|
||||||
-out "${CERT_DIR}/client.p12" \
|
|
||||||
-password "pass:${P12_PASS}"
|
|
||||||
|
|
||||||
chown fc-signage:fc-signage "${CERT_DIR}"/* "$NODE_JSON"
|
|
||||||
chmod 0640 "${CERT_DIR}/client.p12" "${CERT_DIR}/client.crt" "${CERT_DIR}/ca-chain.pem" "$KEY_PATH"
|
|
||||||
chmod 0600 "${CERT_DIR}/client.p12.pass"
|
|
||||||
|
|
||||||
EXPIRY=$(openssl x509 -in "${CERT_DIR}/client.crt" -enddate -noout | sed 's/notAfter=//')
|
|
||||||
jq --arg ts "$(date -Is)" --arg exp "$EXPIRY" \
|
|
||||||
'.enrolledAt = $ts | .certExpiry = $exp' "$NODE_JSON" > "${NODE_JSON}.tmp" \
|
|
||||||
&& mv "${NODE_JSON}.tmp" "$NODE_JSON"
|
|
||||||
|
|
||||||
systemctl start flowercore-signage-detect-display.service || true
|
|
||||||
systemctl start flowercore-signage-player-pi.service || true
|
|
||||||
echo "[$(date -Is)] bootstrap: enrolled and kiosk started (NodeId=${NODE_ID})"
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
sleep 2
|
|
||||||
systemctl start flowercore-signage-detect-display.service || true
|
|
||||||
systemctl restart flowercore-signage-player-pi.service
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
NODE_JSON="/etc/flowercore/signage-node.json"
|
|
||||||
NODE_ID=$(jq -r '.nodeId' "$NODE_JSON")
|
|
||||||
SIGNAGE_URL="${FC_SIGNAGE_URL:-https://signage.iamworkin.lan}"
|
|
||||||
CERT_DIR="/etc/fc-signage-player"
|
|
||||||
|
|
||||||
CERT_THUMB=$(openssl pkcs12 -in "$CERT_DIR/client.p12" -passin file:"$CERT_DIR/client.p12.pass" -nodes -nokeys 2>/dev/null \
|
|
||||||
| openssl x509 -fingerprint -sha256 -noout \
|
|
||||||
| sed 's/.*=//' \
|
|
||||||
| tr -d ':')
|
|
||||||
|
|
||||||
PLAYER_URL="${SIGNAGE_URL}/player/${NODE_ID}/embed?token=${CERT_THUMB}"
|
|
||||||
HTTP_STATUS=$(curl -sk -o /dev/null -w "%{http_code}" --max-time 5 \
|
|
||||||
--cert-type P12 --cert "$CERT_DIR/client.p12:$(cat "$CERT_DIR/client.p12.pass")" \
|
|
||||||
"$PLAYER_URL" || echo "000")
|
|
||||||
|
|
||||||
mkdir -p /var/log/fc-signage-player
|
|
||||||
if [[ "$HTTP_STATUS" != "200" && "$HTTP_STATUS" != "301" && "$HTTP_STATUS" != "302" ]]; then
|
|
||||||
echo "[$(date -Is)] /embed returned $HTTP_STATUS; falling back to /player/${NODE_ID}" \
|
|
||||||
>> /var/log/fc-signage-player/url-divergence.log
|
|
||||||
PLAYER_URL="${SIGNAGE_URL}/player/${NODE_ID}?token=${CERT_THUMB}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
exec chromium-browser \
|
|
||||||
--kiosk \
|
|
||||||
--noerrdialogs \
|
|
||||||
--disable-infobars \
|
|
||||||
--disable-translate \
|
|
||||||
--disable-features=TranslateUI,InfiniteSessionRestore \
|
|
||||||
--autoplay-policy=no-user-gesture-required \
|
|
||||||
--password-store=basic \
|
|
||||||
--user-data-dir=/var/lib/fc-signage-player/profile \
|
|
||||||
--disk-cache-dir=/var/lib/fc-signage-player/cache \
|
|
||||||
--disk-cache-size=104857600 \
|
|
||||||
--no-first-run \
|
|
||||||
--no-default-browser-check \
|
|
||||||
--check-for-update-interval=2592000 \
|
|
||||||
--enable-features=OverlayScrollbar \
|
|
||||||
--start-fullscreen \
|
|
||||||
--window-position=0,0 \
|
|
||||||
--window-size=1920,1080 \
|
|
||||||
"$PLAYER_URL"
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
mkdir -p /var/log/fc-signage-player
|
|
||||||
|
|
||||||
for f in /etc/flowercore/signage-node.json /etc/fc-signage-player/client.p12 /etc/fc-signage-player/client.p12.pass; do
|
|
||||||
if [[ ! -r "$f" ]]; then
|
|
||||||
echo "[$(date -Is)] prelaunch: missing or unreadable $f" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if openssl pkcs12 -in /etc/fc-signage-player/client.p12 -passin file:/etc/fc-signage-player/client.p12.pass -nokeys -clcerts 2>/dev/null \
|
|
||||||
| openssl x509 -checkend $((7*24*3600)) -noout; then
|
|
||||||
:
|
|
||||||
else
|
|
||||||
echo "[$(date -Is)] prelaunch: client cert expires within 7 days" >&2
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "[$(date -Is)] prelaunch: ok" | tee -a /var/log/fc-signage-player/prelaunch.log
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
CERT_DIR="/etc/fc-signage-player"
|
|
||||||
NODE_JSON="/etc/flowercore/signage-node.json"
|
|
||||||
SIGNAGE_URL="${FC_SIGNAGE_URL:-https://signage.iamworkin.lan}"
|
|
||||||
|
|
||||||
[[ -s "$CERT_DIR/client.crt" ]] || { echo "no cert to renew"; exit 0; }
|
|
||||||
|
|
||||||
if openssl x509 -in "$CERT_DIR/client.crt" -checkend $((30*24*3600)) -noout; then
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
NODE_ID=$(jq -r '.nodeId' "$NODE_JSON")
|
|
||||||
NEW_KEY="$CERT_DIR/client.key.new"
|
|
||||||
NEW_CSR="$CERT_DIR/client.csr.new"
|
|
||||||
|
|
||||||
openssl ecparam -genkey -name prime256v1 -out "$NEW_KEY"
|
|
||||||
openssl req -new -key "$NEW_KEY" -out "$NEW_CSR" \
|
|
||||||
-subj "/CN=${NODE_ID}/O=FlowerCore/OU=SignagePlayer-Pi"
|
|
||||||
|
|
||||||
HTTP_STATUS=$(curl -sk -o /tmp/renew-response.json -w "%{http_code}" \
|
|
||||||
--cert "$CERT_DIR/client.crt" --key "$CERT_DIR/client.key" \
|
|
||||||
-X POST "${SIGNAGE_URL}/api/v1/nodes/${NODE_ID}/renew" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "$(jq -n --arg csr "$(cat "$NEW_CSR")" '{certificateSigningRequest: $csr}')")
|
|
||||||
|
|
||||||
if [[ "$HTTP_STATUS" != "200" && "$HTTP_STATUS" != "201" ]]; then
|
|
||||||
echo "[$(date -Is)] renew: failed HTTP $HTTP_STATUS; leaving old cert in place" >&2
|
|
||||||
exit 5
|
|
||||||
fi
|
|
||||||
|
|
||||||
jq -r '.clientCertificatePem // .signedCertificatePem' /tmp/renew-response.json > "$CERT_DIR/client.crt.new"
|
|
||||||
jq -r '.caCertificatePem' /tmp/renew-response.json > "$CERT_DIR/ca-chain.pem.new"
|
|
||||||
P12_PASS=$(cat "$CERT_DIR/client.p12.pass")
|
|
||||||
openssl pkcs12 -export -inkey "$NEW_KEY" -in "$CERT_DIR/client.crt.new" \
|
|
||||||
-certfile "$CERT_DIR/ca-chain.pem.new" \
|
|
||||||
-out "$CERT_DIR/client.p12.new" -password "pass:${P12_PASS}"
|
|
||||||
|
|
||||||
mv "$CERT_DIR/client.key.new" "$CERT_DIR/client.key"
|
|
||||||
mv "$CERT_DIR/client.crt.new" "$CERT_DIR/client.crt"
|
|
||||||
mv "$CERT_DIR/ca-chain.pem.new" "$CERT_DIR/ca-chain.pem"
|
|
||||||
mv "$CERT_DIR/client.p12.new" "$CERT_DIR/client.p12"
|
|
||||||
|
|
||||||
chown fc-signage:fc-signage "$CERT_DIR"/client.*
|
|
||||||
systemctl restart flowercore-signage-player-pi.service
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
# Settle DRM for 2s before restarting Chromium, then redeclare capabilities.
|
|
||||||
SUBSYSTEM=="drm", KERNEL=="card?-HDMI-A-?", ACTION=="change", RUN+="/usr/bin/systemctl start flowercore-signage-player-pi-hdmi.service"
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=FlowerCore Signage Pi: first-boot identity + mTLS enrollment
|
|
||||||
Wants=network-online.target
|
|
||||||
After=network-online.target
|
|
||||||
Before=flowercore-signage-player-pi.service
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=oneshot
|
|
||||||
ExecStart=/usr/local/bin/flowercore-signage-bootstrap.sh
|
|
||||||
RemainAfterExit=yes
|
|
||||||
StandardOutput=journal
|
|
||||||
StandardError=journal
|
|
||||||
TimeoutStartSec=2100
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=FlowerCore Signage Pi: detect connected display + declare capabilities
|
|
||||||
After=flowercore-signage-bootstrap.service
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=oneshot
|
|
||||||
User=fc-signage
|
|
||||||
ExecStart=/usr/local/bin/fc-signage-detect-display
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=Daily FlowerCore Signage Pi display capability redeclaration
|
|
||||||
|
|
||||||
[Timer]
|
|
||||||
OnCalendar=daily
|
|
||||||
RandomizedDelaySec=1h
|
|
||||||
Persistent=true
|
|
||||||
OnBootSec=30s
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=timers.target
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=FlowerCore Signage Pi Player HDMI hotplug responder
|
|
||||||
DefaultDependencies=no
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=oneshot
|
|
||||||
ExecStart=/usr/local/bin/flowercore-signage-hdmi-respond.sh
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=FlowerCore Digital Signage Pi Player (Chromium kiosk)
|
|
||||||
Documentation=https://github.com/astoltz/FlowerCore.Notes/blob/master/docs/standards/appletv-pi-signage-agents-design.md
|
|
||||||
Wants=network-online.target
|
|
||||||
After=network-online.target graphical.target
|
|
||||||
ConditionPathExists=/etc/flowercore/signage-node.json
|
|
||||||
ConditionPathExists=/etc/fc-signage-player/client.p12
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
User=fc-signage
|
|
||||||
Group=fc-signage
|
|
||||||
WorkingDirectory=/var/lib/fc-signage-player
|
|
||||||
EnvironmentFile=-/etc/flowercore/signage-player.env
|
|
||||||
ExecStartPre=/usr/local/bin/flowercore-signage-prelaunch.sh
|
|
||||||
ExecStart=/usr/local/bin/flowercore-signage-launch.sh
|
|
||||||
Restart=always
|
|
||||||
RestartSec=10s
|
|
||||||
StartLimitBurst=5
|
|
||||||
StartLimitIntervalSec=300s
|
|
||||||
MemoryMax=2G
|
|
||||||
MemoryHigh=1500M
|
|
||||||
ProtectSystem=strict
|
|
||||||
ProtectHome=true
|
|
||||||
ReadWritePaths=/var/lib/fc-signage-player /var/log/fc-signage-player
|
|
||||||
PrivateTmp=true
|
|
||||||
NoNewPrivileges=true
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=graphical.target
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=FlowerCore Signage Pi: cert renewal worker
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=oneshot
|
|
||||||
ExecStart=/usr/local/bin/flowercore-signage-renew-cert.sh
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=Daily check for FlowerCore Signage Pi cert renewal
|
|
||||||
|
|
||||||
[Timer]
|
|
||||||
OnCalendar=daily
|
|
||||||
RandomizedDelaySec=2h
|
|
||||||
Persistent=true
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=timers.target
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
#!/usr/bin/env bats
|
|
||||||
|
|
||||||
setup() {
|
|
||||||
APP_ROOT="$(cd "$BATS_TEST_DIRNAME/.." && pwd)"
|
|
||||||
DETECT="$APP_ROOT/scripts/fc-signage-detect-display"
|
|
||||||
}
|
|
||||||
|
|
||||||
@test "display detection emits graceful disconnected profile when no hdmi connector is present" {
|
|
||||||
script="$(cat "$DETECT")"
|
|
||||||
[[ "$script" == *"displayConnected: false"* ]]
|
|
||||||
[[ "$script" == *"No HDMI display detected"* ]]
|
|
||||||
}
|
|
||||||
|
|
||||||
@test "display detection parses edid, falls back to kmsprint, and logs endpoint failures locally" {
|
|
||||||
script="$(cat "$DETECT")"
|
|
||||||
[[ "$script" == *"edid-decode"* ]]
|
|
||||||
[[ "$script" == *"HDR (Static|Dynamic) Metadata Block"* ]]
|
|
||||||
[[ "$script" == *"kmsprint"* ]]
|
|
||||||
[[ "$script" == *"/api/v1/nodes/\${NODE_ID}/capabilities"* ]]
|
|
||||||
[[ "$script" == *"/api/v1/displays/\${NODE_ID}/capability-profile"* ]]
|
|
||||||
[[ "$script" == *"capabilities.log"* ]]
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
#!/usr/bin/env bats
|
|
||||||
|
|
||||||
setup() {
|
|
||||||
APP_ROOT="$(cd "$BATS_TEST_DIRNAME/.." && pwd)"
|
|
||||||
BOOTSTRAP="$APP_ROOT/scripts/flowercore-signage-bootstrap.sh"
|
|
||||||
RENEW="$APP_ROOT/scripts/flowercore-signage-renew-cert.sh"
|
|
||||||
}
|
|
||||||
|
|
||||||
@test "bootstrap is idempotent when node is already enrolled" {
|
|
||||||
script="$(cat "$BOOTSTRAP")"
|
|
||||||
[[ "$script" == *'[[ -s "$NODE_JSON" && -s "$CERT_DIR/client.p12" ]]'* ]]
|
|
||||||
[[ "$script" == *"already enrolled"* ]]
|
|
||||||
[[ "$script" == *"exit 0"* ]]
|
|
||||||
}
|
|
||||||
|
|
||||||
@test "bootstrap generates a stable node uuid and machine id" {
|
|
||||||
script="$(cat "$BOOTSTRAP")"
|
|
||||||
[[ "$script" == *"uuidgen"* ]]
|
|
||||||
[[ "$script" == *"nodeUuid"* ]]
|
|
||||||
[[ "$script" == *"machineId"* ]]
|
|
||||||
[[ "$script" == *"cut -c1-16"* ]]
|
|
||||||
}
|
|
||||||
|
|
||||||
@test "bootstrap posts to the canonical register endpoint" {
|
|
||||||
grep -q '/api/v1/nodes/register' "$BOOTSTRAP"
|
|
||||||
grep -q '"linux-arm64-pi"' "$BOOTSTRAP"
|
|
||||||
}
|
|
||||||
|
|
||||||
@test "bootstrap retries registration once for first-call races" {
|
|
||||||
script="$(cat "$BOOTSTRAP")"
|
|
||||||
[[ "$script" == *"for attempt in 1 2"* ]]
|
|
||||||
[[ "$script" == *"register attempt \$attempt returned"* ]]
|
|
||||||
[[ "$script" == *"sleep 5"* ]]
|
|
||||||
}
|
|
||||||
|
|
||||||
@test "bootstrap supports setup-code approval with manual polling fallback" {
|
|
||||||
script="$(cat "$BOOTSTRAP")"
|
|
||||||
[[ "$script" == *"signage-setup-code"* ]]
|
|
||||||
[[ "$script" == *"approve-via-setup-code"* ]]
|
|
||||||
[[ "$script" == *"+ 1800"* ]]
|
|
||||||
[[ "$script" == *"sleep 15"* ]]
|
|
||||||
}
|
|
||||||
|
|
||||||
@test "bootstrap generates an ecdsa p256 csr for the signage pi subject" {
|
|
||||||
script="$(cat "$BOOTSTRAP")"
|
|
||||||
[[ "$script" == *"ecparam -genkey -name prime256v1"* ]]
|
|
||||||
[[ "$script" == *'/CN=${NODE_ID}/O=FlowerCore/OU=SignagePlayer-Pi'* ]]
|
|
||||||
}
|
|
||||||
|
|
||||||
@test "bootstrap writes pkcs12 bundle with restrictive permissions" {
|
|
||||||
script="$(cat "$BOOTSTRAP")"
|
|
||||||
[[ "$script" == *"openssl pkcs12 -export"* ]]
|
|
||||||
[[ "$script" == *"client.p12.pass"* ]]
|
|
||||||
[[ "$script" == *"chmod 0640"* ]]
|
|
||||||
[[ "$script" == *"chmod 0600"* ]]
|
|
||||||
}
|
|
||||||
|
|
||||||
@test "renewal only calls renew endpoint inside the thirty-day window and swaps atomically" {
|
|
||||||
script="$(cat "$RENEW")"
|
|
||||||
[[ "$script" == *'-checkend $((30*24*3600))'* ]]
|
|
||||||
[[ "$script" == *"/api/v1/nodes/\${NODE_ID}/renew"* ]]
|
|
||||||
[[ "$script" == *"client.key.new"* ]]
|
|
||||||
[[ "$script" == *'mv "$CERT_DIR/client.p12.new" "$CERT_DIR/client.p12"'* ]]
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
#!/usr/bin/env bats
|
|
||||||
|
|
||||||
setup() {
|
|
||||||
APP_ROOT="$(cd "$BATS_TEST_DIRNAME/.." && pwd)"
|
|
||||||
}
|
|
||||||
|
|
||||||
@test "player unit exists" {
|
|
||||||
[ -f "$APP_ROOT/systemd/flowercore-signage-player-pi.service" ]
|
|
||||||
}
|
|
||||||
|
|
||||||
@test "player unit uses simple chromium service with restart backoff" {
|
|
||||||
unit="$(cat "$APP_ROOT/systemd/flowercore-signage-player-pi.service")"
|
|
||||||
[[ "$unit" == *"Type=simple"* ]]
|
|
||||||
[[ "$unit" == *"Restart=always"* ]]
|
|
||||||
[[ "$unit" == *"RestartSec=10s"* ]]
|
|
||||||
[[ "$unit" == *"StartLimitBurst=5"* ]]
|
|
||||||
[[ "$unit" == *"StartLimitIntervalSec=300s"* ]]
|
|
||||||
}
|
|
||||||
|
|
||||||
@test "player unit caps chromium memory at two gigabytes" {
|
|
||||||
grep -q '^MemoryMax=2G$' "$APP_ROOT/systemd/flowercore-signage-player-pi.service"
|
|
||||||
grep -q '^MemoryHigh=1500M$' "$APP_ROOT/systemd/flowercore-signage-player-pi.service"
|
|
||||||
}
|
|
||||||
|
|
||||||
@test "player unit condition-gates startup on identity and p12 certificate" {
|
|
||||||
grep -q '^ConditionPathExists=/etc/flowercore/signage-node.json$' "$APP_ROOT/systemd/flowercore-signage-player-pi.service"
|
|
||||||
grep -q '^ConditionPathExists=/etc/fc-signage-player/client.p12$' "$APP_ROOT/systemd/flowercore-signage-player-pi.service"
|
|
||||||
}
|
|
||||||
|
|
||||||
@test "player unit runs prelaunch checks before chromium" {
|
|
||||||
grep -q '^ExecStartPre=/usr/local/bin/flowercore-signage-prelaunch.sh$' "$APP_ROOT/systemd/flowercore-signage-player-pi.service"
|
|
||||||
grep -q '^ExecStart=/usr/local/bin/flowercore-signage-launch.sh$' "$APP_ROOT/systemd/flowercore-signage-player-pi.service"
|
|
||||||
}
|
|
||||||
|
|
||||||
@test "hdmi udev rule routes through the two-second settle service" {
|
|
||||||
rule="$(cat "$APP_ROOT/systemd/99-flowercore-signage-hdmi.rules")"
|
|
||||||
[[ "$rule" == *'KERNEL=="card?-HDMI-A-?"'* ]]
|
|
||||||
[[ "$rule" == *"systemctl start flowercore-signage-player-pi-hdmi.service"* ]]
|
|
||||||
[[ "$rule" != *"systemctl restart flowercore-signage-player-pi.service"* ]]
|
|
||||||
}
|
|
||||||
|
|
||||||
@test "hdmi responder settles, declares display, then restarts chromium" {
|
|
||||||
responder="$(cat "$APP_ROOT/scripts/flowercore-signage-hdmi-respond.sh")"
|
|
||||||
[[ "$responder" == *"sleep 2"* ]]
|
|
||||||
[[ "$responder" == *"systemctl start flowercore-signage-detect-display.service"* ]]
|
|
||||||
[[ "$responder" == *"systemctl restart flowercore-signage-player-pi.service"* ]]
|
|
||||||
}
|
|
||||||
|
|
||||||
@test "chromium policy json is valid and disables credential prompts" {
|
|
||||||
command -v jq >/dev/null || skip "jq not installed"
|
|
||||||
jq -e '.AutofillAddressEnabled == false and .AutofillCreditCardEnabled == false and .PasswordManagerEnabled == false' \
|
|
||||||
"$APP_ROOT/chromium-policies/flowercore-signage.json" >/dev/null
|
|
||||||
}
|
|
||||||
|
|
||||||
@test "launch script tries embed URL and logs bare-player fallback" {
|
|
||||||
launch="$(cat "$APP_ROOT/scripts/flowercore-signage-launch.sh")"
|
|
||||||
[[ "$launch" == *'/player/${NODE_ID}/embed?token=${CERT_THUMB}'* ]]
|
|
||||||
[[ "$launch" == *"url-divergence.log"* ]]
|
|
||||||
[[ "$launch" == *'/player/${NODE_ID}?token=${CERT_THUMB}'* ]]
|
|
||||||
}
|
|
||||||
|
|
||||||
@test "prelaunch script validates required node and cert files" {
|
|
||||||
prelaunch="$(cat "$APP_ROOT/scripts/flowercore-signage-prelaunch.sh")"
|
|
||||||
[[ "$prelaunch" == *"/etc/flowercore/signage-node.json"* ]]
|
|
||||||
[[ "$prelaunch" == *"/etc/fc-signage-player/client.p12"* ]]
|
|
||||||
[[ "$prelaunch" == *"/etc/fc-signage-player/client.p12.pass"* ]]
|
|
||||||
[[ "$prelaunch" == *"exit 1"* ]]
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
# GitHub Runner Fleet
|
|
||||||
|
|
||||||
ArgoCD owns `apps/github-runner/github-runner.yaml`. Do not patch live runner
|
|
||||||
Deployments with `kubectl`; update this manifest and let ArgoCD reconcile.
|
|
||||||
|
|
||||||
## Runner Shape
|
|
||||||
|
|
||||||
All repo-scoped Linux runners use:
|
|
||||||
|
|
||||||
- `ACCESS_TOKEN` from the `github-runner-token` Secret
|
|
||||||
- `RUN_AS_ROOT=false`
|
|
||||||
- `EPHEMERAL=true`
|
|
||||||
- `LABELS=self-hosted,linux,fc-build-linux`
|
|
||||||
- writable non-root paths under `/home/runner` for .NET, NuGet, XDG cache, and
|
|
||||||
Actions tool cache
|
|
||||||
|
|
||||||
`github-runner` for `FlowerCore.Common` is single-replica because it retains the
|
|
||||||
original Longhorn ReadWriteOnce NuGet PVC. `github-runner-sharedpos` and the top
|
|
||||||
Linux-cost repo runners use two replicas with per-pod `emptyDir` caches. That is
|
|
||||||
the safe backlog-drain strategy: no two pods share one RWO PVC.
|
|
||||||
|
|
||||||
## Post-Merge Proof
|
|
||||||
|
|
||||||
After the PR is merged and ArgoCD syncs, verify the runner fleet:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
kubectl -n github-runner get deploy,pods,pvc
|
|
||||||
```
|
|
||||||
|
|
||||||
Verify GitHub registration for the repo-scoped runners:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
for repo in FlowerCore.Common FlowerCore.Shared.Pos FlowerCore.Puppet FlowerCore.Signage \
|
|
||||||
FlowerCore.DMS FlowerCore.Telephony FlowerCore.Print.Web FlowerCore.Chat \
|
|
||||||
FlowerCore.MySQL FlowerCore.Kiosk.Linux; do
|
|
||||||
echo "=== $repo ==="
|
|
||||||
gh api "/repos/astoltz/$repo/actions/runners" \
|
|
||||||
--jq '.runners[] | select(.labels[].name == "fc-build-linux") | {name,status,busy,labels:[.labels[].name]}'
|
|
||||||
done
|
|
||||||
```
|
|
||||||
|
|
||||||
Shared.Pos publish proof after the runner pod is online:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
gh run list --repo astoltz/FlowerCore.Shared.Pos \
|
|
||||||
--workflow "Build, Test & Publish" --branch main --limit 5
|
|
||||||
```
|
|
||||||
|
|
||||||
If the latest run is still queued after runner registration, rerun the workflow
|
|
||||||
from GitHub Actions and verify it lands on an `rke2-linux-*` runner.
|
|
||||||
|
|
||||||
## Failure Notes
|
|
||||||
|
|
||||||
- `actions/setup-dotnet` permission error at `/usr/share/dotnet`: check that
|
|
||||||
`DOTNET_INSTALL_DIR=/home/runner/.dotnet` and related cache env vars are
|
|
||||||
present on the runner pod.
|
|
||||||
- `404` during runner registration: the fine-grained PAT is valid but missing
|
|
||||||
repository access for that repo. Add the repo to the PAT access list; the PAT
|
|
||||||
value does not change.
|
|
||||||
- `Multi-Attach` volume error: only the Common runner uses a RWO PVC and it must
|
|
||||||
stay single-replica. New multi-replica runners use `emptyDir`.
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -75,20 +75,6 @@ data:
|
|||||||
cluster: "rke2"
|
cluster: "rke2"
|
||||||
role: "agent"
|
role: "agent"
|
||||||
|
|
||||||
# Mac mini macOS runner node (INFRA VLAN)
|
|
||||||
- job_name: "macmini-node"
|
|
||||||
scrape_timeout: 15s
|
|
||||||
static_configs:
|
|
||||||
- targets: ["10.0.56.115:9100"]
|
|
||||||
labels:
|
|
||||||
instance: "macmini"
|
|
||||||
host: "macmini.iamworkin.lan"
|
|
||||||
vlan: "infra"
|
|
||||||
arch: "arm64"
|
|
||||||
role: "macos-runner"
|
|
||||||
puppet_managed: "true"
|
|
||||||
puppet_server: "puppet.iamworkin.lan"
|
|
||||||
|
|
||||||
# In-cluster node-exporter DaemonSet
|
# In-cluster node-exporter DaemonSet
|
||||||
- job_name: "k8s-node-exporter"
|
- job_name: "k8s-node-exporter"
|
||||||
kubernetes_sd_configs:
|
kubernetes_sd_configs:
|
||||||
@@ -711,36 +697,6 @@ data:
|
|||||||
summary: "Print.Web Ollama runner held for >10m ({{ $labels.model }})"
|
summary: "Print.Web Ollama runner held for >10m ({{ $labels.model }})"
|
||||||
description: "Print.Web reports model {{ $labels.model }} with {{ $value | printf \"%.0f\" }}s of keep-alive remaining. Check concurrent requests before the Pi 5 Ollama lane thrashes."
|
description: "Print.Web reports model {{ $labels.model }} with {{ $value | printf \"%.0f\" }}s of keep-alive remaining. Check concurrent requests before the Pi 5 Ollama lane thrashes."
|
||||||
|
|
||||||
- name: macmini-runners
|
|
||||||
rules:
|
|
||||||
- alert: MacMiniRunnerOffline
|
|
||||||
expr: (flowercore_github_runner_online{runner=~"macmini-.*"} == 0) or absent(flowercore_github_runner_online{runner=~"macmini-.*"})
|
|
||||||
for: 10m
|
|
||||||
labels:
|
|
||||||
severity: warning
|
|
||||||
service: github-runner
|
|
||||||
annotations:
|
|
||||||
summary: "Mac mini GitHub runner offline ({{ $labels.runner }})"
|
|
||||||
description: "A macmini-* GitHub Actions runner has not reported online for more than 10 minutes. Puppet manages its LaunchDaemon under /Library/LaunchDaemons/io.flowercore.github-runner-<slug>.plist; runners survive reboot and do not require a GUI session."
|
|
||||||
|
|
||||||
- name: linux-runners
|
|
||||||
rules:
|
|
||||||
- alert: LinuxRunnerOffline
|
|
||||||
expr: |
|
|
||||||
kube_deployment_status_replicas_ready{
|
|
||||||
namespace="github-runner",
|
|
||||||
deployment=~"github-runner(|-(sharedpos|puppet|signage|dms|telephony|print-web|chat|mysql|kiosk-linux))"
|
|
||||||
} == 0
|
|
||||||
for: 5m
|
|
||||||
labels:
|
|
||||||
severity: warning
|
|
||||||
alert_channel: irc
|
|
||||||
service: github-runner
|
|
||||||
team: ci
|
|
||||||
annotations:
|
|
||||||
summary: "Linux CI runner offline: {{ $labels.deployment }}"
|
|
||||||
description: "Deployment {{ $labels.deployment }} in namespace github-runner has 0 ready replicas for more than 5 minutes. CI jobs targeting this repo will queue until the runner pod restarts and re-registers with GitHub. Check pods with: kubectl -n github-runner get pods -l app.kubernetes.io/name={{ $labels.deployment }}. Check logs with: kubectl -n github-runner logs -l app.kubernetes.io/name={{ $labels.deployment }} --tail=50. Common causes: PAT missing repo access, runner CrashLoopBackOff, or node/resource pressure."
|
|
||||||
|
|
||||||
- name: remote-desktop
|
- name: remote-desktop
|
||||||
rules:
|
rules:
|
||||||
- alert: RemoteDesktopWebDown
|
- alert: RemoteDesktopWebDown
|
||||||
@@ -3439,39 +3395,6 @@ data:
|
|||||||
relativeTimeRange: {from: 120, to: 0}
|
relativeTimeRange: {from: 120, to: 0}
|
||||||
datasourceUid: __expr__
|
datasourceUid: __expr__
|
||||||
model: {type: threshold, expression: B, conditions: [{evaluator: {params: [600], type: gt}}], refId: C}
|
model: {type: threshold, expression: B, conditions: [{evaluator: {params: [600], type: gt}}], refId: C}
|
||||||
- orgId: 1
|
|
||||||
name: CI Runners
|
|
||||||
folder: CI Alerts
|
|
||||||
interval: 1m
|
|
||||||
rules:
|
|
||||||
- uid: linux-runner-offline
|
|
||||||
title: LinuxRunnerOffline
|
|
||||||
condition: C
|
|
||||||
for: 5m
|
|
||||||
noDataState: OK
|
|
||||||
execErrState: Error
|
|
||||||
annotations:
|
|
||||||
summary: "Linux CI runner offline: {{ $labels.deployment }}"
|
|
||||||
description: "A github-runner namespace Deployment has 0 ready replicas for more than 5 minutes. CI jobs targeting that repo will queue until the runner pod restarts and re-registers."
|
|
||||||
runbook: "1. kubectl -n github-runner get pods -l app.kubernetes.io/name={{ $labels.deployment }} 2. kubectl -n github-runner logs -l app.kubernetes.io/name={{ $labels.deployment }} --tail=50 3. Verify PAT repo access if registration returns 404 4. Verify no RWO PVC is shared by scaled runners"
|
|
||||||
labels:
|
|
||||||
severity: warning
|
|
||||||
service: github-runner
|
|
||||||
alert_channel: irc
|
|
||||||
team: ci
|
|
||||||
data:
|
|
||||||
- refId: A
|
|
||||||
relativeTimeRange: {from: 300, to: 0}
|
|
||||||
datasourceUid: prometheus
|
|
||||||
model: {expr: 'kube_deployment_status_replicas_ready{namespace="github-runner",deployment=~"github-runner(|-(sharedpos|puppet|signage|dms|telephony|print-web|chat|mysql|kiosk-linux))"} == 0', instant: true, refId: A}
|
|
||||||
- refId: B
|
|
||||||
relativeTimeRange: {from: 300, to: 0}
|
|
||||||
datasourceUid: __expr__
|
|
||||||
model: {type: reduce, expression: A, reducer: last, refId: B}
|
|
||||||
- refId: C
|
|
||||||
relativeTimeRange: {from: 300, to: 0}
|
|
||||||
datasourceUid: __expr__
|
|
||||||
model: {type: threshold, expression: B, conditions: [{evaluator: {params: [0], type: gt}}], refId: C}
|
|
||||||
- orgId: 1
|
- orgId: 1
|
||||||
name: Infrastructure
|
name: Infrastructure
|
||||||
folder: AI Stack Alerts
|
folder: AI Stack Alerts
|
||||||
@@ -3504,32 +3427,6 @@ data:
|
|||||||
relativeTimeRange: {from: 120, to: 0}
|
relativeTimeRange: {from: 120, to: 0}
|
||||||
datasourceUid: __expr__
|
datasourceUid: __expr__
|
||||||
model: {type: threshold, expression: B, conditions: [{evaluator: {params: [1], type: lt}}], refId: C}
|
model: {type: threshold, expression: B, conditions: [{evaluator: {params: [1], type: lt}}], refId: C}
|
||||||
- uid: macmini-runner-offline
|
|
||||||
title: MacMiniRunnerOffline
|
|
||||||
condition: C
|
|
||||||
for: 10m
|
|
||||||
noDataState: Alerting
|
|
||||||
execErrState: OK
|
|
||||||
annotations:
|
|
||||||
summary: Mac mini GitHub runner offline
|
|
||||||
description: "One or more macmini-* GitHub Actions runners have not reported online for more than 10 minutes. LaunchDaemons survive reboot and do not require the bluejay GUI session."
|
|
||||||
runbook: "1. ssh fcadmin@macmini.iamworkin.lan 2. launchctl print system/io.flowercore.github-runner-<slug> 3. Check /Users/fcadmin/Library/Logs/github-runners/<slug>/stderr.log 4. Re-register the repo runner if .runner is missing"
|
|
||||||
labels:
|
|
||||||
severity: warning
|
|
||||||
service: github-runner
|
|
||||||
data:
|
|
||||||
- refId: A
|
|
||||||
relativeTimeRange: {from: 600, to: 0}
|
|
||||||
datasourceUid: prometheus
|
|
||||||
model: {expr: 'min(flowercore_github_runner_online{runner=~"macmini-.*"} or vector(0))', instant: true, refId: A}
|
|
||||||
- refId: B
|
|
||||||
relativeTimeRange: {from: 600, to: 0}
|
|
||||||
datasourceUid: __expr__
|
|
||||||
model: {type: reduce, expression: A, reducer: last, refId: B}
|
|
||||||
- refId: C
|
|
||||||
relativeTimeRange: {from: 600, to: 0}
|
|
||||||
datasourceUid: __expr__
|
|
||||||
model: {type: threshold, expression: B, conditions: [{evaluator: {params: [1], type: lt}}], refId: C}
|
|
||||||
- uid: high-cpu
|
- uid: high-cpu
|
||||||
title: High CPU (>85%)
|
title: High CPU (>85%)
|
||||||
condition: C
|
condition: C
|
||||||
|
|||||||
@@ -305,17 +305,15 @@ spec:
|
|||||||
path: /
|
path: /
|
||||||
port: 8080
|
port: 8080
|
||||||
initialDelaySeconds: 60
|
initialDelaySeconds: 60
|
||||||
timeoutSeconds: 15
|
timeoutSeconds: 5
|
||||||
periodSeconds: 10
|
periodSeconds: 10
|
||||||
failureThreshold: 3
|
|
||||||
readinessProbe:
|
readinessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /
|
path: /
|
||||||
port: 8080
|
port: 8080
|
||||||
initialDelaySeconds: 30
|
initialDelaySeconds: 30
|
||||||
periodSeconds: 5
|
periodSeconds: 5
|
||||||
timeoutSeconds: 15
|
timeoutSeconds: 5
|
||||||
failureThreshold: 3
|
|
||||||
---
|
---
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: Service
|
kind: Service
|
||||||
|
|||||||
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);
|
||||||
|
}
|
||||||
@@ -54,43 +54,6 @@ public sealed class FleetManifestLintTests
|
|||||||
"ttsreader-piper",
|
"ttsreader-piper",
|
||||||
};
|
};
|
||||||
|
|
||||||
private static readonly IReadOnlyDictionary<string, string> LinuxRunnerRepos = new Dictionary<string, string>(StringComparer.Ordinal)
|
|
||||||
{
|
|
||||||
["github-runner"] = "https://github.com/astoltz/FlowerCore.Common",
|
|
||||||
["github-runner-sharedpos"] = "https://github.com/astoltz/FlowerCore.Shared.Pos",
|
|
||||||
["github-runner-puppet"] = "https://github.com/astoltz/FlowerCore.Puppet",
|
|
||||||
["github-runner-signage"] = "https://github.com/astoltz/FlowerCore.Signage",
|
|
||||||
["github-runner-dms"] = "https://github.com/astoltz/FlowerCore.DMS",
|
|
||||||
["github-runner-telephony"] = "https://github.com/astoltz/FlowerCore.Telephony",
|
|
||||||
["github-runner-print-web"] = "https://github.com/astoltz/FlowerCore.Print.Web",
|
|
||||||
["github-runner-chat"] = "https://github.com/astoltz/FlowerCore.Chat",
|
|
||||||
["github-runner-mysql"] = "https://github.com/astoltz/FlowerCore.MySQL",
|
|
||||||
["github-runner-kiosk-linux"] = "https://github.com/astoltz/FlowerCore.Kiosk.Linux",
|
|
||||||
};
|
|
||||||
|
|
||||||
private static readonly HashSet<string> ScaledLinuxRunnerDeployments = new(StringComparer.Ordinal)
|
|
||||||
{
|
|
||||||
"github-runner-sharedpos",
|
|
||||||
"github-runner-puppet",
|
|
||||||
"github-runner-signage",
|
|
||||||
"github-runner-dms",
|
|
||||||
"github-runner-telephony",
|
|
||||||
"github-runner-print-web",
|
|
||||||
"github-runner-chat",
|
|
||||||
"github-runner-mysql",
|
|
||||||
"github-runner-kiosk-linux",
|
|
||||||
};
|
|
||||||
|
|
||||||
private static readonly IReadOnlyDictionary<string, string> WritableRunnerEnv = new Dictionary<string, string>(StringComparer.Ordinal)
|
|
||||||
{
|
|
||||||
["HOME"] = "/home/runner",
|
|
||||||
["DOTNET_INSTALL_DIR"] = "/home/runner/.dotnet",
|
|
||||||
["DOTNET_CLI_HOME"] = "/home/runner",
|
|
||||||
["NUGET_PACKAGES"] = "/home/runner/.nuget/packages",
|
|
||||||
["XDG_CACHE_HOME"] = "/home/runner/.cache",
|
|
||||||
["RUNNER_TOOL_CACHE"] = "/home/runner/_tool",
|
|
||||||
};
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void IngressRoutes_MustKeepServiceReferencesInTheSameNamespace()
|
public void IngressRoutes_MustKeepServiceReferencesInTheSameNamespace()
|
||||||
{
|
{
|
||||||
@@ -224,98 +187,6 @@ public sealed class FleetManifestLintTests
|
|||||||
violations.Should().BeEmpty();
|
violations.Should().BeEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void GitHubRunnerFleet_MustRegisterRequiredReposAsRepoScopedDeployments()
|
|
||||||
{
|
|
||||||
var deployments = GitHubRunnerDeployments();
|
|
||||||
|
|
||||||
foreach (var expectedRunner in LinuxRunnerRepos)
|
|
||||||
{
|
|
||||||
deployments.Should().ContainKey(expectedRunner.Key);
|
|
||||||
|
|
||||||
var container = deployments[expectedRunner.Key].ContainerMappings().Should().ContainSingle().Subject;
|
|
||||||
EnvValue(container, "REPO_URL").Should().Be(expectedRunner.Value);
|
|
||||||
EnvValue(container, "EPHEMERAL").Should().Be("true");
|
|
||||||
EnvValue(container, "LABELS").Should().Be("self-hosted,linux,fc-build-linux");
|
|
||||||
EnvValue(container, "RUN_AS_ROOT").Should().Be("false");
|
|
||||||
EnvValue(container, "ACCESS_TOKEN").Should().BeNull("ACCESS_TOKEN must come from github-runner-token Secret, not a literal");
|
|
||||||
EnvSecretName(container, "ACCESS_TOKEN").Should().Be("github-runner-token");
|
|
||||||
EnvSecretKey(container, "ACCESS_TOKEN").Should().Be("credential");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void GitHubRunnerFleet_MustSetWritableNonRootDotnetAndCachePaths()
|
|
||||||
{
|
|
||||||
foreach (var deployment in GitHubRunnerDeployments().Values)
|
|
||||||
{
|
|
||||||
var container = deployment.ContainerMappings().Should().ContainSingle().Subject;
|
|
||||||
|
|
||||||
foreach (var expectedEnv in WritableRunnerEnv)
|
|
||||||
{
|
|
||||||
EnvValue(container, expectedEnv.Key).Should().Be(expectedEnv.Value, $"{deployment.Name} must keep .NET paths writable for uid 1001");
|
|
||||||
}
|
|
||||||
|
|
||||||
var mounts = ManifestNodeExtensions.MappingSequence(container, "volumeMounts")
|
|
||||||
.ToDictionary(
|
|
||||||
mount => ManifestNodeExtensions.Scalar(mount, "name") ?? string.Empty,
|
|
||||||
mount => ManifestNodeExtensions.Scalar(mount, "mountPath") ?? string.Empty,
|
|
||||||
StringComparer.Ordinal);
|
|
||||||
|
|
||||||
mounts.Should().Contain("runner-home", "/home/runner");
|
|
||||||
mounts.Should().Contain("nuget-cache", "/home/runner/.nuget/packages");
|
|
||||||
mounts.Should().Contain("tmp", "/tmp");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void GitHubRunnerFleet_MustAvoidRwoMultiAttachForScaledDeployments()
|
|
||||||
{
|
|
||||||
var deployments = GitHubRunnerDeployments();
|
|
||||||
|
|
||||||
foreach (var deploymentName in ScaledLinuxRunnerDeployments)
|
|
||||||
{
|
|
||||||
var deployment = deployments[deploymentName];
|
|
||||||
ReplicaCount(deployment).Should().Be(2);
|
|
||||||
|
|
||||||
var volumes = deployment.MappingSequence("spec", "template", "spec", "volumes");
|
|
||||||
var claimNames = volumes
|
|
||||||
.Select(volume => ManifestNodeExtensions.Scalar(volume, "persistentVolumeClaim", "claimName"))
|
|
||||||
.Where(value => !string.IsNullOrWhiteSpace(value))
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
claimNames.Should().BeEmpty($"{deploymentName} is scaled and must not share a RWO PVC");
|
|
||||||
volumes.Should().Contain(volume =>
|
|
||||||
string.Equals(ManifestNodeExtensions.Scalar(volume, "name"), "nuget-cache", StringComparison.Ordinal)
|
|
||||||
&& ManifestNodeExtensions.Mapping(volume, "emptyDir") != null);
|
|
||||||
}
|
|
||||||
|
|
||||||
var common = deployments["github-runner"];
|
|
||||||
ReplicaCount(common).Should().Be(1);
|
|
||||||
common.MappingSequence("spec", "template", "spec", "volumes")
|
|
||||||
.Select(volume => ManifestNodeExtensions.Scalar(volume, "persistentVolumeClaim", "claimName"))
|
|
||||||
.Where(value => !string.IsNullOrWhiteSpace(value))
|
|
||||||
.Should()
|
|
||||||
.ContainSingle()
|
|
||||||
.Which
|
|
||||||
.Should()
|
|
||||||
.Be("github-runner-nuget-cache");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Monitoring_MustAlertWhenLinuxRunnerDeploymentIsUnavailable()
|
|
||||||
{
|
|
||||||
var monitoring = File.ReadAllText(Path.Combine(Inventory.BluejayRoot, "apps", "monitoring", "noc-monitoring.yaml"));
|
|
||||||
|
|
||||||
monitoring.Should().Contain("MacMiniRunnerOffline");
|
|
||||||
monitoring.Should().Contain("LinuxRunnerOffline");
|
|
||||||
monitoring.Should().Contain("kube_deployment_status_replicas_ready");
|
|
||||||
monitoring.Should().Contain("github-runner(|-(sharedpos|puppet|signage|dms|telephony|print-web|chat|mysql|kiosk-linux))");
|
|
||||||
monitoring.Should().Contain("folder: CI Alerts");
|
|
||||||
monitoring.Should().Contain("uid: linux-runner-offline");
|
|
||||||
monitoring.Should().Contain("alert_channel: irc");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void StatefulSets_WithVolumeClaimTemplates_MustDeclareFilesystemDefaults()
|
public void StatefulSets_WithVolumeClaimTemplates_MustDeclareFilesystemDefaults()
|
||||||
{
|
{
|
||||||
@@ -420,184 +291,6 @@ public sealed class FleetManifestLintTests
|
|||||||
violations.Should().BeEmpty();
|
violations.Should().BeEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void FcDeviceManagement_MustShipExpectedManifestSet()
|
|
||||||
{
|
|
||||||
var appRoot = Path.Combine(Inventory.BluejayRoot, "apps", "fc-devicemgmt");
|
|
||||||
Directory.Exists(appRoot).Should().BeTrue("Sprint 8 Cx-5 owns apps/fc-devicemgmt.");
|
|
||||||
|
|
||||||
var expectedFiles = new[]
|
|
||||||
{
|
|
||||||
"1password-item.yaml",
|
|
||||||
"argocd-application.yaml",
|
|
||||||
"certificate-web.yaml",
|
|
||||||
"clusterrole-operator.yaml",
|
|
||||||
"clusterrolebinding-operator.yaml",
|
|
||||||
"deployment-operator.yaml",
|
|
||||||
"deployment-web.yaml",
|
|
||||||
"ingressroute-web.yaml",
|
|
||||||
"namespace.yaml",
|
|
||||||
"network-policy.yaml",
|
|
||||||
"service-web.yaml",
|
|
||||||
"serviceaccount-operator.yaml",
|
|
||||||
};
|
|
||||||
|
|
||||||
Directory.GetFiles(appRoot, "*.yaml")
|
|
||||||
.Select(Path.GetFileName)
|
|
||||||
.Should()
|
|
||||||
.BeEquivalentTo(expectedFiles);
|
|
||||||
|
|
||||||
foreach (var expectedFile in expectedFiles)
|
|
||||||
{
|
|
||||||
FcDeviceManagementDocuments()
|
|
||||||
.Should()
|
|
||||||
.Contain(document => document.RelativePath == $"fc-devicemgmt/{expectedFile}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void FcDeviceManagement_ObjectsMustCarryStandardTraceabilityLabels()
|
|
||||||
{
|
|
||||||
var requiredLabels = new[]
|
|
||||||
{
|
|
||||||
"app.kubernetes.io/name",
|
|
||||||
"app.kubernetes.io/part-of",
|
|
||||||
"app.kubernetes.io/managed-by",
|
|
||||||
"flowercore.io/tenant-id",
|
|
||||||
"flowercore.io/created-by",
|
|
||||||
};
|
|
||||||
|
|
||||||
var violations = FcDeviceManagementDocuments()
|
|
||||||
.SelectMany(document => requiredLabels
|
|
||||||
.Where(label => string.IsNullOrWhiteSpace(document.Scalar("metadata", "labels", label)))
|
|
||||||
.Select(label => $"{document.Descriptor} is missing metadata.labels['{label}']."))
|
|
||||||
.Concat(FcDeviceManagementDocuments()
|
|
||||||
.Where(document => document.Kind == "Deployment")
|
|
||||||
.SelectMany(document => requiredLabels
|
|
||||||
.Where(label => string.IsNullOrWhiteSpace(document.Scalar("spec", "template", "metadata", "labels", label)))
|
|
||||||
.Select(label => $"{document.Descriptor} pod template is missing metadata.labels['{label}'].")))
|
|
||||||
.Concat(FcDeviceManagementDocuments()
|
|
||||||
.Where(document => document.Kind == "Deployment")
|
|
||||||
.Where(document => string.IsNullOrWhiteSpace(document.Scalar("spec", "template", "metadata", "annotations", "flowercore.io/audit-trace-id")))
|
|
||||||
.Select(document => $"{document.Descriptor} pod template is missing flowercore.io/audit-trace-id."))
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
violations.Should().BeEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void FcDeviceManagement_IngressMustUseCertManagerAndKeepPublicHostDisabled()
|
|
||||||
{
|
|
||||||
var appText = string.Join(
|
|
||||||
Environment.NewLine,
|
|
||||||
Directory.GetFiles(Path.Combine(Inventory.BluejayRoot, "apps", "fc-devicemgmt"), "*.yaml")
|
|
||||||
.Select(File.ReadAllText));
|
|
||||||
|
|
||||||
appText.Should().NotContain("certResolver");
|
|
||||||
appText.Should().Contain("update.flowercore.io");
|
|
||||||
appText.Should().Contain("disabled-until-Q-OIDC-1");
|
|
||||||
|
|
||||||
FcDeviceManagementDocuments()
|
|
||||||
.Where(document => document.Kind == "IngressRoute")
|
|
||||||
.SelectMany(document => document.MappingSequence("spec", "routes"))
|
|
||||||
.Select(route => ManifestNodeExtensions.Scalar(route, "match") ?? string.Empty)
|
|
||||||
.Should()
|
|
||||||
.Contain(match => match.Contains("Host(`devices.iamworkin.lan`)", StringComparison.Ordinal))
|
|
||||||
.And.NotContain(match => match.Contains("Host(`update.flowercore.io`)", StringComparison.Ordinal));
|
|
||||||
|
|
||||||
var certificate = FcDeviceManagementDocuments()
|
|
||||||
.Single(document => document.Kind == "Certificate" && document.Name == "fc-devicemgmt-web-tls");
|
|
||||||
|
|
||||||
certificate.Scalar("spec", "issuerRef", "name").Should().Be("step-ca-acme");
|
|
||||||
certificate.Scalar("spec", "issuerRef", "kind").Should().Be("ClusterIssuer");
|
|
||||||
ManifestNodeExtensions.ScalarSequence(certificate.Root, "spec", "dnsNames")
|
|
||||||
.Should()
|
|
||||||
.ContainSingle("devices.iamworkin.lan");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void FcDeviceManagement_OperatorRbacMustCoverDevicesAndOwnerLookup()
|
|
||||||
{
|
|
||||||
var clusterRole = FcDeviceManagementDocuments()
|
|
||||||
.Single(document => document.Kind == "ClusterRole" && document.Name == "fc-devicemgmt-operator");
|
|
||||||
var allScalars = clusterRole.AllScalars().ToList();
|
|
||||||
|
|
||||||
allScalars.Should().Contain("devices.flowercore.io");
|
|
||||||
allScalars.Should().Contain("*");
|
|
||||||
allScalars.Should().Contain("deployments");
|
|
||||||
allScalars.Should().Contain("get");
|
|
||||||
|
|
||||||
var operatorDeployment = FcDeviceManagementDocuments()
|
|
||||||
.Single(document => document.Kind == "Deployment" && document.Name == "fc-devicemgmt-operator");
|
|
||||||
|
|
||||||
operatorDeployment.AllScalars().Should().Contain("FLOWERCORE_KUBERNETES_OWNER_DEPLOYMENT");
|
|
||||||
operatorDeployment.AllScalars().Should().Contain("fc-devicemgmt-operator");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void FcDeviceManagement_RuntimeSecretsMustUseOnePasswordItemPattern()
|
|
||||||
{
|
|
||||||
var item = FcDeviceManagementDocuments()
|
|
||||||
.Single(document => document.Kind == "OnePasswordItem" && document.Name == "fc-devicemgmt-runtime");
|
|
||||||
|
|
||||||
item.Scalar("spec", "itemPath")
|
|
||||||
.Should()
|
|
||||||
.Be("vaults/IAmWorkin/items/FlowerCore DeviceManagement Runtime");
|
|
||||||
|
|
||||||
var appText = string.Join(
|
|
||||||
Environment.NewLine,
|
|
||||||
Directory.GetFiles(Path.Combine(Inventory.BluejayRoot, "apps", "fc-devicemgmt"), "*.yaml")
|
|
||||||
.Select(File.ReadAllText));
|
|
||||||
|
|
||||||
FcDeviceManagementDocuments().Should().NotContain(document => document.Kind == "Secret");
|
|
||||||
appText.Should().Contain("secretKeyRef:");
|
|
||||||
appText.Should().Contain("secretName: fc-devicemgmt-runtime");
|
|
||||||
appText.Should().NotContain("stringData:");
|
|
||||||
appText.Should().NotContain("from-literal");
|
|
||||||
appText.Should().NotContain("tls.key:");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void FcDeviceManagement_NetworkPoliciesMustAllowLanAgentsSynologyAndDnatPorts()
|
|
||||||
{
|
|
||||||
var policies = FcDeviceManagementDocuments()
|
|
||||||
.Where(document => document.Kind == "NetworkPolicy")
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
policies.Should().HaveCount(2);
|
|
||||||
|
|
||||||
var combinedScalars = policies.SelectMany(policy => policy.AllScalars()).ToList();
|
|
||||||
combinedScalars.Should().Contain("10.0.56.0/24");
|
|
||||||
combinedScalars.Should().Contain("10.0.57.0/24");
|
|
||||||
combinedScalars.Should().Contain("10.0.58.0/24");
|
|
||||||
combinedScalars.Should().Contain("10.0.68.0/27");
|
|
||||||
combinedScalars.Should().Contain("10.0.58.3/32");
|
|
||||||
|
|
||||||
var combinedEgressPorts = policies.SelectMany(policy => policy.EgressPorts()).ToHashSet(StringComparer.Ordinal);
|
|
||||||
combinedEgressPorts.Should().Contain(new[] { "80", "443", "8080", "8443", "2049", "111" });
|
|
||||||
|
|
||||||
var traefikVipPolicies = policies
|
|
||||||
.Where(policy => policy.AllScalars().Any(value => value.Contains("10.0.56.200", StringComparison.Ordinal)))
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
traefikVipPolicies.Should().ContainSingle();
|
|
||||||
traefikVipPolicies[0].EgressPorts().Should().Contain(new[] { "80", "443", "8080", "8443" });
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void FcDeviceManagement_ArgocdApplicationMustMatchApplicationSetDiscoveryConventions()
|
|
||||||
{
|
|
||||||
var application = FcDeviceManagementDocuments()
|
|
||||||
.Single(document => document.Kind == "Application" && document.Name == "infra-fc-devicemgmt");
|
|
||||||
|
|
||||||
application.Namespace.Should().Be("argocd");
|
|
||||||
application.Scalar("spec", "source", "repoURL")
|
|
||||||
.Should()
|
|
||||||
.Be("http://gitea-clusterip.gitea.svc.cluster.local:3000/bluejay/bluejay-infra.git");
|
|
||||||
application.Scalar("spec", "source", "path").Should().Be("apps/fc-devicemgmt");
|
|
||||||
application.Scalar("spec", "destination", "namespace").Should().Be("fc-devicemgmt");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static IEnumerable<string> ProbeViolations(
|
private static IEnumerable<string> ProbeViolations(
|
||||||
ManifestDocument document,
|
ManifestDocument document,
|
||||||
YamlMappingNode container,
|
YamlMappingNode container,
|
||||||
@@ -621,51 +314,6 @@ public sealed class FleetManifestLintTests
|
|||||||
$"{document.Descriptor} container '{containerName}' still uses {probeKey}.httpGet on /health.",
|
$"{document.Descriptor} container '{containerName}' still uses {probeKey}.httpGet on /health.",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IReadOnlyDictionary<string, ManifestDocument> GitHubRunnerDeployments()
|
|
||||||
{
|
|
||||||
return Inventory.Documents
|
|
||||||
.Where(document => document.Kind == "Deployment")
|
|
||||||
.Where(document => document.Namespace == "github-runner")
|
|
||||||
.ToDictionary(document => document.Name, StringComparer.Ordinal);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int ReplicaCount(ManifestDocument document)
|
|
||||||
{
|
|
||||||
return int.TryParse(document.Scalar("spec", "replicas"), out var replicas) ? replicas : 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string? EnvValue(YamlMappingNode container, string name)
|
|
||||||
{
|
|
||||||
return EnvMapping(container, name) is { } env ? ManifestNodeExtensions.Scalar(env, "value") : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string? EnvSecretName(YamlMappingNode container, string name)
|
|
||||||
{
|
|
||||||
return EnvMapping(container, name) is { } env
|
|
||||||
? ManifestNodeExtensions.Scalar(env, "valueFrom", "secretKeyRef", "name")
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string? EnvSecretKey(YamlMappingNode container, string name)
|
|
||||||
{
|
|
||||||
return EnvMapping(container, name) is { } env
|
|
||||||
? ManifestNodeExtensions.Scalar(env, "valueFrom", "secretKeyRef", "key")
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static YamlMappingNode? EnvMapping(YamlMappingNode container, string name)
|
|
||||||
{
|
|
||||||
return ManifestNodeExtensions.MappingSequence(container, "env")
|
|
||||||
.SingleOrDefault(env => string.Equals(ManifestNodeExtensions.Scalar(env, "name"), name, StringComparison.Ordinal));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static IReadOnlyList<ManifestDocument> FcDeviceManagementDocuments()
|
|
||||||
{
|
|
||||||
return Inventory.Documents
|
|
||||||
.Where(document => document.RelativePath.StartsWith("fc-devicemgmt/", StringComparison.Ordinal))
|
|
||||||
.ToList();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal sealed class ManifestInventory
|
internal sealed class ManifestInventory
|
||||||
|
|||||||
@@ -1,269 +0,0 @@
|
|||||||
using System.Text.Json;
|
|
||||||
using FluentAssertions;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace BluejayInfraLint.Tests;
|
|
||||||
|
|
||||||
[Trait("Category", "Unit")]
|
|
||||||
public sealed class PiSignagePlayerArtifactTests
|
|
||||||
{
|
|
||||||
private static readonly string Root = FindRepoRoot();
|
|
||||||
private static readonly string AppRoot = Path.Combine(Root, "apps", "fc-signage-pi-player");
|
|
||||||
|
|
||||||
public static TheoryData<string> RequiredArtifacts => new()
|
|
||||||
{
|
|
||||||
"README.md",
|
|
||||||
"systemd/flowercore-signage-player-pi.service",
|
|
||||||
"systemd/flowercore-signage-player-pi-hdmi.service",
|
|
||||||
"systemd/flowercore-signage-bootstrap.service",
|
|
||||||
"systemd/flowercore-signage-renew.service",
|
|
||||||
"systemd/flowercore-signage-renew.timer",
|
|
||||||
"systemd/flowercore-signage-detect-display.service",
|
|
||||||
"systemd/flowercore-signage-detect-display.timer",
|
|
||||||
"systemd/99-flowercore-signage-hdmi.rules",
|
|
||||||
"chromium-policies/flowercore-signage.json",
|
|
||||||
"scripts/flowercore-signage-launch.sh",
|
|
||||||
"scripts/flowercore-signage-prelaunch.sh",
|
|
||||||
"scripts/flowercore-signage-bootstrap.sh",
|
|
||||||
"scripts/flowercore-signage-renew-cert.sh",
|
|
||||||
"scripts/flowercore-signage-hdmi-respond.sh",
|
|
||||||
"scripts/fc-signage-detect-display",
|
|
||||||
};
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[MemberData(nameof(RequiredArtifacts))]
|
|
||||||
public void RequiredArtifacts_ArePresent(string relativePath)
|
|
||||||
{
|
|
||||||
File.Exists(Path.Combine(AppRoot, relativePath)).Should().BeTrue(relativePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void PlayerService_UsesExpectedRestartAndMemoryGuards()
|
|
||||||
{
|
|
||||||
var unit = Read("systemd/flowercore-signage-player-pi.service");
|
|
||||||
|
|
||||||
unit.Should().Contain("Restart=always");
|
|
||||||
unit.Should().Contain("RestartSec=10s");
|
|
||||||
unit.Should().Contain("StartLimitBurst=5");
|
|
||||||
unit.Should().Contain("StartLimitIntervalSec=300s");
|
|
||||||
unit.Should().Contain("MemoryMax=2G");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void PlayerService_IsGatedByNodeIdentityAndMtlsCertificate()
|
|
||||||
{
|
|
||||||
var unit = Read("systemd/flowercore-signage-player-pi.service");
|
|
||||||
|
|
||||||
unit.Should().Contain("ConditionPathExists=/etc/flowercore/signage-node.json");
|
|
||||||
unit.Should().Contain("ConditionPathExists=/etc/fc-signage-player/client.p12");
|
|
||||||
unit.Should().Contain("ExecStartPre=/usr/local/bin/flowercore-signage-prelaunch.sh");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void LaunchScript_TriesEmbedThenFallsBackToBarePlayerRoute()
|
|
||||||
{
|
|
||||||
var script = Read("scripts/flowercore-signage-launch.sh");
|
|
||||||
|
|
||||||
script.Should().Contain("/player/${NODE_ID}/embed?token=${CERT_THUMB}");
|
|
||||||
script.Should().Contain("url-divergence.log");
|
|
||||||
script.Should().Contain("/player/${NODE_ID}?token=${CERT_THUMB}");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void LaunchScript_DisablesChromiumPromptsAndRuntimeUpdates()
|
|
||||||
{
|
|
||||||
var script = Read("scripts/flowercore-signage-launch.sh");
|
|
||||||
|
|
||||||
script.Should().Contain("--noerrdialogs");
|
|
||||||
script.Should().Contain("--disable-infobars");
|
|
||||||
script.Should().Contain("--password-store=basic");
|
|
||||||
script.Should().Contain("--check-for-update-interval=2592000");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void PrelaunchScript_AbortsWhenRequiredFilesAreMissing()
|
|
||||||
{
|
|
||||||
var script = Read("scripts/flowercore-signage-prelaunch.sh");
|
|
||||||
|
|
||||||
script.Should().Contain("for f in /etc/flowercore/signage-node.json /etc/fc-signage-player/client.p12 /etc/fc-signage-player/client.p12.pass");
|
|
||||||
script.Should().Contain("exit 1");
|
|
||||||
script.Should().Contain("-checkend $((7*24*3600))");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void BootstrapScript_IsIdempotentWhenAlreadyEnrolled()
|
|
||||||
{
|
|
||||||
var script = Read("scripts/flowercore-signage-bootstrap.sh");
|
|
||||||
|
|
||||||
script.Should().Contain("already enrolled");
|
|
||||||
script.Should().Contain("exit 0");
|
|
||||||
script.Should().Contain(".enrolledAt");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void BootstrapScript_GeneratesStableMachineIdFromUuid()
|
|
||||||
{
|
|
||||||
var script = Read("scripts/flowercore-signage-bootstrap.sh");
|
|
||||||
|
|
||||||
script.Should().Contain("uuidgen");
|
|
||||||
script.Should().Contain("cut -c1-16");
|
|
||||||
script.Should().Contain("machineId");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void BootstrapScript_RetriesRegisterOnceForFirstCallRace()
|
|
||||||
{
|
|
||||||
var script = Read("scripts/flowercore-signage-bootstrap.sh");
|
|
||||||
|
|
||||||
script.Should().Contain("for attempt in 1 2");
|
|
||||||
script.Should().Contain("register attempt $attempt returned");
|
|
||||||
script.Should().Contain("sleep 5");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void BootstrapScript_SupportsSetupCodeAndApprovalPollingBudget()
|
|
||||||
{
|
|
||||||
var script = Read("scripts/flowercore-signage-bootstrap.sh");
|
|
||||||
|
|
||||||
script.Should().Contain("signage-setup-code");
|
|
||||||
script.Should().Contain("approve-via-setup-code");
|
|
||||||
script.Should().Contain("+ 1800");
|
|
||||||
script.Should().Contain("sleep 15");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void BootstrapScript_CsrSubjectIdentifiesPiPlayer()
|
|
||||||
{
|
|
||||||
var script = Read("scripts/flowercore-signage-bootstrap.sh");
|
|
||||||
|
|
||||||
script.Should().Contain("/CN=${NODE_ID}/O=FlowerCore/OU=SignagePlayer-Pi");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void BootstrapScript_PersistsCertificateAsP12WithRestrictivePermissions()
|
|
||||||
{
|
|
||||||
var script = Read("scripts/flowercore-signage-bootstrap.sh");
|
|
||||||
|
|
||||||
script.Should().Contain("openssl pkcs12 -export");
|
|
||||||
script.Should().Contain("client.p12.pass");
|
|
||||||
script.Should().Contain("chmod 0600");
|
|
||||||
script.Should().Contain("chmod 0640");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void RenewScript_OnlyRunsWhenCertHasLessThanThirtyDays()
|
|
||||||
{
|
|
||||||
var script = Read("scripts/flowercore-signage-renew-cert.sh");
|
|
||||||
|
|
||||||
script.Should().Contain("-checkend $((30*24*3600))");
|
|
||||||
script.Should().Contain("exit 0");
|
|
||||||
script.Should().Contain("/renew");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void RenewScript_AtomicallySwapsNewCertificateFiles()
|
|
||||||
{
|
|
||||||
var script = Read("scripts/flowercore-signage-renew-cert.sh");
|
|
||||||
|
|
||||||
script.Should().Contain("client.key.new");
|
|
||||||
script.Should().Contain("mv \"$CERT_DIR/client.key.new\" \"$CERT_DIR/client.key\"");
|
|
||||||
script.Should().Contain("mv \"$CERT_DIR/client.p12.new\" \"$CERT_DIR/client.p12\"");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void HdmiRule_RestartsPlayerAndRunsCapabilityDetection()
|
|
||||||
{
|
|
||||||
var rule = Read("systemd/99-flowercore-signage-hdmi.rules");
|
|
||||||
var responder = Read("scripts/flowercore-signage-hdmi-respond.sh");
|
|
||||||
|
|
||||||
rule.Should().Contain("KERNEL==\"card?-HDMI-A-?\"");
|
|
||||||
rule.Should().Contain("start flowercore-signage-player-pi-hdmi.service");
|
|
||||||
responder.Should().Contain("sleep 2");
|
|
||||||
responder.Should().Contain("start flowercore-signage-detect-display.service");
|
|
||||||
responder.Should().Contain("restart flowercore-signage-player-pi.service");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void DetectDisplayServiceAndTimer_RunAtBootAndDaily()
|
|
||||||
{
|
|
||||||
var service = Read("systemd/flowercore-signage-detect-display.service");
|
|
||||||
var timer = Read("systemd/flowercore-signage-detect-display.timer");
|
|
||||||
|
|
||||||
service.Should().Contain("ExecStart=/usr/local/bin/fc-signage-detect-display");
|
|
||||||
timer.Should().Contain("OnBootSec=30s");
|
|
||||||
timer.Should().Contain("OnCalendar=daily");
|
|
||||||
timer.Should().Contain("RandomizedDelaySec=1h");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void DetectDisplayScript_EmitsDisconnectedProfileWhenNoHdmiIsPresent()
|
|
||||||
{
|
|
||||||
var script = Read("scripts/fc-signage-detect-display");
|
|
||||||
|
|
||||||
script.Should().Contain("displayConnected: false");
|
|
||||||
script.Should().Contain("No HDMI display detected");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void DetectDisplayScript_ParsesEdidForHdrResolutionAndAudio()
|
|
||||||
{
|
|
||||||
var script = Read("scripts/fc-signage-detect-display");
|
|
||||||
|
|
||||||
script.Should().Contain("edid-decode");
|
|
||||||
script.Should().Contain("HDR (Static|Dynamic) Metadata Block");
|
|
||||||
script.Should().Contain("maxResolution");
|
|
||||||
script.Should().Contain("hasAudioOutput");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void DetectDisplayScript_TriesBothForwardCompatibleCapabilityEndpoints()
|
|
||||||
{
|
|
||||||
var script = Read("scripts/fc-signage-detect-display");
|
|
||||||
|
|
||||||
script.Should().Contain("/api/v1/nodes/${NODE_ID}/capabilities");
|
|
||||||
script.Should().Contain("/api/v1/displays/${NODE_ID}/capability-profile");
|
|
||||||
script.Should().Contain("no endpoint accepted the profile");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void ChromiumPolicy_IsValidJsonAndDisablesCredentialPrompts()
|
|
||||||
{
|
|
||||||
using var doc = JsonDocument.Parse(Read("chromium-policies/flowercore-signage.json"));
|
|
||||||
var root = doc.RootElement;
|
|
||||||
|
|
||||||
root.GetProperty("AutofillAddressEnabled").GetBoolean().Should().BeFalse();
|
|
||||||
root.GetProperty("AutofillCreditCardEnabled").GetBoolean().Should().BeFalse();
|
|
||||||
root.GetProperty("PasswordManagerEnabled").GetBoolean().Should().BeFalse();
|
|
||||||
root.GetProperty("ExtensionInstallBlocklist")[0].GetString().Should().Be("*");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void RenewalTimer_UsesDailyCadenceWithTwoHourJitter()
|
|
||||||
{
|
|
||||||
var timer = Read("systemd/flowercore-signage-renew.timer");
|
|
||||||
|
|
||||||
timer.Should().Contain("OnCalendar=daily");
|
|
||||||
timer.Should().Contain("RandomizedDelaySec=2h");
|
|
||||||
timer.Should().Contain("Persistent=true");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string Read(string relativePath)
|
|
||||||
=> File.ReadAllText(Path.Combine(AppRoot, relativePath.Replace('/', Path.DirectorySeparatorChar)));
|
|
||||||
|
|
||||||
private static string FindRepoRoot()
|
|
||||||
{
|
|
||||||
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.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user