# Bridge cluster mode on Kubernetes.
# Apply this manifest with kubectl after replacing the placeholder secrets and
# tenant values. The orchestrator owns migrations and spawns both session and
# proxy pods; the bridge-proxy Service routes to ready proxies.
# Session and proxy pods share the configured recordings volume so the proxy can
# play back recordings written by session workers.
---
apiVersion: v1
kind: Namespace
metadata:
  name: bridge
---
apiVersion: v1
kind: Secret
metadata:
  name: bridge-secrets
  namespace: bridge
stringData:
  # Datastore: Bridge reads the discrete BRIDGE_DATASTORE_* values (or a
  # server.datastore block in config.yaml), not a single connection URL. Host
  # points at the bridge-postgres Service below.
  BRIDGE_DATASTORE_HOST: "bridge-postgres"
  BRIDGE_DATASTORE_PORT: "5432"
  BRIDGE_DATASTORE_USER: "bridge"
  BRIDGE_DATASTORE_PASSWORD: "bridge"
  BRIDGE_DATASTORE_NAME: "bridge"
  BRIDGE_DATASTORE_SSLMODE: "disable"
  BRIDGE_ENCRYPTION_KEY_B64: "<ENCRYPTION_KEY_B64 from Step 2>"
  BRIDGE_CLUSTER_TOKEN: "<CLUSTER_TOKEN from Step 2>"
  BRIDGE_HOST_KEY_SEED: "<HOST_KEY_SEED from Step 2>"
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: bridge-config
  namespace: bridge
data:
  config.yaml: |
    server:
      listen: ":8080"
      tls:
        enabled: false
      # Recording settings live under server.
      recording:
        output_dir: /data/recordings
      # Web-UI login. Britive OAuth is the active method; set this to your tenant.
      # auth.types must validate: the default ["britive"] with no tenant is not
      # enough, so set the tenant before applying this manifest.
      #
      # No LDAP server is shipped here. To authenticate against your OWN LDAP, add
      # "ldap" to types and fill in the (commented) ldap block, pointing it at your
      # directory; set the bind password via the BRIDGE_LDAP_BIND_PASSWORD env.
      auth:
        types: [britive]
        britive:
          tenant: https://<your-tenant>.britive-app.com
          scopes: [openid]
          # redirect_url left unset → derived from the request (localhost:8888 via the
          # proxy Service). Set it explicitly if the OAuth callback must be pinned.
        # ldap:                                   # bring-your-own: also add "ldap" to types above
        #   server: ldap://your-ldap.example.com:389
        #   base_dn: ou=users,dc=example,dc=com
        #   bind_dn: cn=admin,dc=example,dc=com
        #   bind_password: ""                     # prefer the BRIDGE_LDAP_BIND_PASSWORD env
        #   user_filter: (uid={{username}})
    ssh:
      native: { enabled: true, listen: ":2222" }
      browser: { enabled: true }
    telnet: { native: { enabled: false }, browser: { enabled: false } }
    rdp: { native: { enabled: false }, browser: { enabled: false } }
    vnc: { browser: { enabled: false } }
    k8s_exec: { browser: { enabled: false } }
    cluster:
      session:
        max_concurrent_sessions: 1
        session_limit: 1         # ephemeral: one session then exit (watch the churn)
        lifespan: "0s"
      proxy:
        lifespan: "0s"            # 0 = no cycling. Set >0 (e.g. "1m" for a demo, "36h" prod) to
                                  # cycle proxies: spawn replacement → wait ready → retire → drain → terminate.
        drain_deadline: "30s"
        max_cycle_concurrency: 1
      orchestrator:
        warm_session_workers: 3  # keep 3 warm session pods ready
        proxy_workers: 1         # orchestrator owns proxy lifecycle (no static Deployment)
        reconcile: "5s"
        provisioner:
          platform: k8s
          path: "/opt/bridge/drivers/k8s/driver"
          k8s:
            image: "britive/bridge:latest"
            namespace: "bridge"
---
# --- Postgres (shared state) ---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: bridge-postgres
  namespace: bridge
spec:
  replicas: 1
  selector:
    matchLabels: { app: bridge-postgres }
  template:
    metadata:
      labels: { app: bridge-postgres }
    spec:
      containers:
      - name: postgres
        image: postgres:17-alpine
        env:
        - { name: POSTGRES_USER, value: bridge }
        - { name: POSTGRES_PASSWORD, value: bridge }
        - { name: POSTGRES_DB, value: bridge }
        ports:
        - containerPort: 5432
        readinessProbe:
          exec: { command: ["pg_isready", "-U", "bridge"] }
          periodSeconds: 2
---
apiVersion: v1
kind: Service
metadata:
  name: bridge-postgres
  namespace: bridge
spec:
  selector: { app: bridge-postgres }
  ports:
  - { port: 5432, targetPort: 5432 }
---
# --- RBAC: orchestrator manages session/proxy pods ---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: bridge-orchestrator
  namespace: bridge
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: bridge-orchestrator
  namespace: bridge
rules:
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["get", "list", "watch", "create", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: bridge-orchestrator
  namespace: bridge
subjects:
- { kind: ServiceAccount, name: bridge-orchestrator, namespace: bridge }
roleRef: { kind: Role, name: bridge-orchestrator, apiGroup: rbac.authorization.k8s.io }
---
# --- Orchestrator (owns migrations, leader, reconciler) ---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: bridge-orchestrator
  namespace: bridge
spec:
  replicas: 1
  selector:
    matchLabels: { app: bridge-orchestrator }
  template:
    metadata:
      labels: { app: bridge-orchestrator }
    spec:
      serviceAccountName: bridge-orchestrator
      containers:
      - name: bridge
        image: britive/bridge:latest
        imagePullPolicy: Always
        env:
        - { name: BRIDGE_ROLE, value: orchestrator }
        - { name: BRIDGE_NAMESPACE, value: bridge }
        envFrom:
        - secretRef: { name: bridge-secrets }
        ports:
        - { containerPort: 9091 }
        readinessProbe:
          httpGet: { path: /readyz, port: 9091 }
          periodSeconds: 5
        volumeMounts:
        - { name: config, mountPath: /etc/britive-bridge }
        # Read-only view of the shared recordings volume.
        - { name: recordings, mountPath: /data/recordings, readOnly: true }
      volumes:
      - name: config
        configMap: { name: bridge-config }
      - name: recordings
        hostPath: { path: /var/lib/bridge-recordings, type: DirectoryOrCreate }
---
# --- Proxy Service ---
# Proxies are spawned and cycled by the orchestrator, not defined as a static
# Deployment. This Service routes to the ready proxy pods.
apiVersion: v1
kind: Service
metadata:
  name: bridge-proxy
  namespace: bridge
spec:
  # LoadBalancer so it "just works" locally: Docker Desktop binds this to
  # localhost, so SSH on localhost:2222 and the web/UI on localhost:8080 are
  # reachable with no `kubectl port-forward` (like docker-compose port mappings).
  # On a real cloud the same type provisions an L4 load balancer (NLB on EKS) —
  # which is the right edge for raw TCP protocols like SSH (an L7 HTTP Ingress
  # can't carry them). On bare on-prem k8s with no LB controller, change this to
  # NodePort and front it with your own L4 LB/DNS.
  type: LoadBalancer
  selector: { bridge.role: proxy }
  ports:
  # Local access (Docker Desktop binds these to localhost): ssh → localhost:2222,
  # web/UI → localhost:8888. The web port is published as 8888 (not 8080) because
  # Docker Desktop shadows :8080 with its own listener and returns HTTP 400 before
  # it ever reaches the proxy — any other external port forwards cleanly. The pod
  # still serves the web tier on 8080 internally (targetPort). On a real cloud you
  # can use 8080 (or 443) here freely.
  - { name: ssh, port: 2222, targetPort: 2222 }
  - { name: web, port: 8888, targetPort: 8080 }
---
# --- Test SSH backend (the target a session worker dials) ---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: test-sshd
  namespace: bridge
spec:
  replicas: 1
  selector:
    matchLabels: { app: test-sshd }
  template:
    metadata:
      labels: { app: test-sshd }
    spec:
      containers:
      - name: sshd
        image: bridge-test-sshd:cluster
        imagePullPolicy: Always
        ports:
        - { containerPort: 10022 }
---
apiVersion: v1
kind: Service
metadata:
  name: test-sshd
  namespace: bridge
spec:
  selector: { app: test-sshd }
  ports:
  - { port: 10022, targetPort: 10022 }
