@intentius/chant-lexicon-k8s 0.0.15 → 0.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -15,6 +15,7 @@ import { MonitoredService } from "./monitored-service";
15
15
  import { NetworkIsolatedApp } from "./network-isolated-app";
16
16
  import { IrsaServiceAccount } from "./irsa-service-account";
17
17
  import { AlbIngress } from "./alb-ingress";
18
+ import { GceIngress } from "./gce-ingress";
18
19
  import { EbsStorageClass } from "./ebs-storage-class";
19
20
  import { EfsStorageClass } from "./efs-storage-class";
20
21
  import { FluentBitAgent } from "./fluent-bit-agent";
@@ -1860,6 +1861,79 @@ describe("AlbIngress", () => {
1860
1861
  });
1861
1862
  });
1862
1863
 
1864
+ // ── GceIngress ──────────────────────────────────────────────────────
1865
+
1866
+ describe("GceIngress", () => {
1867
+ const minProps = {
1868
+ name: "api-ingress",
1869
+ hosts: [{ hostname: "api.example.com", paths: [{ path: "/", serviceName: "api", servicePort: 80 }] }],
1870
+ };
1871
+
1872
+ test("returns ingress with GCE annotations", () => {
1873
+ const result = GceIngress(minProps);
1874
+ expect(result.ingress).toBeDefined();
1875
+ const meta = result.ingress.metadata as any;
1876
+ expect(meta.annotations["kubernetes.io/ingress.class"]).toBe("gce");
1877
+ });
1878
+
1879
+ test("static IP annotation set", () => {
1880
+ const result = GceIngress({ ...minProps, staticIpName: "microservice-ip" });
1881
+ const meta = result.ingress.metadata as any;
1882
+ expect(meta.annotations["kubernetes.io/ingress.global-static-ip-name"]).toBe("microservice-ip");
1883
+ });
1884
+
1885
+ test("managed certificate annotation set", () => {
1886
+ const result = GceIngress({ ...minProps, managedCertificate: "api-cert" });
1887
+ const meta = result.ingress.metadata as any;
1888
+ expect(meta.annotations["networking.gke.io/managed-certificates"]).toBe("api-cert");
1889
+ });
1890
+
1891
+ test("frontendConfig annotation set", () => {
1892
+ const result = GceIngress({ ...minProps, frontendConfig: "my-frontend" });
1893
+ const meta = result.ingress.metadata as any;
1894
+ expect(meta.annotations["networking.gke.io/v1beta1.FrontendConfig"]).toBe("my-frontend");
1895
+ });
1896
+
1897
+ test("managedCertificate sets default FrontendConfig for ssl redirect", () => {
1898
+ const result = GceIngress({ ...minProps, managedCertificate: "api-cert" });
1899
+ const meta = result.ingress.metadata as any;
1900
+ expect(meta.annotations["networking.gke.io/v1beta1.FrontendConfig"]).toBe("api-ingress-frontend-config");
1901
+ });
1902
+
1903
+ test("explicit sslRedirect: false suppresses FrontendConfig even with cert", () => {
1904
+ const result = GceIngress({ ...minProps, managedCertificate: "api-cert", sslRedirect: false });
1905
+ const meta = result.ingress.metadata as any;
1906
+ expect(meta.annotations["networking.gke.io/v1beta1.FrontendConfig"]).toBeUndefined();
1907
+ });
1908
+
1909
+ test("explicit sslRedirect: true sets default FrontendConfig without cert", () => {
1910
+ const result = GceIngress({ ...minProps, sslRedirect: true });
1911
+ const meta = result.ingress.metadata as any;
1912
+ expect(meta.annotations["networking.gke.io/v1beta1.FrontendConfig"]).toBe("api-ingress-frontend-config");
1913
+ });
1914
+
1915
+ test("namespace is set when provided", () => {
1916
+ const result = GceIngress({ ...minProps, namespace: "production" });
1917
+ const meta = result.ingress.metadata as any;
1918
+ expect(meta.namespace).toBe("production");
1919
+ });
1920
+
1921
+ test("host rules are mapped correctly", () => {
1922
+ const result = GceIngress(minProps);
1923
+ const spec = result.ingress.spec as any;
1924
+ expect(spec.rules).toHaveLength(1);
1925
+ expect(spec.rules[0].host).toBe("api.example.com");
1926
+ expect(spec.rules[0].http.paths[0].backend.service.name).toBe("api");
1927
+ });
1928
+
1929
+ test("labels include component: ingress", () => {
1930
+ const result = GceIngress(minProps);
1931
+ const meta = result.ingress.metadata as any;
1932
+ expect(meta.labels["app.kubernetes.io/component"]).toBe("ingress");
1933
+ expect(meta.labels["app.kubernetes.io/managed-by"]).toBe("chant");
1934
+ });
1935
+ });
1936
+
1863
1937
  // ── EbsStorageClass ─────────────────────────────────────────────────
1864
1938
 
1865
1939
  describe("EbsStorageClass", () => {
@@ -2691,3 +2765,455 @@ describe("ConfigConnectorContext", () => {
2691
2765
  expect((result.context as any).metadata.namespace).toBe("config-connector");
2692
2766
  });
2693
2767
  });
2768
+
2769
+ // ── GkeFluentBitAgent ────────────────────────────────────────────────
2770
+
2771
+ describe("GkeFluentBitAgent", () => {
2772
+ const { GkeFluentBitAgent } = require("./gke-fluent-bit-agent");
2773
+
2774
+ const minProps = {
2775
+ clusterName: "test-cluster",
2776
+ projectId: "test-project",
2777
+ gcpServiceAccountEmail: "fluent-bit@test-project.iam.gserviceaccount.com",
2778
+ };
2779
+
2780
+ test("returns all expected resources", () => {
2781
+ const result = GkeFluentBitAgent(minProps);
2782
+ expect(result.daemonSet).toBeDefined();
2783
+ expect(result.serviceAccount).toBeDefined();
2784
+ expect(result.clusterRole).toBeDefined();
2785
+ expect(result.clusterRoleBinding).toBeDefined();
2786
+ expect(result.configMap).toBeDefined();
2787
+ });
2788
+
2789
+ test("SA has GKE Workload Identity annotation", () => {
2790
+ const result = GkeFluentBitAgent(minProps);
2791
+ const meta = result.serviceAccount.metadata as any;
2792
+ expect(meta.annotations["iam.gke.io/gcp-service-account"]).toBe(
2793
+ "fluent-bit@test-project.iam.gserviceaccount.com",
2794
+ );
2795
+ });
2796
+
2797
+ test("SA has no annotation when gcpServiceAccountEmail omitted", () => {
2798
+ const result = GkeFluentBitAgent({
2799
+ clusterName: "test-cluster",
2800
+ projectId: "test-project",
2801
+ });
2802
+ const meta = result.serviceAccount.metadata as any;
2803
+ expect(meta.annotations).toBeUndefined();
2804
+ });
2805
+
2806
+ test("configMap uses stackdriver output", () => {
2807
+ const result = GkeFluentBitAgent(minProps);
2808
+ const data = result.configMap.data as any;
2809
+ expect(data["fluent-bit.conf"]).toContain("stackdriver");
2810
+ expect(data["fluent-bit.conf"]).toContain("test-cluster");
2811
+ });
2812
+
2813
+ test("default namespace is gke-logging", () => {
2814
+ const result = GkeFluentBitAgent(minProps);
2815
+ expect((result.daemonSet.metadata as any).namespace).toBe("gke-logging");
2816
+ expect((result.serviceAccount.metadata as any).namespace).toBe("gke-logging");
2817
+ });
2818
+
2819
+ test("common labels on all resources", () => {
2820
+ const result = GkeFluentBitAgent(minProps);
2821
+ for (const resource of [result.daemonSet, result.serviceAccount, result.clusterRole, result.clusterRoleBinding, result.configMap]) {
2822
+ expect((resource.metadata as any).labels["app.kubernetes.io/managed-by"]).toBe("chant");
2823
+ }
2824
+ });
2825
+
2826
+ test("DaemonSet mounts host log directory", () => {
2827
+ const result = GkeFluentBitAgent(minProps);
2828
+ const spec = (result.daemonSet as any).spec.template.spec;
2829
+ const varlog = spec.volumes.find((v: any) => v.name === "varlog");
2830
+ expect(varlog.hostPath.path).toBe("/var/log");
2831
+ });
2832
+
2833
+ test("container runs as root for log access", () => {
2834
+ const result = GkeFluentBitAgent(minProps);
2835
+ const container = (result.daemonSet as any).spec.template.spec.containers[0];
2836
+ expect(container.securityContext.runAsUser).toBe(0);
2837
+ expect(container.securityContext.readOnlyRootFilesystem).toBe(true);
2838
+ });
2839
+ });
2840
+
2841
+ // ── GkeOtelCollector ─────────────────────────────────────────────────
2842
+
2843
+ describe("GkeOtelCollector", () => {
2844
+ const { GkeOtelCollector } = require("./gke-otel-collector");
2845
+
2846
+ const minProps = {
2847
+ clusterName: "test-cluster",
2848
+ projectId: "test-project",
2849
+ gcpServiceAccountEmail: "otel@test-project.iam.gserviceaccount.com",
2850
+ };
2851
+
2852
+ test("returns all expected resources", () => {
2853
+ const result = GkeOtelCollector(minProps);
2854
+ expect(result.daemonSet).toBeDefined();
2855
+ expect(result.serviceAccount).toBeDefined();
2856
+ expect(result.clusterRole).toBeDefined();
2857
+ expect(result.clusterRoleBinding).toBeDefined();
2858
+ expect(result.configMap).toBeDefined();
2859
+ });
2860
+
2861
+ test("SA has GKE Workload Identity annotation", () => {
2862
+ const result = GkeOtelCollector(minProps);
2863
+ const meta = result.serviceAccount.metadata as any;
2864
+ expect(meta.annotations["iam.gke.io/gcp-service-account"]).toBe(
2865
+ "otel@test-project.iam.gserviceaccount.com",
2866
+ );
2867
+ });
2868
+
2869
+ test("SA has no annotation when gcpServiceAccountEmail omitted", () => {
2870
+ const result = GkeOtelCollector({
2871
+ clusterName: "test-cluster",
2872
+ projectId: "test-project",
2873
+ });
2874
+ const meta = result.serviceAccount.metadata as any;
2875
+ expect(meta.annotations).toBeUndefined();
2876
+ });
2877
+
2878
+ test("configMap uses googlecloud exporter", () => {
2879
+ const result = GkeOtelCollector(minProps);
2880
+ const data = result.configMap.data as any;
2881
+ expect(data["config.yaml"]).toContain("googlecloud");
2882
+ expect(data["config.yaml"]).toContain("test-project");
2883
+ });
2884
+
2885
+ test("default namespace is gke-monitoring", () => {
2886
+ const result = GkeOtelCollector(minProps);
2887
+ expect((result.daemonSet.metadata as any).namespace).toBe("gke-monitoring");
2888
+ });
2889
+
2890
+ test("container exposes OTLP ports", () => {
2891
+ const result = GkeOtelCollector(minProps);
2892
+ const container = (result.daemonSet as any).spec.template.spec.containers[0];
2893
+ const ports = container.ports.map((p: any) => p.containerPort);
2894
+ expect(ports).toContain(4317);
2895
+ expect(ports).toContain(4318);
2896
+ });
2897
+
2898
+ test("container runs as non-root", () => {
2899
+ const result = GkeOtelCollector(minProps);
2900
+ const container = (result.daemonSet as any).spec.template.spec.containers[0];
2901
+ expect(container.securityContext.runAsNonRoot).toBe(true);
2902
+ expect(container.securityContext.runAsUser).toBe(10001);
2903
+ });
2904
+
2905
+ test("common labels on all resources", () => {
2906
+ const result = GkeOtelCollector(minProps);
2907
+ for (const resource of [result.daemonSet, result.serviceAccount, result.clusterRole, result.clusterRoleBinding, result.configMap]) {
2908
+ expect((resource.metadata as any).labels["app.kubernetes.io/managed-by"]).toBe("chant");
2909
+ }
2910
+ });
2911
+ });
2912
+
2913
+ // ── GkeExternalDnsAgent ──────────────────────────────────────────────
2914
+
2915
+ describe("GkeExternalDnsAgent", () => {
2916
+ const { GkeExternalDnsAgent } = require("./gke-external-dns-agent");
2917
+
2918
+ const minProps = {
2919
+ gcpServiceAccountEmail: "external-dns@test-project.iam.gserviceaccount.com",
2920
+ gcpProjectId: "test-project",
2921
+ domainFilters: ["example.com"],
2922
+ };
2923
+
2924
+ test("returns all expected resources", () => {
2925
+ const result = GkeExternalDnsAgent(minProps);
2926
+ expect(result.deployment).toBeDefined();
2927
+ expect(result.serviceAccount).toBeDefined();
2928
+ expect(result.clusterRole).toBeDefined();
2929
+ expect(result.clusterRoleBinding).toBeDefined();
2930
+ });
2931
+
2932
+ test("SA has GKE Workload Identity annotation", () => {
2933
+ const result = GkeExternalDnsAgent(minProps);
2934
+ const meta = result.serviceAccount.metadata as any;
2935
+ expect(meta.annotations["iam.gke.io/gcp-service-account"]).toBe(
2936
+ "external-dns@test-project.iam.gserviceaccount.com",
2937
+ );
2938
+ });
2939
+
2940
+ test("uses google provider", () => {
2941
+ const result = GkeExternalDnsAgent(minProps);
2942
+ const container = (result.deployment as any).spec.template.spec.containers[0];
2943
+ expect(container.args).toContain("--provider=google");
2944
+ });
2945
+
2946
+ test("passes google-project arg", () => {
2947
+ const result = GkeExternalDnsAgent(minProps);
2948
+ const container = (result.deployment as any).spec.template.spec.containers[0];
2949
+ expect(container.args).toContain("--google-project=test-project");
2950
+ });
2951
+
2952
+ test("domain filters are applied", () => {
2953
+ const result = GkeExternalDnsAgent(minProps);
2954
+ const container = (result.deployment as any).spec.template.spec.containers[0];
2955
+ expect(container.args).toContain("--domain-filter=example.com");
2956
+ });
2957
+
2958
+ test("txtOwnerId is applied when set", () => {
2959
+ const result = GkeExternalDnsAgent({ ...minProps, txtOwnerId: "my-cluster" });
2960
+ const container = (result.deployment as any).spec.template.spec.containers[0];
2961
+ expect(container.args).toContain("--txt-owner-id=my-cluster");
2962
+ });
2963
+
2964
+ test("default namespace is kube-system", () => {
2965
+ const result = GkeExternalDnsAgent(minProps);
2966
+ expect((result.deployment.metadata as any).namespace).toBe("kube-system");
2967
+ });
2968
+
2969
+ test("container runs as non-root", () => {
2970
+ const result = GkeExternalDnsAgent(minProps);
2971
+ const container = (result.deployment as any).spec.template.spec.containers[0];
2972
+ expect(container.securityContext.runAsNonRoot).toBe(true);
2973
+ expect(container.securityContext.runAsUser).toBe(65534);
2974
+ });
2975
+ });
2976
+
2977
+ // ── AksExternalDnsAgent ──────────────────────────────────────────────
2978
+
2979
+ describe("AksExternalDnsAgent", () => {
2980
+ const { AksExternalDnsAgent } = require("./aks-external-dns-agent");
2981
+
2982
+ const minProps = {
2983
+ clientId: "00000000-0000-0000-0000-000000000000",
2984
+ resourceGroup: "test-rg",
2985
+ subscriptionId: "11111111-1111-1111-1111-111111111111",
2986
+ tenantId: "22222222-2222-2222-2222-222222222222",
2987
+ domainFilters: ["example.com"],
2988
+ };
2989
+
2990
+ test("returns all expected resources", () => {
2991
+ const result = AksExternalDnsAgent(minProps);
2992
+ expect(result.deployment).toBeDefined();
2993
+ expect(result.serviceAccount).toBeDefined();
2994
+ expect(result.clusterRole).toBeDefined();
2995
+ expect(result.clusterRoleBinding).toBeDefined();
2996
+ });
2997
+
2998
+ test("SA has AKS Workload Identity annotation and label", () => {
2999
+ const result = AksExternalDnsAgent(minProps);
3000
+ const meta = result.serviceAccount.metadata as any;
3001
+ expect(meta.annotations["azure.workload.identity/client-id"]).toBe(
3002
+ "00000000-0000-0000-0000-000000000000",
3003
+ );
3004
+ expect(meta.labels["azure.workload.identity/use"]).toBe("true");
3005
+ });
3006
+
3007
+ test("uses azure provider", () => {
3008
+ const result = AksExternalDnsAgent(minProps);
3009
+ const container = (result.deployment as any).spec.template.spec.containers[0];
3010
+ expect(container.args).toContain("--provider=azure");
3011
+ });
3012
+
3013
+ test("passes azure resource group and subscription", () => {
3014
+ const result = AksExternalDnsAgent(minProps);
3015
+ const container = (result.deployment as any).spec.template.spec.containers[0];
3016
+ expect(container.args).toContain("--azure-resource-group=test-rg");
3017
+ expect(container.args).toContain("--azure-subscription-id=11111111-1111-1111-1111-111111111111");
3018
+ });
3019
+
3020
+ test("container has Azure env vars", () => {
3021
+ const result = AksExternalDnsAgent(minProps);
3022
+ const container = (result.deployment as any).spec.template.spec.containers[0];
3023
+ const envMap = Object.fromEntries(container.env.map((e: any) => [e.name, e.value]));
3024
+ expect(envMap.AZURE_TENANT_ID).toBe("22222222-2222-2222-2222-222222222222");
3025
+ expect(envMap.AZURE_SUBSCRIPTION_ID).toBe("11111111-1111-1111-1111-111111111111");
3026
+ expect(envMap.AZURE_RESOURCE_GROUP).toBe("test-rg");
3027
+ });
3028
+
3029
+ test("pod labels include workload identity use label", () => {
3030
+ const result = AksExternalDnsAgent(minProps);
3031
+ const podLabels = (result.deployment as any).spec.template.metadata.labels;
3032
+ expect(podLabels["azure.workload.identity/use"]).toBe("true");
3033
+ });
3034
+
3035
+ test("domain filters are applied", () => {
3036
+ const result = AksExternalDnsAgent(minProps);
3037
+ const container = (result.deployment as any).spec.template.spec.containers[0];
3038
+ expect(container.args).toContain("--domain-filter=example.com");
3039
+ });
3040
+
3041
+ test("default namespace is kube-system", () => {
3042
+ const result = AksExternalDnsAgent(minProps);
3043
+ expect((result.deployment.metadata as any).namespace).toBe("kube-system");
3044
+ });
3045
+
3046
+ test("container runs as non-root", () => {
3047
+ const result = AksExternalDnsAgent(minProps);
3048
+ const container = (result.deployment as any).spec.template.spec.containers[0];
3049
+ expect(container.securityContext.runAsNonRoot).toBe(true);
3050
+ expect(container.securityContext.runAsUser).toBe(65534);
3051
+ });
3052
+ });
3053
+
3054
+ // ── CockroachDbCluster ──────────────────────────────────────────────
3055
+
3056
+ describe("CockroachDbCluster", () => {
3057
+ const { CockroachDbCluster } = require("./cockroachdb-cluster");
3058
+
3059
+ const minProps = { name: "cockroachdb" };
3060
+
3061
+ test("returns all expected resources", () => {
3062
+ const result = CockroachDbCluster(minProps);
3063
+ expect(result.serviceAccount).toBeDefined();
3064
+ expect(result.role).toBeDefined();
3065
+ expect(result.roleBinding).toBeDefined();
3066
+ expect(result.clusterRole).toBeDefined();
3067
+ expect(result.clusterRoleBinding).toBeDefined();
3068
+ expect(result.publicService).toBeDefined();
3069
+ expect(result.headlessService).toBeDefined();
3070
+ expect(result.pdb).toBeDefined();
3071
+ expect(result.statefulSet).toBeDefined();
3072
+ expect(result.initJob).toBeDefined();
3073
+ expect(result.certGenJob).toBeDefined();
3074
+ });
3075
+
3076
+ test("default replicas is 3", () => {
3077
+ const result = CockroachDbCluster(minProps);
3078
+ const spec = result.statefulSet.spec as any;
3079
+ expect(spec.replicas).toBe(3);
3080
+ });
3081
+
3082
+ test("default image is cockroachdb/cockroach:v24.3.0", () => {
3083
+ const result = CockroachDbCluster(minProps);
3084
+ const container = (result.statefulSet.spec as any).template.spec.containers[0];
3085
+ expect(container.image).toBe("cockroachdb/cockroach:v24.3.0");
3086
+ });
3087
+
3088
+ test("StatefulSet has correct ports (26257+8080)", () => {
3089
+ const result = CockroachDbCluster(minProps);
3090
+ const container = (result.statefulSet.spec as any).template.spec.containers[0];
3091
+ const ports = container.ports.map((p: any) => p.containerPort);
3092
+ expect(ports).toContain(26257);
3093
+ expect(ports).toContain(8080);
3094
+ });
3095
+
3096
+ test("StatefulSet has PVC with default 100Gi storage", () => {
3097
+ const result = CockroachDbCluster(minProps);
3098
+ const vct = (result.statefulSet.spec as any).volumeClaimTemplates[0];
3099
+ expect(vct.spec.resources.requests.storage).toBe("100Gi");
3100
+ expect(vct.spec.accessModes).toEqual(["ReadWriteOnce"]);
3101
+ });
3102
+
3103
+ test("headless service has clusterIP None and publishNotReadyAddresses", () => {
3104
+ const result = CockroachDbCluster(minProps);
3105
+ const spec = result.headlessService.spec as any;
3106
+ expect(spec.clusterIP).toBe("None");
3107
+ expect(spec.publishNotReadyAddresses).toBe(true);
3108
+ });
3109
+
3110
+ test("public service has ClusterIP type with both ports", () => {
3111
+ const result = CockroachDbCluster(minProps);
3112
+ const spec = result.publicService.spec as any;
3113
+ expect(spec.type).toBe("ClusterIP");
3114
+ const ports = spec.ports.map((p: any) => p.port);
3115
+ expect(ports).toContain(26257);
3116
+ expect(ports).toContain(8080);
3117
+ });
3118
+
3119
+ test("PDB has maxUnavailable 1", () => {
3120
+ const result = CockroachDbCluster(minProps);
3121
+ const spec = result.pdb.spec as any;
3122
+ expect(spec.maxUnavailable).toBe(1);
3123
+ });
3124
+
3125
+ test("StatefulSet has pod anti-affinity", () => {
3126
+ const result = CockroachDbCluster(minProps);
3127
+ const affinity = (result.statefulSet.spec as any).template.spec.affinity;
3128
+ expect(affinity.podAntiAffinity).toBeDefined();
3129
+ });
3130
+
3131
+ test("props flow through (replicas, image, storage)", () => {
3132
+ const result = CockroachDbCluster({
3133
+ name: "crdb",
3134
+ replicas: 5,
3135
+ image: "cockroachdb/cockroach:v23.2.0",
3136
+ storageSize: "200Gi",
3137
+ });
3138
+ const spec = result.statefulSet.spec as any;
3139
+ expect(spec.replicas).toBe(5);
3140
+ expect(spec.template.spec.containers[0].image).toBe("cockroachdb/cockroach:v23.2.0");
3141
+ expect(spec.volumeClaimTemplates[0].spec.resources.requests.storage).toBe("200Gi");
3142
+ });
3143
+
3144
+ test("joinAddresses appear in container args", () => {
3145
+ const joins = ["crdb-0.crdb.ns.svc.cluster.local", "crdb-1.crdb.ns.svc.cluster.local"];
3146
+ const result = CockroachDbCluster({ name: "crdb", joinAddresses: joins });
3147
+ const args = (result.statefulSet.spec as any).template.spec.containers[0].args as string[];
3148
+ const joinArg = args.find((a: string) => a.startsWith("--join="));
3149
+ expect(joinArg).toBeDefined();
3150
+ expect(joinArg).toContain("crdb-0.crdb.ns.svc.cluster.local");
3151
+ expect(joinArg).toContain("crdb-1.crdb.ns.svc.cluster.local");
3152
+ });
3153
+
3154
+ test("locality appears in container args when set", () => {
3155
+ const result = CockroachDbCluster({ name: "crdb", locality: "cloud=aws,region=us-east-1" });
3156
+ const args = (result.statefulSet.spec as any).template.spec.containers[0].args as string[];
3157
+ expect(args).toContain("--locality=cloud=aws,region=us-east-1");
3158
+ });
3159
+
3160
+ test("namespace is set on all namespaced resources", () => {
3161
+ const result = CockroachDbCluster({ name: "crdb", namespace: "crdb-eks" });
3162
+ for (const key of ["serviceAccount", "role", "roleBinding", "publicService", "headlessService", "pdb", "statefulSet", "initJob", "certGenJob"] as const) {
3163
+ expect((result[key].metadata as any).namespace).toBe("crdb-eks");
3164
+ }
3165
+ });
3166
+
3167
+ test("cluster-scoped resources do not have namespace", () => {
3168
+ const result = CockroachDbCluster({ name: "crdb", namespace: "crdb-eks" });
3169
+ expect((result.clusterRole.metadata as any).namespace).toBeUndefined();
3170
+ expect((result.clusterRoleBinding.metadata as any).namespace).toBeUndefined();
3171
+ });
3172
+
3173
+ test("includes common labels", () => {
3174
+ const result = CockroachDbCluster(minProps);
3175
+ const meta = result.statefulSet.metadata as any;
3176
+ expect(meta.labels["app.kubernetes.io/name"]).toBe("cockroachdb");
3177
+ expect(meta.labels["app.kubernetes.io/managed-by"]).toBe("chant");
3178
+ });
3179
+
3180
+ test("secure mode mounts certs volume", () => {
3181
+ const result = CockroachDbCluster({ name: "crdb", secure: true });
3182
+ const spec = (result.statefulSet.spec as any).template.spec;
3183
+ expect(spec.volumes).toBeDefined();
3184
+ const certsVol = spec.volumes.find((v: any) => v.name === "certs");
3185
+ expect(certsVol).toBeDefined();
3186
+ expect(certsVol.secret.secretName).toBe("crdb-node-certs");
3187
+ });
3188
+
3189
+ test("insecure mode omits certs volume", () => {
3190
+ const result = CockroachDbCluster({ name: "crdb", secure: false });
3191
+ const spec = (result.statefulSet.spec as any).template.spec;
3192
+ expect(spec.volumes).toBeUndefined();
3193
+ const args = spec.containers[0].args as string[];
3194
+ expect(args).toContain("--insecure");
3195
+ });
3196
+
3197
+ test("storageClassName is set when provided", () => {
3198
+ const result = CockroachDbCluster({ name: "crdb", storageClassName: "gp3-encrypted" });
3199
+ const vct = (result.statefulSet.spec as any).volumeClaimTemplates[0];
3200
+ expect(vct.spec.storageClassName).toBe("gp3-encrypted");
3201
+ });
3202
+
3203
+ test("init job references correct host", () => {
3204
+ const result = CockroachDbCluster({ name: "crdb" });
3205
+ const container = (result.initJob.spec as any).template.spec.containers[0];
3206
+ expect(container.args).toContain("--host=crdb-0.crdb");
3207
+ });
3208
+
3209
+ test("StatefulSet uses Parallel podManagementPolicy", () => {
3210
+ const result = CockroachDbCluster(minProps);
3211
+ expect((result.statefulSet.spec as any).podManagementPolicy).toBe("Parallel");
3212
+ });
3213
+
3214
+ test("cert-gen job uses same image as StatefulSet", () => {
3215
+ const result = CockroachDbCluster({ name: "crdb", image: "cockroachdb/cockroach:v23.2.0" });
3216
+ const container = (result.certGenJob.spec as any).template.spec.containers[0];
3217
+ expect(container.image).toBe("cockroachdb/cockroach:v23.2.0");
3218
+ });
3219
+ });
@@ -0,0 +1,143 @@
1
+ /**
2
+ * GceIngress composite — Ingress with GCE Ingress Controller annotations.
3
+ *
4
+ * @gke Full `kubernetes.io/ingress.*` and `networking.gke.io/*` annotation set
5
+ * including static IP, managed certificates, and FrontendConfig.
6
+ */
7
+
8
+ export interface GceIngressHost {
9
+ /** Hostname (e.g., "api.example.com"). */
10
+ hostname: string;
11
+ /** Path rules for this host. */
12
+ paths: Array<{
13
+ path: string;
14
+ pathType?: string;
15
+ serviceName: string;
16
+ /** Port on the Kubernetes Service (not the container port). */
17
+ servicePort: number;
18
+ }>;
19
+ }
20
+
21
+ export interface GceIngressProps {
22
+ /** Ingress name — used in metadata and labels. */
23
+ name: string;
24
+ /** Host definitions with paths. */
25
+ hosts: GceIngressHost[];
26
+ /** Global static IP name reserved in GCP (sets `kubernetes.io/ingress.global-static-ip-name`). */
27
+ staticIpName?: string;
28
+ /** GKE managed certificate name (sets `networking.gke.io/managed-certificates`). */
29
+ managedCertificate?: string;
30
+ /** FrontendConfig name for SSL policy / redirects (sets `networking.gke.io/v1beta1.FrontendConfig`). */
31
+ frontendConfig?: string;
32
+ /** Health check path for backend (sets `cloud.google.com/backend-config` is NOT handled here — use BackendConfig CRD). */
33
+ healthCheckPath?: string;
34
+ /** Enable HTTP->HTTPS redirect (default: true when managedCertificate set). */
35
+ sslRedirect?: boolean;
36
+ /** Additional annotations on the Ingress. */
37
+ annotations?: Record<string, string>;
38
+ /** Additional labels to apply to all resources. */
39
+ labels?: Record<string, string>;
40
+ /** Namespace for all resources. */
41
+ namespace?: string;
42
+ }
43
+
44
+ export interface GceIngressResult {
45
+ ingress: Record<string, unknown>;
46
+ }
47
+
48
+ /**
49
+ * Create a GceIngress composite — returns prop objects for
50
+ * an Ingress with GCE Ingress Controller annotations.
51
+ *
52
+ * @gke
53
+ * @example
54
+ * ```ts
55
+ * import { GceIngress } from "@intentius/chant-lexicon-k8s";
56
+ *
57
+ * const { ingress } = GceIngress({
58
+ * name: "api-ingress",
59
+ * hosts: [
60
+ * {
61
+ * hostname: "api.example.com",
62
+ * paths: [{ path: "/", serviceName: "api", servicePort: 80 }],
63
+ * },
64
+ * ],
65
+ * staticIpName: "microservice-ip",
66
+ * managedCertificate: "api-cert",
67
+ * });
68
+ * ```
69
+ */
70
+ export function GceIngress(props: GceIngressProps): GceIngressResult {
71
+ const {
72
+ name,
73
+ hosts,
74
+ staticIpName,
75
+ managedCertificate,
76
+ frontendConfig,
77
+ healthCheckPath,
78
+ sslRedirect,
79
+ annotations: extraAnnotations = {},
80
+ labels: extraLabels = {},
81
+ namespace,
82
+ } = props;
83
+
84
+ const commonLabels: Record<string, string> = {
85
+ "app.kubernetes.io/name": name,
86
+ "app.kubernetes.io/managed-by": "chant",
87
+ ...extraLabels,
88
+ };
89
+
90
+ // Build GCE annotations
91
+ const annotations: Record<string, string> = {
92
+ "kubernetes.io/ingress.class": "gce",
93
+ ...extraAnnotations,
94
+ };
95
+
96
+ if (staticIpName) {
97
+ annotations["kubernetes.io/ingress.global-static-ip-name"] = staticIpName;
98
+ }
99
+
100
+ if (managedCertificate) {
101
+ annotations["networking.gke.io/managed-certificates"] = managedCertificate;
102
+ }
103
+
104
+ if (frontendConfig) {
105
+ annotations["networking.gke.io/v1beta1.FrontendConfig"] = frontendConfig;
106
+ }
107
+
108
+ if (sslRedirect ?? !!managedCertificate) {
109
+ annotations["networking.gke.io/v1beta1.FrontendConfig"] =
110
+ annotations["networking.gke.io/v1beta1.FrontendConfig"] ?? `${name}-frontend-config`;
111
+ }
112
+
113
+ if (healthCheckPath) {
114
+ annotations["cloud.google.com/neg"] = '{"ingress": true}';
115
+ }
116
+
117
+ const ingressRules = hosts.map((host) => ({
118
+ host: host.hostname,
119
+ http: {
120
+ paths: host.paths.map((p) => ({
121
+ path: p.path,
122
+ pathType: p.pathType ?? "Prefix",
123
+ backend: {
124
+ service: { name: p.serviceName, port: { number: p.servicePort } },
125
+ },
126
+ })),
127
+ },
128
+ }));
129
+
130
+ const ingressProps: Record<string, unknown> = {
131
+ metadata: {
132
+ name,
133
+ ...(namespace && { namespace }),
134
+ labels: { ...commonLabels, "app.kubernetes.io/component": "ingress" },
135
+ annotations,
136
+ },
137
+ spec: {
138
+ rules: ingressRules,
139
+ },
140
+ };
141
+
142
+ return { ingress: ingressProps };
143
+ }