feat(apps): forte-drop web + mcp argocd apps (prod) #18

Merged
jorgen.stensrud merged 23 commits from feat/forte-drop-apps into main 2026-06-04 18:47:08 +00:00
7 changed files with 144 additions and 0 deletions
Showing only changes of commit a2fae9dd0c - Show all commits

View File

@@ -0,0 +1,37 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: forte-drop-mcp
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "1"
notifications.argoproj.io/subscribe.on-sync-succeeded.slack: ""
notifications.argoproj.io/subscribe.on-sync-failed.slack: ""
notifications.argoproj.io/subscribe.on-degraded.slack: ""
labels:
app.kubernetes.io/name: forte-drop-mcp
app.kubernetes.io/part-of: apps
app.kubernetes.io/managed-by: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: default
sources:
- repoURL: ssh://git@git.forteapps.net:2222/Forte/forte-helm.git
path: forteapp
targetRevision: HEAD

Using 'HEAD' for targetRevision can lead to unpredictable deployments - prefer a specific tag or branch.

    targetRevision: main

#ai-review-inline

Using 'HEAD' for targetRevision can lead to unpredictable deployments - prefer a specific tag or branch. ```suggestion targetRevision: main ``` #ai-review-inline
helm:
valueFiles:
- $values/forte-drop-mcp/values.yaml
- repoURL: ssh://git@git.forteapps.net:2222/Forte/helm-prod-values.git
targetRevision: HEAD

Using 'HEAD' for targetRevision can lead to unpredictable deployments - prefer a specific tag or branch.

    targetRevision: main

#ai-review-inline

Using 'HEAD' for targetRevision can lead to unpredictable deployments - prefer a specific tag or branch. ```suggestion targetRevision: main ``` #ai-review-inline
ref: values
destination:
server: https://kubernetes.default.svc
namespace: forte-drop
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true

View File

@@ -0,0 +1,27 @@
# MCP audience client. RFC 7591 dynamic-registration capable MCP clients (e.g.,
# Claude Desktop) discover this via /.well-known/oauth-protected-resource and
# request tokens with aud=https://mcp.drop-k8s.hackathon.forteapps.net/mcp.
apiVersion: v1
kind: Secret

Plain-text Secret should not be committed to Git - use SealedSecret or the Keycloak client registrar instead.

#ai-review-inline

Plain-text Secret should not be committed to Git - use SealedSecret or the Keycloak client registrar instead. #ai-review-inline
metadata:
name: keycloak-client-forte-drop-mcp
namespace: forte-drop
labels:
keycloak.forteapps.net/client-config: "true"
stringData:
client.json: |
{

Du trenger ikke en egen klient for mcp server. Det lages automatisk når du legger inn auth.type: mcp i helm values. Så denne kan slettes.

Du trenger ikke en egen klient for mcp server. Det lages automatisk når du legger inn `auth.type: mcp` i helm values. Så denne kan slettes.
"clientId": "forte-drop-mcp",
"name": "Forte Drop (MCP)",
"enabled": true,
"protocol": "openid-connect",
"clientAuthenticatorType": "client-secret",

Client secret authentication requires a secret value which should not be stored in plain-text Git.

#ai-review-inline

Client secret authentication requires a secret value which should not be stored in plain-text Git. #ai-review-inline
"standardFlowEnabled": false,
"directAccessGrantsEnabled": false,
"serviceAccountsEnabled": false,
"publicClient": false,
"defaultClientScopes": ["openid","profile","email"],
"attributes": {
"access.token.lifespan": "3600"
}
}

View File

@@ -0,0 +1,6 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- forte-drop-mcp.yaml
- keycloak-client-forte-drop-mcp.yaml
# - auth-oidc-sealed.yaml # added in follow-up commit

View File

@@ -0,0 +1,37 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: forte-drop
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "1"
notifications.argoproj.io/subscribe.on-sync-succeeded.slack: ""
notifications.argoproj.io/subscribe.on-sync-failed.slack: ""
notifications.argoproj.io/subscribe.on-degraded.slack: ""
labels:
app.kubernetes.io/name: forte-drop
app.kubernetes.io/part-of: apps
app.kubernetes.io/managed-by: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: default
sources:
- repoURL: ssh://git@git.forteapps.net:2222/Forte/forte-helm.git
path: forteapp
targetRevision: HEAD

Using 'HEAD' as targetRevision can lead to unstable deployments; prefer a specific tag, branch, or commit hash for better control.

    targetRevision: main

#ai-review-inline

Using 'HEAD' as targetRevision can lead to unstable deployments; prefer a specific tag, branch, or commit hash for better control. ```suggestion targetRevision: main ``` #ai-review-inline
helm:
valueFiles:
- $values/forte-drop/values.yaml
- repoURL: ssh://git@git.forteapps.net:2222/Forte/helm-prod-values.git
targetRevision: HEAD

Using 'HEAD' as targetRevision can lead to unstable deployments; prefer a specific tag, branch, or commit hash for better control.

    targetRevision: main

#ai-review-inline

Using 'HEAD' as targetRevision can lead to unstable deployments; prefer a specific tag, branch, or commit hash for better control. ```suggestion targetRevision: main ``` #ai-review-inline
ref: values
destination:
server: https://kubernetes.default.svc
namespace: forte-drop
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true

View File

@@ -0,0 +1,28 @@
# Labeled config Secret read by the Keycloak Client Registrar. The registrar will
# create the OIDC client in the forte realm and write the resulting credentials
# back into forte-drop-oidc-credentials Secret in this namespace within ~2 min.
# That client-secret then gets manually copied into the auth-oidc SealedSecret
# (one-time per cluster; see PR description).
apiVersion: v1
kind: Secret
metadata:
name: keycloak-client-forte-drop
namespace: forte-drop
labels:
keycloak.forteapps.net/client-config: "true"
stringData:
client.json: |
{
"clientId": "forte-drop",
"name": "Forte Drop (web)",
"enabled": true,
"protocol": "openid-connect",
"clientAuthenticatorType": "client-secret",
"standardFlowEnabled": true,
"directAccessGrantsEnabled": false,
"serviceAccountsEnabled": false,
"publicClient": false,
"redirectUris": ["https://drop-k8s.hackathon.forteapps.net/auth/callback"],

Hardcoded URL in redirectUris should be templated or moved to values to support different environments.

#ai-review-inline

Hardcoded URL in redirectUris should be templated or moved to values to support different environments. #ai-review-inline
"webOrigins": ["https://drop-k8s.hackathon.forteapps.net"],

Hardcoded URL in webOrigins should be templated or moved to values to support different environments.

#ai-review-inline

Hardcoded URL in webOrigins should be templated or moved to values to support different environments. #ai-review-inline
"defaultClientScopes": ["openid","email","profile"]
}

Det du mangler her er hvor du vil at Secret skal bli laget og med hvilke keys:

      "secret": {
        "namespace": "myapp",                # Where to create credential Secret
        "name": "myapp-oidc-credentials",    # Name of credential Secret
        "keys": {
          "clientId": "client-id",           # Key name for client ID
          "clientSecret": "client-secret"    # Key name for client secret
        }
      }
Det du mangler her er hvor du vil at `Secret` skal bli laget og med hvilke keys: ``` "secret": { "namespace": "myapp", # Where to create credential Secret "name": "myapp-oidc-credentials", # Name of credential Secret "keys": { "clientId": "client-id", # Key name for client ID "clientSecret": "client-secret" # Key name for client secret } } ```

Detaljert forklaring, hvis interessant:

What a Developer Should Push

A developer deploying a new application must create a Config Secret in their application's namespace. This Secret contains the desired Keycloak client configuration.

The Format

Kubernetes Secret YAML:

apiVersion: v1
kind: Secret
metadata:
  name: keycloak-client-<app-name>          # Any descriptive name
  namespace: <app-namespace>                 # Your app's namespace (e.g., "myapp")
  labels:
    keycloak.forteapps.net/client-config: "true"  # REQUIRED - triggers processing
  annotations:
    keycloak.forteapps.net/source-namespace: "myapp"  # Optional - tracks ownership
stringData:
  client.json: |                             # REQUIRED - the client configuration
    {
      "clientId": "myapp",                   # Pre-determined by developer
      "name": "My Application",              # Display name in Keycloak UI
      "redirectUris": ["https://myapp.forteapps.net/*"],
      "webOrigins": ["https://myapp.forteapps.net"],
      "defaultClientScopes": ["openid", "email", "profile"],
      "protocolMappers": [],
      "secret": {
        "namespace": "myapp",                # Where to create credential Secret
        "name": "myapp-oidc-credentials",    # Name of credential Secret
        "keys": {
          "clientId": "client-id",           # Key name for client ID
          "clientSecret": "client-secret"    # Key name for client secret
        }
      }
    }

How It Works: Client ID vs Client Secret

Client ID (Pre-determined by Developer):

  • The developer chooses clientId (e.g., "myapp")
  • This becomes the public identifier for the OIDC client
  • Used in OAuth flows, login redirects, token requests
  • Stored in the credential Secret under the specified key (default: client-id)

Client Secret (Generated by Keycloak):

  • Keycloak auto-generates a cryptographically secure secret

  • The CronJob fetches it via: GET /admin/realms/forte/clients/{uuid}/client-secret

  • The CronJob then creates/updates TWO Secrets:

    1. Target Namespace Secret (myapp/myapp-oidc-credentials):

    apiVersion: v1
    kind: Secret
    metadata:
      name: myapp-oidc-credentials
      namespace: myapp
      labels:
        app.kubernetes.io/managed-by: keycloak-client-registrar
    type: Opaque
    data:
      client-id: bXlhcHA=           # base64("myapp") - from client.json
      client-secret: <base64>        # Generated by Keycloak
    

    2. Central Backup Secret (secrets/myapp-oidc-credentials):

    • Identical content
    • Always created even if target namespace doesn't exist
    • Used for external deployments, disaster recovery, auditing

Developer Workflow

  1. Create Config Secret in your Helm chart or Kustomize (in your app's namespace)

  2. Deploy - Kyverno policy clones it to keycloak namespace automatically

  3. Wait - CronJob picks it up within 2 minutes

  4. Reference the generated credential Secret in your Deployment:

    env:
    - name: OIDC_CLIENT_ID
      valueFrom:
        secretKeyRef:
          name: myapp-oidc-credentials
          key: client-id
    - name: OIDC_CLIENT_SECRET
      valueFrom:
        secretKeyRef:
          name: myapp-oidc-credentials
          key: client-secret
    

Change Detection

The CronJob computes a SHA256 hash of client.json. If unchanged and credential Secret exists, it skips processing. To force re-sync, modify any field in client.json (e.g., add trailing space to name).

Detaljert forklaring, hvis interessant: ### What a Developer Should Push A developer deploying a new application must create a __Config Secret__ in their application's namespace. This Secret contains the desired Keycloak client configuration. ### The Format __Kubernetes Secret YAML:__ ```yaml apiVersion: v1 kind: Secret metadata: name: keycloak-client-<app-name> # Any descriptive name namespace: <app-namespace> # Your app's namespace (e.g., "myapp") labels: keycloak.forteapps.net/client-config: "true" # REQUIRED - triggers processing annotations: keycloak.forteapps.net/source-namespace: "myapp" # Optional - tracks ownership stringData: client.json: | # REQUIRED - the client configuration { "clientId": "myapp", # Pre-determined by developer "name": "My Application", # Display name in Keycloak UI "redirectUris": ["https://myapp.forteapps.net/*"], "webOrigins": ["https://myapp.forteapps.net"], "defaultClientScopes": ["openid", "email", "profile"], "protocolMappers": [], "secret": { "namespace": "myapp", # Where to create credential Secret "name": "myapp-oidc-credentials", # Name of credential Secret "keys": { "clientId": "client-id", # Key name for client ID "clientSecret": "client-secret" # Key name for client secret } } } ``` ### How It Works: Client ID vs Client Secret __Client ID (Pre-determined by Developer):__ - The developer chooses `clientId` (e.g., `"myapp"`) - This becomes the public identifier for the OIDC client - Used in OAuth flows, login redirects, token requests - Stored in the credential Secret under the specified key (default: `client-id`) __Client Secret (Generated by Keycloak):__ - Keycloak auto-generates a cryptographically secure secret - The CronJob fetches it via: `GET /admin/realms/forte/clients/{uuid}/client-secret` - The CronJob then creates/updates __TWO__ Secrets: __1. Target Namespace Secret__ (`myapp/myapp-oidc-credentials`): ```yaml apiVersion: v1 kind: Secret metadata: name: myapp-oidc-credentials namespace: myapp labels: app.kubernetes.io/managed-by: keycloak-client-registrar type: Opaque data: client-id: bXlhcHA= # base64("myapp") - from client.json client-secret: <base64> # Generated by Keycloak ``` __2. Central Backup Secret__ (`secrets/myapp-oidc-credentials`): - Identical content - Always created even if target namespace doesn't exist - Used for external deployments, disaster recovery, auditing ### Developer Workflow 1. __Create Config Secret__ in your Helm chart or Kustomize (in your app's namespace) 2. __Deploy__ - Kyverno policy clones it to `keycloak` namespace automatically 3. __Wait__ - CronJob picks it up within 2 minutes 4. __Reference__ the generated credential Secret in your Deployment: ```yaml env: - name: OIDC_CLIENT_ID valueFrom: secretKeyRef: name: myapp-oidc-credentials key: client-id - name: OIDC_CLIENT_SECRET valueFrom: secretKeyRef: name: myapp-oidc-credentials key: client-secret ``` ### Change Detection The CronJob computes a SHA256 hash of `client.json`. If unchanged and credential Secret exists, it skips processing. To force re-sync, modify any field in `client.json` (e.g., add trailing space to `name`).

View File

@@ -0,0 +1,7 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- forte-drop.yaml
- keycloak-client-forte-drop.yaml
# - forte-drop-secrets-sealed.yaml # added in follow-up commit
# - auth-oidc-sealed.yaml # added in follow-up commit (after Keycloak registrar creates client_secret)

View File

@@ -6,3 +6,5 @@ resources:
- musicman
- ts-mcp
- argo-mcp
- forte-drop
- forte-drop-mcp