diff --git a/apps-gx10/fc-gateway/fc-gateway.yaml b/apps-gx10/fc-gateway/fc-gateway.yaml new file mode 100644 index 0000000..7bbe658 --- /dev/null +++ b/apps-gx10/fc-gateway/fc-gateway.yaml @@ -0,0 +1,302 @@ +# FlowerCore MCP Gateway for the GX10 cluster. +# Secret values are copied into Kubernetes Secrets out of band until the +# 1Password operator exists on GX10; never commit secret data here. +--- +apiVersion: v1 +kind: Namespace +metadata: + name: fc-gateway + labels: + app.kubernetes.io/part-of: flowercore +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: fc-gateway + namespace: fc-gateway + labels: + app.kubernetes.io/name: fc-gateway + app.kubernetes.io/part-of: flowercore +spec: + replicas: 1 + revisionHistoryLimit: 3 + strategy: + type: Recreate + selector: + matchLabels: + app.kubernetes.io/name: fc-gateway + template: + metadata: + labels: + app.kubernetes.io/name: fc-gateway + app.kubernetes.io/part-of: flowercore + annotations: + fc.flowercore.io/healthz-anon: "true" + fc.flowercore.io/probe-path: "/healthz" + prometheus.io/scrape: "true" + prometheus.io/port: "8080" + prometheus.io/path: "/metrics/prometheus" + spec: + securityContext: + runAsNonRoot: true + runAsUser: 1654 + runAsGroup: 1654 + fsGroup: 1654 + fsGroupChangePolicy: OnRootMismatch + containers: + - name: web + image: localhost/fc-gateway:v20260617-hm1-gateway-e0627e3 + imagePullPolicy: Never + ports: + - containerPort: 8080 + name: http + env: + - name: ASPNETCORE_URLS + value: "http://+:8080" + - name: ASPNETCORE_ENVIRONMENT + value: "Production" + - name: DOTNET_SYSTEM_GLOBALIZATION_INVARIANT + value: "false" + - name: FlowerCore__Mcp__ApiKey__Key + valueFrom: + secretKeyRef: + name: gateway-mcp-keys + key: credential + - name: FlowerCore__Mcp__Gateway__Embedding__BaseUrl + value: "http://fc-llm-bridge.fc-llm-bridge.svc:8080/v1" + - name: FlowerCore__Mcp__Gateway__Embedding__Model + value: "fc:embedding" + - name: FlowerCore__Mcp__Gateway__Embedding__Mode + value: "openai" + - name: FlowerCore__Mcp__Gateway__Embedding__ApiKey + valueFrom: + secretKeyRef: + name: fc-llm-bridge-api-keys + key: agent-zero-k8s + optional: true + - name: GW_BACKEND_fc_mysql_KEY + valueFrom: + secretKeyRef: + name: mysql-mcp-keys + key: credential + optional: true + - name: GW_BACKEND_fc_php_KEY + valueFrom: + secretKeyRef: + name: php-mcp-keys + key: credential + optional: true + - name: GW_BACKEND_fc_telephony_KEY + valueFrom: + secretKeyRef: + name: telephony-mcp-keys + key: credential + optional: true + - name: GW_BACKEND_fc_chat_KEY + valueFrom: + secretKeyRef: + name: chat-mcp-keys + key: credential + optional: true + - name: GW_BACKEND_fc_dms_KEY + valueFrom: + secretKeyRef: + name: dms-mcp-keys + key: credential + optional: true + - name: GW_BACKEND_fc_knowledge_KEY + valueFrom: + secretKeyRef: + name: knowledge-mcp-tokens + key: password + optional: true + resources: + requests: + cpu: 50m + memory: 128Mi + limits: + cpu: 500m + memory: 384Mi + volumeMounts: + - name: tmp + mountPath: /tmp + - name: logs + mountPath: /home/app/logs + securityContext: + runAsNonRoot: true + runAsUser: 1654 + runAsGroup: 1654 + readOnlyRootFilesystem: true + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + startupProbe: + httpGet: + path: /healthz + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 5 + failureThreshold: 30 + readinessProbe: + httpGet: + path: /healthz + port: 8080 + periodSeconds: 10 + livenessProbe: + tcpSocket: + port: 8080 + initialDelaySeconds: 30 + periodSeconds: 30 + volumes: + - name: tmp + emptyDir: {} + - name: logs + emptyDir: {} +--- +apiVersion: v1 +kind: Service +metadata: + name: fc-gateway + namespace: fc-gateway +spec: + type: ClusterIP + selector: + app.kubernetes.io/name: fc-gateway + ports: + - name: http + port: 80 + targetPort: 8080 + protocol: TCP +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: fc-gateway-tls + namespace: fc-gateway +spec: + secretName: fc-gateway-tls + issuerRef: + name: step-ca-acme + kind: ClusterIssuer + dnsNames: + - gateway.iamworkin.lan + duration: 720h + renewBefore: 240h +--- +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: fc-gateway + namespace: fc-gateway +spec: + entryPoints: + - websecure + routes: + - match: Host(`gateway.iamworkin.lan`) + kind: Rule + services: + - name: fc-gateway + port: 80 + tls: + secretName: fc-gateway-tls +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: fc-gateway-netpol + namespace: fc-gateway +spec: + podSelector: + matchLabels: + app.kubernetes.io/name: fc-gateway + policyTypes: + - Ingress + - Egress + ingress: + - from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: agent-zero + ports: + - port: 8080 + protocol: TCP + - from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: traefik-system + ports: + - port: 8080 + protocol: TCP + - from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: monitoring + ports: + - port: 8080 + protocol: TCP + egress: + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + ports: + - port: 53 + protocol: UDP + - port: 53 + protocol: TCP + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: fc-llm-bridge + ports: + - port: 8080 + protocol: TCP + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: fc-mysql + ports: + - port: 5300 + protocol: TCP + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: fc-php + ports: + - port: 5400 + protocol: TCP + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: telephony + ports: + - port: 5100 + protocol: TCP + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: fc-chat + ports: + - port: 80 + protocol: TCP + - port: 8080 + protocol: TCP + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: fc-dms + ports: + - port: 80 + protocol: TCP + - port: 8080 + protocol: TCP + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: knowledge + ports: + - port: 80 + protocol: TCP + - port: 8080 + protocol: TCP diff --git a/apps-gx10/fc-mysql/deployment-mysql-web.json b/apps-gx10/fc-mysql/deployment-mysql-web.json index 0ef0528..bf5fadf 100644 --- a/apps-gx10/fc-mysql/deployment-mysql-web.json +++ b/apps-gx10/fc-mysql/deployment-mysql-web.json @@ -37,14 +37,24 @@ "containers": [ { "env": [ - { - "name": "FlowerCore__Auth__Enabled", - "value": "false" - }, - { - "name": "FlowerCore__Auth__Oidc__Authority", - "valueFrom": { - "secretKeyRef": { + { + "name": "FlowerCore__Auth__Enabled", + "value": "false" + }, + { + "name": "FlowerCore__Mcp__ApiKey__Key", + "valueFrom": { + "secretKeyRef": { + "key": "credential", + "name": "mysql-mcp-keys", + "optional": true + } + } + }, + { + "name": "FlowerCore__Auth__Oidc__Authority", + "valueFrom": { + "secretKeyRef": { "key": "issuer_url", "name": "mysql-oidc-client", "optional": true diff --git a/apps-gx10/fc-php/deployment-php-web.json b/apps-gx10/fc-php/deployment-php-web.json index 68c138c..2b07acd 100644 --- a/apps-gx10/fc-php/deployment-php-web.json +++ b/apps-gx10/fc-php/deployment-php-web.json @@ -37,14 +37,24 @@ "containers": [ { "env": [ - { - "name": "FlowerCore__Auth__Enabled", - "value": "false" - }, - { - "name": "FlowerCore__Auth__Oidc__Authority", - "valueFrom": { - "secretKeyRef": { + { + "name": "FlowerCore__Auth__Enabled", + "value": "false" + }, + { + "name": "FlowerCore__Mcp__ApiKey__Key", + "valueFrom": { + "secretKeyRef": { + "key": "credential", + "name": "php-mcp-keys", + "optional": true + } + } + }, + { + "name": "FlowerCore__Auth__Oidc__Authority", + "valueFrom": { + "secretKeyRef": { "key": "issuer_url", "name": "php-oidc-client", "optional": true diff --git a/tests/bluejay-infra-lint/FleetManifestLintTests.cs b/tests/bluejay-infra-lint/FleetManifestLintTests.cs index e369d74..77a2659 100644 --- a/tests/bluejay-infra-lint/FleetManifestLintTests.cs +++ b/tests/bluejay-infra-lint/FleetManifestLintTests.cs @@ -1,4 +1,5 @@ using FluentAssertions; +using System.Text.Json; using System.Text.RegularExpressions; using Xunit; using YamlDotNet.Core; @@ -949,6 +950,37 @@ public sealed class FleetManifestLintTests EnvSecretOptional(webhook, "FlowerCore__Dns__AcmeWebhook__ApiKey").Should().Be("true"); } + [Fact] + public void Gx10HostingManagers_AreWiredBehindMcpGateway() + { + var mysqlWeb = Gx10DeploymentContainer("fc-mysql", "deployment-mysql-web.json"); + JsonEnvSecretName(mysqlWeb, "FlowerCore__Mcp__ApiKey__Key").Should().Be("mysql-mcp-keys"); + JsonEnvSecretKey(mysqlWeb, "FlowerCore__Mcp__ApiKey__Key").Should().Be("credential"); + JsonEnvSecretOptional(mysqlWeb, "FlowerCore__Mcp__ApiKey__Key").Should().BeTrue(); + + var phpWeb = Gx10DeploymentContainer("fc-php", "deployment-php-web.json"); + JsonEnvSecretName(phpWeb, "FlowerCore__Mcp__ApiKey__Key").Should().Be("php-mcp-keys"); + JsonEnvSecretKey(phpWeb, "FlowerCore__Mcp__ApiKey__Key").Should().Be("credential"); + JsonEnvSecretOptional(phpWeb, "FlowerCore__Mcp__ApiKey__Key").Should().BeTrue(); + + var gatewayManifest = File.ReadAllText(Path.Combine( + Inventory.BluejayRoot, + "apps-gx10", + "fc-gateway", + "fc-gateway.yaml")); + + gatewayManifest.Should().Contain("Host(`gateway.iamworkin.lan`)"); + gatewayManifest.Should().Contain("name: FlowerCore__Mcp__ApiKey__Key"); + gatewayManifest.Should().Contain("name: GW_BACKEND_fc_mysql_KEY"); + gatewayManifest.Should().Contain("name: mysql-mcp-keys"); + gatewayManifest.Should().Contain("name: GW_BACKEND_fc_php_KEY"); + gatewayManifest.Should().Contain("name: php-mcp-keys"); + gatewayManifest.Should().Contain("kubernetes.io/metadata.name: fc-mysql"); + gatewayManifest.Should().Contain("port: 5300"); + gatewayManifest.Should().Contain("kubernetes.io/metadata.name: fc-php"); + gatewayManifest.Should().Contain("port: 5400"); + } + [Fact] public void DnsAndMediaGitOpsAdoption_PreservesLiveStorageAndImageShape() { @@ -1096,6 +1128,52 @@ public sealed class FleetManifestLintTests : null; } + private static JsonElement Gx10DeploymentContainer(string app, string fileName) + { + var path = Path.Combine(Inventory.BluejayRoot, "apps-gx10", app, fileName); + using var document = JsonDocument.Parse(File.ReadAllText(path)); + return document.RootElement + .GetProperty("spec") + .GetProperty("template") + .GetProperty("spec") + .GetProperty("containers")[0] + .Clone(); + } + + private static string? JsonEnvSecretName(JsonElement container, string name) + { + return JsonEnvMapping(container, name) is { } env + ? env.GetProperty("valueFrom").GetProperty("secretKeyRef").GetProperty("name").GetString() + : null; + } + + private static string? JsonEnvSecretKey(JsonElement container, string name) + { + return JsonEnvMapping(container, name) is { } env + ? env.GetProperty("valueFrom").GetProperty("secretKeyRef").GetProperty("key").GetString() + : null; + } + + private static bool? JsonEnvSecretOptional(JsonElement container, string name) + { + return JsonEnvMapping(container, name) is { } env + ? env.GetProperty("valueFrom").GetProperty("secretKeyRef").GetProperty("optional").GetBoolean() + : null; + } + + private static JsonElement? JsonEnvMapping(JsonElement container, string name) + { + foreach (var env in container.GetProperty("env").EnumerateArray()) + { + if (string.Equals(env.GetProperty("name").GetString(), name, StringComparison.Ordinal)) + { + return env.Clone(); + } + } + + return null; + } + private static string? ProbePath(YamlMappingNode container, string probeKey) { return ManifestNodeExtensions.Scalar(container, probeKey, "httpGet", "path");