@intentius/chant-lexicon-k8s 0.0.15 → 0.0.16
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.
- package/dist/integrity.json +4 -3
- package/dist/manifest.json +1 -1
- package/dist/rules/latest-image-tag.ts +121 -0
- package/dist/rules/missing-resource-limits.ts +111 -0
- package/package.json +1 -1
- package/src/composites/agic-ingress.ts +0 -1
- package/src/composites/aks-external-dns-agent.ts +199 -0
- package/src/composites/azure-monitor-collector.ts +2 -2
- package/src/composites/composites.test.ts +359 -0
- package/src/composites/gce-ingress.ts +143 -0
- package/src/composites/gke-external-dns-agent.ts +175 -0
- package/src/composites/gke-fluent-bit-agent.ts +219 -0
- package/src/composites/gke-otel-collector.ts +229 -0
- package/src/composites/index.ts +12 -0
- package/src/index.ts +14 -0
- package/src/lint/rules/latest-image-tag.ts +121 -0
- package/src/lint/rules/missing-resource-limits.ts +111 -0
- package/src/lint/rules/rules.test.ts +192 -0
- package/src/plugin.ts +125 -208
- package/src/skills/chant-k8s-aks.md +146 -0
- package/src/skills/chant-k8s-gke.md +191 -0
- package/src/skills/kubernetes-patterns.md +183 -0
- package/src/skills/kubernetes-security.md +237 -0
- /package/{dist → src}/skills/chant-k8s-eks.md +0 -0
|
@@ -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,288 @@ 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
|
+
});
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GkeExternalDnsAgent composite — Deployment + ServiceAccount + ClusterRole + ClusterRoleBinding.
|
|
3
|
+
*
|
|
4
|
+
* @gke Like ExternalDnsAgent but uses --provider=google and GKE Workload Identity
|
|
5
|
+
* instead of IRSA for Cloud DNS management.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface GkeExternalDnsAgentProps {
|
|
9
|
+
/** GCP service account email for Workload Identity (needs Cloud DNS permissions). */
|
|
10
|
+
gcpServiceAccountEmail: string;
|
|
11
|
+
/** GCP project ID. */
|
|
12
|
+
gcpProjectId: string;
|
|
13
|
+
/** Domain filters — only manage DNS records for these domains. */
|
|
14
|
+
domainFilters: string[];
|
|
15
|
+
/** TXT record owner ID for identifying managed records. */
|
|
16
|
+
txtOwnerId?: string;
|
|
17
|
+
/** Source of DNS records (default: "ingress"). */
|
|
18
|
+
source?: string;
|
|
19
|
+
/** Agent name (default: "external-dns"). */
|
|
20
|
+
name?: string;
|
|
21
|
+
/** Container image (default: "registry.k8s.io/external-dns/external-dns:v0.14.0"). */
|
|
22
|
+
image?: string;
|
|
23
|
+
/** Namespace (default: "kube-system"). */
|
|
24
|
+
namespace?: string;
|
|
25
|
+
/** Additional labels. */
|
|
26
|
+
labels?: Record<string, string>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface GkeExternalDnsAgentResult {
|
|
30
|
+
deployment: Record<string, unknown>;
|
|
31
|
+
serviceAccount: Record<string, unknown>;
|
|
32
|
+
clusterRole: Record<string, unknown>;
|
|
33
|
+
clusterRoleBinding: Record<string, unknown>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Create a GkeExternalDnsAgent composite — returns prop objects for
|
|
38
|
+
* a Deployment, ServiceAccount (with Workload Identity), ClusterRole, and ClusterRoleBinding.
|
|
39
|
+
*
|
|
40
|
+
* @gke
|
|
41
|
+
* @example
|
|
42
|
+
* ```ts
|
|
43
|
+
* import { GkeExternalDnsAgent } from "@intentius/chant-lexicon-k8s";
|
|
44
|
+
*
|
|
45
|
+
* const { deployment, serviceAccount, clusterRole, clusterRoleBinding } = GkeExternalDnsAgent({
|
|
46
|
+
* gcpServiceAccountEmail: "external-dns@my-project.iam.gserviceaccount.com",
|
|
47
|
+
* gcpProjectId: "my-project",
|
|
48
|
+
* domainFilters: ["example.com"],
|
|
49
|
+
* txtOwnerId: "my-cluster",
|
|
50
|
+
* });
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export function GkeExternalDnsAgent(props: GkeExternalDnsAgentProps): GkeExternalDnsAgentResult {
|
|
54
|
+
const {
|
|
55
|
+
gcpServiceAccountEmail,
|
|
56
|
+
gcpProjectId,
|
|
57
|
+
domainFilters,
|
|
58
|
+
txtOwnerId,
|
|
59
|
+
source = "ingress",
|
|
60
|
+
name = "external-dns",
|
|
61
|
+
image = "registry.k8s.io/external-dns/external-dns:v0.14.0",
|
|
62
|
+
namespace = "kube-system",
|
|
63
|
+
labels: extraLabels = {},
|
|
64
|
+
} = props;
|
|
65
|
+
|
|
66
|
+
const saName = `${name}-sa`;
|
|
67
|
+
const clusterRoleName = `${name}-role`;
|
|
68
|
+
const bindingName = `${name}-binding`;
|
|
69
|
+
|
|
70
|
+
const commonLabels: Record<string, string> = {
|
|
71
|
+
"app.kubernetes.io/name": name,
|
|
72
|
+
"app.kubernetes.io/managed-by": "chant",
|
|
73
|
+
...extraLabels,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const args: string[] = [
|
|
77
|
+
`--source=${source}`,
|
|
78
|
+
"--provider=google",
|
|
79
|
+
`--google-project=${gcpProjectId}`,
|
|
80
|
+
"--policy=upsert-only",
|
|
81
|
+
"--registry=txt",
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
for (const domain of domainFilters) {
|
|
85
|
+
args.push(`--domain-filter=${domain}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (txtOwnerId) {
|
|
89
|
+
args.push(`--txt-owner-id=${txtOwnerId}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const deploymentProps: Record<string, unknown> = {
|
|
93
|
+
metadata: {
|
|
94
|
+
name,
|
|
95
|
+
namespace,
|
|
96
|
+
labels: { ...commonLabels, "app.kubernetes.io/component": "dns" },
|
|
97
|
+
},
|
|
98
|
+
spec: {
|
|
99
|
+
replicas: 1,
|
|
100
|
+
selector: { matchLabels: { "app.kubernetes.io/name": name } },
|
|
101
|
+
template: {
|
|
102
|
+
metadata: { labels: { "app.kubernetes.io/name": name, ...extraLabels } },
|
|
103
|
+
spec: {
|
|
104
|
+
serviceAccountName: saName,
|
|
105
|
+
containers: [
|
|
106
|
+
{
|
|
107
|
+
name,
|
|
108
|
+
image,
|
|
109
|
+
args,
|
|
110
|
+
resources: {
|
|
111
|
+
requests: { cpu: "50m", memory: "64Mi" },
|
|
112
|
+
limits: { cpu: "100m", memory: "128Mi" },
|
|
113
|
+
},
|
|
114
|
+
securityContext: {
|
|
115
|
+
runAsNonRoot: true,
|
|
116
|
+
runAsUser: 65534,
|
|
117
|
+
readOnlyRootFilesystem: true,
|
|
118
|
+
allowPrivilegeEscalation: false,
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const serviceAccountProps: Record<string, unknown> = {
|
|
128
|
+
metadata: {
|
|
129
|
+
name: saName,
|
|
130
|
+
namespace,
|
|
131
|
+
labels: { ...commonLabels, "app.kubernetes.io/component": "dns" },
|
|
132
|
+
annotations: {
|
|
133
|
+
"iam.gke.io/gcp-service-account": gcpServiceAccountEmail,
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const clusterRoleProps: Record<string, unknown> = {
|
|
139
|
+
metadata: {
|
|
140
|
+
name: clusterRoleName,
|
|
141
|
+
labels: { ...commonLabels, "app.kubernetes.io/component": "rbac" },
|
|
142
|
+
},
|
|
143
|
+
rules: [
|
|
144
|
+
{ apiGroups: [""], resources: ["services", "endpoints", "pods"], verbs: ["get", "watch", "list"] },
|
|
145
|
+
{ apiGroups: ["extensions", "networking.k8s.io"], resources: ["ingresses"], verbs: ["get", "watch", "list"] },
|
|
146
|
+
{ apiGroups: [""], resources: ["nodes"], verbs: ["list", "watch"] },
|
|
147
|
+
],
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const clusterRoleBindingProps: Record<string, unknown> = {
|
|
151
|
+
metadata: {
|
|
152
|
+
name: bindingName,
|
|
153
|
+
labels: { ...commonLabels, "app.kubernetes.io/component": "rbac" },
|
|
154
|
+
},
|
|
155
|
+
roleRef: {
|
|
156
|
+
apiGroup: "rbac.authorization.k8s.io",
|
|
157
|
+
kind: "ClusterRole",
|
|
158
|
+
name: clusterRoleName,
|
|
159
|
+
},
|
|
160
|
+
subjects: [
|
|
161
|
+
{
|
|
162
|
+
kind: "ServiceAccount",
|
|
163
|
+
name: saName,
|
|
164
|
+
namespace,
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
deployment: deploymentProps,
|
|
171
|
+
serviceAccount: serviceAccountProps,
|
|
172
|
+
clusterRole: clusterRoleProps,
|
|
173
|
+
clusterRoleBinding: clusterRoleBindingProps,
|
|
174
|
+
};
|
|
175
|
+
}
|