@intentius/chant-lexicon-aws 0.0.13 → 0.0.15
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 +26 -24
- package/dist/manifest.json +1 -1
- package/dist/meta.json +1093 -341
- package/dist/rules/waw030.ts +55 -0
- package/dist/rules/waw031.ts +66 -0
- package/dist/skills/chant-eks.md +175 -0
- package/dist/types/index.d.ts +841 -61
- package/package.json +29 -26
- package/src/codegen/docs.ts +103 -8
- package/src/generated/index.d.ts +841 -61
- package/src/generated/index.ts +65 -4
- package/src/generated/lexicon-aws.json +1093 -341
- package/src/lint/post-synth/waw030.test.ts +209 -1
- package/src/lint/post-synth/waw030.ts +55 -0
- package/src/lint/post-synth/waw031.test.ts +273 -0
- package/src/lint/post-synth/waw031.ts +66 -0
- package/src/plugin.ts +320 -2
- package/src/serializer.test.ts +40 -0
- package/src/serializer.ts +6 -1
|
@@ -761,16 +761,30 @@ describe("WAW030: Missing DependsOn for Known Patterns", () => {
|
|
|
761
761
|
ResourceId: "service/c/s",
|
|
762
762
|
},
|
|
763
763
|
},
|
|
764
|
+
EksCluster: {
|
|
765
|
+
Type: "AWS::EKS::Cluster",
|
|
766
|
+
Properties: { Name: "my-cluster" },
|
|
767
|
+
},
|
|
768
|
+
EksNodegroup: {
|
|
769
|
+
Type: "AWS::EKS::Nodegroup",
|
|
770
|
+
Properties: { ClusterName: "my-cluster" },
|
|
771
|
+
},
|
|
772
|
+
EksAddon: {
|
|
773
|
+
Type: "AWS::EKS::Addon",
|
|
774
|
+
Properties: { AddonName: "vpc-cni", ClusterName: "my-cluster" },
|
|
775
|
+
},
|
|
764
776
|
},
|
|
765
777
|
});
|
|
766
778
|
const diags = checkMissingDependsOn(ctx);
|
|
767
|
-
expect(diags).toHaveLength(
|
|
779
|
+
expect(diags).toHaveLength(8);
|
|
768
780
|
const entities = diags.map((d) => d.entity).sort();
|
|
769
781
|
expect(entities).toEqual([
|
|
770
782
|
"ApiDeployment",
|
|
771
783
|
"DynamoTarget",
|
|
772
784
|
"EcsService",
|
|
773
785
|
"EcsTarget",
|
|
786
|
+
"EksAddon",
|
|
787
|
+
"EksNodegroup",
|
|
774
788
|
"Route",
|
|
775
789
|
"V2Deployment",
|
|
776
790
|
]);
|
|
@@ -797,4 +811,198 @@ describe("WAW030: Missing DependsOn for Known Patterns", () => {
|
|
|
797
811
|
expect(diags[0].entity).toBe("MyTarget");
|
|
798
812
|
expect(diags[0].message).toContain("DynamoDB");
|
|
799
813
|
});
|
|
814
|
+
|
|
815
|
+
// --- EKS Addon + Cluster/Nodegroup pattern ---
|
|
816
|
+
|
|
817
|
+
test("EKS Addon with hardcoded ClusterName, no DependsOn on Cluster → warning", () => {
|
|
818
|
+
const ctx = makeCtx({
|
|
819
|
+
Resources: {
|
|
820
|
+
MyCluster: {
|
|
821
|
+
Type: "AWS::EKS::Cluster",
|
|
822
|
+
Properties: { Name: "my-cluster" },
|
|
823
|
+
},
|
|
824
|
+
MyNodegroup: {
|
|
825
|
+
Type: "AWS::EKS::Nodegroup",
|
|
826
|
+
DependsOn: "MyCluster",
|
|
827
|
+
Properties: { ClusterName: "my-cluster" },
|
|
828
|
+
},
|
|
829
|
+
VpcCni: {
|
|
830
|
+
Type: "AWS::EKS::Addon",
|
|
831
|
+
Properties: { AddonName: "vpc-cni", ClusterName: "my-cluster" },
|
|
832
|
+
},
|
|
833
|
+
},
|
|
834
|
+
});
|
|
835
|
+
const diags = checkMissingDependsOn(ctx);
|
|
836
|
+
expect(diags).toHaveLength(1);
|
|
837
|
+
expect(diags[0].checkId).toBe("WAW030");
|
|
838
|
+
expect(diags[0].severity).toBe("warning");
|
|
839
|
+
expect(diags[0].message).toContain("VpcCni");
|
|
840
|
+
expect(diags[0].message).toContain("Addon");
|
|
841
|
+
expect(diags[0].entity).toBe("VpcCni");
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
test("EKS Addon with DependsOn on Cluster → no diagnostic", () => {
|
|
845
|
+
const ctx = makeCtx({
|
|
846
|
+
Resources: {
|
|
847
|
+
MyCluster: {
|
|
848
|
+
Type: "AWS::EKS::Cluster",
|
|
849
|
+
Properties: { Name: "my-cluster" },
|
|
850
|
+
},
|
|
851
|
+
VpcCni: {
|
|
852
|
+
Type: "AWS::EKS::Addon",
|
|
853
|
+
DependsOn: "MyCluster",
|
|
854
|
+
Properties: { AddonName: "vpc-cni", ClusterName: "my-cluster" },
|
|
855
|
+
},
|
|
856
|
+
},
|
|
857
|
+
});
|
|
858
|
+
const diags = checkMissingDependsOn(ctx);
|
|
859
|
+
expect(diags).toHaveLength(0);
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
test("EKS Addon with DependsOn on Nodegroup → no diagnostic", () => {
|
|
863
|
+
const ctx = makeCtx({
|
|
864
|
+
Resources: {
|
|
865
|
+
MyCluster: {
|
|
866
|
+
Type: "AWS::EKS::Cluster",
|
|
867
|
+
Properties: { Name: "my-cluster" },
|
|
868
|
+
},
|
|
869
|
+
MyNodegroup: {
|
|
870
|
+
Type: "AWS::EKS::Nodegroup",
|
|
871
|
+
DependsOn: "MyCluster",
|
|
872
|
+
Properties: { ClusterName: "my-cluster" },
|
|
873
|
+
},
|
|
874
|
+
VpcCni: {
|
|
875
|
+
Type: "AWS::EKS::Addon",
|
|
876
|
+
DependsOn: ["MyCluster", "MyNodegroup"],
|
|
877
|
+
Properties: { AddonName: "vpc-cni", ClusterName: "my-cluster" },
|
|
878
|
+
},
|
|
879
|
+
},
|
|
880
|
+
});
|
|
881
|
+
const diags = checkMissingDependsOn(ctx);
|
|
882
|
+
expect(diags).toHaveLength(0);
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
test("EKS Addon with Ref to Cluster in ClusterName → no diagnostic", () => {
|
|
886
|
+
const ctx = makeCtx({
|
|
887
|
+
Resources: {
|
|
888
|
+
MyCluster: {
|
|
889
|
+
Type: "AWS::EKS::Cluster",
|
|
890
|
+
Properties: { Name: "my-cluster" },
|
|
891
|
+
},
|
|
892
|
+
VpcCni: {
|
|
893
|
+
Type: "AWS::EKS::Addon",
|
|
894
|
+
Properties: { AddonName: "vpc-cni", ClusterName: { Ref: "MyCluster" } },
|
|
895
|
+
},
|
|
896
|
+
},
|
|
897
|
+
});
|
|
898
|
+
const diags = checkMissingDependsOn(ctx);
|
|
899
|
+
expect(diags).toHaveLength(0);
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
test("EKS Addon without Cluster in template → no diagnostic", () => {
|
|
903
|
+
const ctx = makeCtx({
|
|
904
|
+
Resources: {
|
|
905
|
+
VpcCni: {
|
|
906
|
+
Type: "AWS::EKS::Addon",
|
|
907
|
+
Properties: { AddonName: "vpc-cni", ClusterName: "external-cluster" },
|
|
908
|
+
},
|
|
909
|
+
},
|
|
910
|
+
});
|
|
911
|
+
const diags = checkMissingDependsOn(ctx);
|
|
912
|
+
expect(diags).toHaveLength(0);
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
// --- EKS Nodegroup + Cluster pattern ---
|
|
916
|
+
|
|
917
|
+
test("EKS Nodegroup with hardcoded ClusterName, no DependsOn on Cluster → warning", () => {
|
|
918
|
+
const ctx = makeCtx({
|
|
919
|
+
Resources: {
|
|
920
|
+
MyCluster: {
|
|
921
|
+
Type: "AWS::EKS::Cluster",
|
|
922
|
+
Properties: { Name: "my-cluster" },
|
|
923
|
+
},
|
|
924
|
+
MyNodegroup: {
|
|
925
|
+
Type: "AWS::EKS::Nodegroup",
|
|
926
|
+
Properties: { ClusterName: "my-cluster" },
|
|
927
|
+
},
|
|
928
|
+
},
|
|
929
|
+
});
|
|
930
|
+
const diags = checkMissingDependsOn(ctx);
|
|
931
|
+
expect(diags).toHaveLength(1);
|
|
932
|
+
expect(diags[0].checkId).toBe("WAW030");
|
|
933
|
+
expect(diags[0].message).toContain("MyNodegroup");
|
|
934
|
+
expect(diags[0].message).toContain("Nodegroup");
|
|
935
|
+
expect(diags[0].entity).toBe("MyNodegroup");
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
test("EKS Nodegroup with DependsOn on Cluster → no diagnostic", () => {
|
|
939
|
+
const ctx = makeCtx({
|
|
940
|
+
Resources: {
|
|
941
|
+
MyCluster: {
|
|
942
|
+
Type: "AWS::EKS::Cluster",
|
|
943
|
+
Properties: { Name: "my-cluster" },
|
|
944
|
+
},
|
|
945
|
+
MyNodegroup: {
|
|
946
|
+
Type: "AWS::EKS::Nodegroup",
|
|
947
|
+
DependsOn: "MyCluster",
|
|
948
|
+
Properties: { ClusterName: "my-cluster" },
|
|
949
|
+
},
|
|
950
|
+
},
|
|
951
|
+
});
|
|
952
|
+
const diags = checkMissingDependsOn(ctx);
|
|
953
|
+
expect(diags).toHaveLength(0);
|
|
954
|
+
});
|
|
955
|
+
|
|
956
|
+
test("EKS Nodegroup with Ref to Cluster → no diagnostic", () => {
|
|
957
|
+
const ctx = makeCtx({
|
|
958
|
+
Resources: {
|
|
959
|
+
MyCluster: {
|
|
960
|
+
Type: "AWS::EKS::Cluster",
|
|
961
|
+
Properties: { Name: "my-cluster" },
|
|
962
|
+
},
|
|
963
|
+
MyNodegroup: {
|
|
964
|
+
Type: "AWS::EKS::Nodegroup",
|
|
965
|
+
Properties: { ClusterName: { Ref: "MyCluster" } },
|
|
966
|
+
},
|
|
967
|
+
},
|
|
968
|
+
});
|
|
969
|
+
const diags = checkMissingDependsOn(ctx);
|
|
970
|
+
expect(diags).toHaveLength(0);
|
|
971
|
+
});
|
|
972
|
+
|
|
973
|
+
test("EKS Nodegroup without Cluster in template → no diagnostic", () => {
|
|
974
|
+
const ctx = makeCtx({
|
|
975
|
+
Resources: {
|
|
976
|
+
MyNodegroup: {
|
|
977
|
+
Type: "AWS::EKS::Nodegroup",
|
|
978
|
+
Properties: { ClusterName: "external-cluster" },
|
|
979
|
+
},
|
|
980
|
+
},
|
|
981
|
+
});
|
|
982
|
+
const diags = checkMissingDependsOn(ctx);
|
|
983
|
+
expect(diags).toHaveLength(0);
|
|
984
|
+
});
|
|
985
|
+
|
|
986
|
+
test("multiple EKS Addons — only flags those missing DependsOn", () => {
|
|
987
|
+
const ctx = makeCtx({
|
|
988
|
+
Resources: {
|
|
989
|
+
MyCluster: {
|
|
990
|
+
Type: "AWS::EKS::Cluster",
|
|
991
|
+
Properties: { Name: "my-cluster" },
|
|
992
|
+
},
|
|
993
|
+
GoodAddon: {
|
|
994
|
+
Type: "AWS::EKS::Addon",
|
|
995
|
+
DependsOn: "MyCluster",
|
|
996
|
+
Properties: { AddonName: "vpc-cni", ClusterName: "my-cluster" },
|
|
997
|
+
},
|
|
998
|
+
BadAddon: {
|
|
999
|
+
Type: "AWS::EKS::Addon",
|
|
1000
|
+
Properties: { AddonName: "coredns", ClusterName: "my-cluster" },
|
|
1001
|
+
},
|
|
1002
|
+
},
|
|
1003
|
+
});
|
|
1004
|
+
const diags = checkMissingDependsOn(ctx);
|
|
1005
|
+
expect(diags).toHaveLength(1);
|
|
1006
|
+
expect(diags[0].entity).toBe("BadAddon");
|
|
1007
|
+
});
|
|
800
1008
|
});
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
* - API Gateway V2 Deployment with no DependsOn on any Route
|
|
11
11
|
* - DynamoDB ScalableTarget with no DependsOn on the Table
|
|
12
12
|
* - ECS ScalableTarget with no DependsOn on the ECS Service
|
|
13
|
+
* - EKS Addon with hardcoded ClusterName but no DependsOn on the Cluster or Nodegroup
|
|
14
|
+
* - EKS Nodegroup with hardcoded ClusterName but no DependsOn on the Cluster
|
|
13
15
|
*/
|
|
14
16
|
|
|
15
17
|
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
@@ -34,6 +36,9 @@ export function checkMissingDependsOn(ctx: PostSynthContext): PostSynthDiagnosti
|
|
|
34
36
|
const dynamoTableIds: string[] = [];
|
|
35
37
|
const ecsServiceIds: string[] = [];
|
|
36
38
|
const scalableTargetEntries: { logicalId: string; namespace: string }[] = [];
|
|
39
|
+
const eksClusterIds: string[] = [];
|
|
40
|
+
const eksNodegroupIds: string[] = [];
|
|
41
|
+
const eksAddonIds: string[] = [];
|
|
37
42
|
|
|
38
43
|
for (const [logicalId, resource] of Object.entries(resources)) {
|
|
39
44
|
if (resource.Type === "AWS::ElasticLoadBalancingV2::Listener") {
|
|
@@ -60,6 +65,15 @@ export function checkMissingDependsOn(ctx: PostSynthContext): PostSynthDiagnosti
|
|
|
60
65
|
if (resource.Type === "AWS::ECS::Service") {
|
|
61
66
|
ecsServiceIds.push(logicalId);
|
|
62
67
|
}
|
|
68
|
+
if (resource.Type === "AWS::EKS::Cluster") {
|
|
69
|
+
eksClusterIds.push(logicalId);
|
|
70
|
+
}
|
|
71
|
+
if (resource.Type === "AWS::EKS::Nodegroup") {
|
|
72
|
+
eksNodegroupIds.push(logicalId);
|
|
73
|
+
}
|
|
74
|
+
if (resource.Type === "AWS::EKS::Addon") {
|
|
75
|
+
eksAddonIds.push(logicalId);
|
|
76
|
+
}
|
|
63
77
|
if (resource.Type === "AWS::ApplicationAutoScaling::ScalableTarget") {
|
|
64
78
|
const props = resource.Properties ?? {};
|
|
65
79
|
const ns = inferScalingNamespace(props);
|
|
@@ -144,6 +158,47 @@ export function checkMissingDependsOn(ctx: PostSynthContext): PostSynthDiagnosti
|
|
|
144
158
|
}
|
|
145
159
|
}
|
|
146
160
|
|
|
161
|
+
// Pattern 7: EKS Addon with hardcoded ClusterName but no dependency on Cluster or Nodegroup
|
|
162
|
+
for (const addonId of eksAddonIds) {
|
|
163
|
+
const resource = resources[addonId];
|
|
164
|
+
const deps = getDependsOnSet(resource);
|
|
165
|
+
const propRefs = collectPropertyRefs(resource);
|
|
166
|
+
const allClusterAndNodeDeps = [...eksClusterIds, ...eksNodegroupIds];
|
|
167
|
+
|
|
168
|
+
const hasClusterDep = allClusterAndNodeDeps.some((id) => deps.has(id));
|
|
169
|
+
const hasClusterRef = allClusterAndNodeDeps.some((id) => propRefs.has(id));
|
|
170
|
+
|
|
171
|
+
if (!hasClusterDep && !hasClusterRef && eksClusterIds.length > 0) {
|
|
172
|
+
diagnostics.push({
|
|
173
|
+
checkId: "WAW030",
|
|
174
|
+
severity: "warning",
|
|
175
|
+
message: `EKS Addon "${addonId}" has no dependency on the Cluster or Nodegroup — the addon may fail if the cluster or nodes aren't ready yet. Add DependsOn.`,
|
|
176
|
+
entity: addonId,
|
|
177
|
+
lexicon: "aws",
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Pattern 8: EKS Nodegroup with hardcoded ClusterName but no dependency on Cluster
|
|
183
|
+
for (const ngId of eksNodegroupIds) {
|
|
184
|
+
const resource = resources[ngId];
|
|
185
|
+
const deps = getDependsOnSet(resource);
|
|
186
|
+
const propRefs = collectPropertyRefs(resource);
|
|
187
|
+
|
|
188
|
+
const hasClusterDep = eksClusterIds.some((id) => deps.has(id));
|
|
189
|
+
const hasClusterRef = eksClusterIds.some((id) => propRefs.has(id));
|
|
190
|
+
|
|
191
|
+
if (!hasClusterDep && !hasClusterRef && eksClusterIds.length > 0) {
|
|
192
|
+
diagnostics.push({
|
|
193
|
+
checkId: "WAW030",
|
|
194
|
+
severity: "warning",
|
|
195
|
+
message: `EKS Nodegroup "${ngId}" has no dependency on the Cluster — the node group may fail if the cluster isn't ready yet. Add DependsOn.`,
|
|
196
|
+
entity: ngId,
|
|
197
|
+
lexicon: "aws",
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
147
202
|
// Pattern 5 & 6: ScalableTarget with no DependsOn on the target resource
|
|
148
203
|
for (const entry of scalableTargetEntries) {
|
|
149
204
|
const resource = resources[entry.logicalId];
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { createPostSynthContext } from "@intentius/chant-test-utils";
|
|
3
|
+
import { waw031, checkAddonMissingRole } from "./waw031";
|
|
4
|
+
|
|
5
|
+
function makeCtx(template: object) {
|
|
6
|
+
return createPostSynthContext({ aws: template });
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe("WAW031: EKS Addon Missing ServiceAccountRoleArn", () => {
|
|
10
|
+
test("check metadata", () => {
|
|
11
|
+
expect(waw031.id).toBe("WAW031");
|
|
12
|
+
expect(waw031.description).toContain("ServiceAccountRoleArn");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
// --- aws-ebs-csi-driver ---
|
|
16
|
+
|
|
17
|
+
test("EBS CSI addon without ServiceAccountRoleArn → warning", () => {
|
|
18
|
+
const ctx = makeCtx({
|
|
19
|
+
Resources: {
|
|
20
|
+
EbsCsi: {
|
|
21
|
+
Type: "AWS::EKS::Addon",
|
|
22
|
+
Properties: {
|
|
23
|
+
AddonName: "aws-ebs-csi-driver",
|
|
24
|
+
ClusterName: "my-cluster",
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
const diags = checkAddonMissingRole(ctx);
|
|
30
|
+
expect(diags).toHaveLength(1);
|
|
31
|
+
expect(diags[0].checkId).toBe("WAW031");
|
|
32
|
+
expect(diags[0].severity).toBe("warning");
|
|
33
|
+
expect(diags[0].message).toContain("EbsCsi");
|
|
34
|
+
expect(diags[0].message).toContain("aws-ebs-csi-driver");
|
|
35
|
+
expect(diags[0].message).toContain("IRSA");
|
|
36
|
+
expect(diags[0].entity).toBe("EbsCsi");
|
|
37
|
+
expect(diags[0].lexicon).toBe("aws");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("EBS CSI addon with ServiceAccountRoleArn string → no diagnostic", () => {
|
|
41
|
+
const ctx = makeCtx({
|
|
42
|
+
Resources: {
|
|
43
|
+
EbsCsi: {
|
|
44
|
+
Type: "AWS::EKS::Addon",
|
|
45
|
+
Properties: {
|
|
46
|
+
AddonName: "aws-ebs-csi-driver",
|
|
47
|
+
ClusterName: "my-cluster",
|
|
48
|
+
ServiceAccountRoleArn: "arn:aws:iam::123456789012:role/ebs-csi-role",
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
const diags = checkAddonMissingRole(ctx);
|
|
54
|
+
expect(diags).toHaveLength(0);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("EBS CSI addon with ServiceAccountRoleArn via GetAtt → no diagnostic", () => {
|
|
58
|
+
const ctx = makeCtx({
|
|
59
|
+
Resources: {
|
|
60
|
+
EbsCsi: {
|
|
61
|
+
Type: "AWS::EKS::Addon",
|
|
62
|
+
Properties: {
|
|
63
|
+
AddonName: "aws-ebs-csi-driver",
|
|
64
|
+
ClusterName: "my-cluster",
|
|
65
|
+
ServiceAccountRoleArn: { "Fn::GetAtt": ["EbsCsiRole", "Arn"] },
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
const diags = checkAddonMissingRole(ctx);
|
|
71
|
+
expect(diags).toHaveLength(0);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// --- aws-efs-csi-driver ---
|
|
75
|
+
|
|
76
|
+
test("EFS CSI addon without ServiceAccountRoleArn → warning", () => {
|
|
77
|
+
const ctx = makeCtx({
|
|
78
|
+
Resources: {
|
|
79
|
+
EfsCsi: {
|
|
80
|
+
Type: "AWS::EKS::Addon",
|
|
81
|
+
Properties: {
|
|
82
|
+
AddonName: "aws-efs-csi-driver",
|
|
83
|
+
ClusterName: "my-cluster",
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
const diags = checkAddonMissingRole(ctx);
|
|
89
|
+
expect(diags).toHaveLength(1);
|
|
90
|
+
expect(diags[0].message).toContain("aws-efs-csi-driver");
|
|
91
|
+
expect(diags[0].entity).toBe("EfsCsi");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// --- adot ---
|
|
95
|
+
|
|
96
|
+
test("ADOT addon without ServiceAccountRoleArn → warning", () => {
|
|
97
|
+
const ctx = makeCtx({
|
|
98
|
+
Resources: {
|
|
99
|
+
Adot: {
|
|
100
|
+
Type: "AWS::EKS::Addon",
|
|
101
|
+
Properties: {
|
|
102
|
+
AddonName: "adot",
|
|
103
|
+
ClusterName: "my-cluster",
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
const diags = checkAddonMissingRole(ctx);
|
|
109
|
+
expect(diags).toHaveLength(1);
|
|
110
|
+
expect(diags[0].message).toContain("adot");
|
|
111
|
+
expect(diags[0].entity).toBe("Adot");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("ADOT addon with ServiceAccountRoleArn → no diagnostic", () => {
|
|
115
|
+
const ctx = makeCtx({
|
|
116
|
+
Resources: {
|
|
117
|
+
Adot: {
|
|
118
|
+
Type: "AWS::EKS::Addon",
|
|
119
|
+
Properties: {
|
|
120
|
+
AddonName: "adot",
|
|
121
|
+
ClusterName: "my-cluster",
|
|
122
|
+
ServiceAccountRoleArn: "arn:aws:iam::123456789012:role/adot-role",
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
const diags = checkAddonMissingRole(ctx);
|
|
128
|
+
expect(diags).toHaveLength(0);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// --- amazon-cloudwatch-observability ---
|
|
132
|
+
|
|
133
|
+
test("CloudWatch observability addon without ServiceAccountRoleArn → warning", () => {
|
|
134
|
+
const ctx = makeCtx({
|
|
135
|
+
Resources: {
|
|
136
|
+
CwObs: {
|
|
137
|
+
Type: "AWS::EKS::Addon",
|
|
138
|
+
Properties: {
|
|
139
|
+
AddonName: "amazon-cloudwatch-observability",
|
|
140
|
+
ClusterName: "my-cluster",
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
const diags = checkAddonMissingRole(ctx);
|
|
146
|
+
expect(diags).toHaveLength(1);
|
|
147
|
+
expect(diags[0].message).toContain("amazon-cloudwatch-observability");
|
|
148
|
+
expect(diags[0].entity).toBe("CwObs");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("CloudWatch observability addon with ServiceAccountRoleArn → no diagnostic", () => {
|
|
152
|
+
const ctx = makeCtx({
|
|
153
|
+
Resources: {
|
|
154
|
+
CwObs: {
|
|
155
|
+
Type: "AWS::EKS::Addon",
|
|
156
|
+
Properties: {
|
|
157
|
+
AddonName: "amazon-cloudwatch-observability",
|
|
158
|
+
ClusterName: "my-cluster",
|
|
159
|
+
ServiceAccountRoleArn: { "Fn::GetAtt": ["CwObsRole", "Arn"] },
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
const diags = checkAddonMissingRole(ctx);
|
|
165
|
+
expect(diags).toHaveLength(0);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// --- Addons that don't require IRSA ---
|
|
169
|
+
|
|
170
|
+
test("vpc-cni addon without ServiceAccountRoleArn → no diagnostic", () => {
|
|
171
|
+
const ctx = makeCtx({
|
|
172
|
+
Resources: {
|
|
173
|
+
VpcCni: {
|
|
174
|
+
Type: "AWS::EKS::Addon",
|
|
175
|
+
Properties: {
|
|
176
|
+
AddonName: "vpc-cni",
|
|
177
|
+
ClusterName: "my-cluster",
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
const diags = checkAddonMissingRole(ctx);
|
|
183
|
+
expect(diags).toHaveLength(0);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("coredns addon without ServiceAccountRoleArn → no diagnostic", () => {
|
|
187
|
+
const ctx = makeCtx({
|
|
188
|
+
Resources: {
|
|
189
|
+
CoreDns: {
|
|
190
|
+
Type: "AWS::EKS::Addon",
|
|
191
|
+
Properties: {
|
|
192
|
+
AddonName: "coredns",
|
|
193
|
+
ClusterName: "my-cluster",
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
const diags = checkAddonMissingRole(ctx);
|
|
199
|
+
expect(diags).toHaveLength(0);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test("kube-proxy addon without ServiceAccountRoleArn → no diagnostic", () => {
|
|
203
|
+
const ctx = makeCtx({
|
|
204
|
+
Resources: {
|
|
205
|
+
KubeProxy: {
|
|
206
|
+
Type: "AWS::EKS::Addon",
|
|
207
|
+
Properties: {
|
|
208
|
+
AddonName: "kube-proxy",
|
|
209
|
+
ClusterName: "my-cluster",
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
const diags = checkAddonMissingRole(ctx);
|
|
215
|
+
expect(diags).toHaveLength(0);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// --- Edge cases ---
|
|
219
|
+
|
|
220
|
+
test("multiple addons — only flags those requiring IRSA", () => {
|
|
221
|
+
const ctx = makeCtx({
|
|
222
|
+
Resources: {
|
|
223
|
+
VpcCni: {
|
|
224
|
+
Type: "AWS::EKS::Addon",
|
|
225
|
+
Properties: { AddonName: "vpc-cni", ClusterName: "c" },
|
|
226
|
+
},
|
|
227
|
+
EbsCsi: {
|
|
228
|
+
Type: "AWS::EKS::Addon",
|
|
229
|
+
Properties: { AddonName: "aws-ebs-csi-driver", ClusterName: "c" },
|
|
230
|
+
},
|
|
231
|
+
CoreDns: {
|
|
232
|
+
Type: "AWS::EKS::Addon",
|
|
233
|
+
Properties: { AddonName: "coredns", ClusterName: "c" },
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
const diags = checkAddonMissingRole(ctx);
|
|
238
|
+
expect(diags).toHaveLength(1);
|
|
239
|
+
expect(diags[0].entity).toBe("EbsCsi");
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("addon without AddonName property → no diagnostic", () => {
|
|
243
|
+
const ctx = makeCtx({
|
|
244
|
+
Resources: {
|
|
245
|
+
Mystery: {
|
|
246
|
+
Type: "AWS::EKS::Addon",
|
|
247
|
+
Properties: { ClusterName: "c" },
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
const diags = checkAddonMissingRole(ctx);
|
|
252
|
+
expect(diags).toHaveLength(0);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test("non-addon resource → no diagnostic", () => {
|
|
256
|
+
const ctx = makeCtx({
|
|
257
|
+
Resources: {
|
|
258
|
+
MyCluster: {
|
|
259
|
+
Type: "AWS::EKS::Cluster",
|
|
260
|
+
Properties: { Name: "my-cluster" },
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
const diags = checkAddonMissingRole(ctx);
|
|
265
|
+
expect(diags).toHaveLength(0);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test("empty Resources → no diagnostic", () => {
|
|
269
|
+
const ctx = makeCtx({ Resources: {} });
|
|
270
|
+
const diags = checkAddonMissingRole(ctx);
|
|
271
|
+
expect(diags).toHaveLength(0);
|
|
272
|
+
});
|
|
273
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WAW031: EKS Addon Missing ServiceAccountRoleArn
|
|
3
|
+
*
|
|
4
|
+
* Certain EKS addons require a ServiceAccountRoleArn (IRSA role) to function.
|
|
5
|
+
* Without one, the addon pods can't authenticate to AWS APIs and the addon
|
|
6
|
+
* hangs in CREATING status indefinitely.
|
|
7
|
+
*
|
|
8
|
+
* Known addons that require IRSA:
|
|
9
|
+
* - aws-ebs-csi-driver (needs EBS API access)
|
|
10
|
+
* - aws-efs-csi-driver (needs EFS API access)
|
|
11
|
+
* - adot (needs CloudWatch/X-Ray access)
|
|
12
|
+
* - amazon-cloudwatch-observability (needs CloudWatch access)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
16
|
+
import { parseCFTemplate } from "./cf-refs";
|
|
17
|
+
|
|
18
|
+
/** Addons that are known to require a ServiceAccountRoleArn to function. */
|
|
19
|
+
const ADDONS_REQUIRING_IRSA: Record<string, string> = {
|
|
20
|
+
"aws-ebs-csi-driver": "EBS API access to manage volumes",
|
|
21
|
+
"aws-efs-csi-driver": "EFS API access to manage file systems",
|
|
22
|
+
"adot": "CloudWatch/X-Ray access for metrics and traces",
|
|
23
|
+
"amazon-cloudwatch-observability": "CloudWatch access for logs and metrics",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export function checkAddonMissingRole(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
27
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
28
|
+
|
|
29
|
+
for (const [_lexicon, output] of ctx.outputs) {
|
|
30
|
+
const template = parseCFTemplate(output);
|
|
31
|
+
if (!template?.Resources) continue;
|
|
32
|
+
|
|
33
|
+
for (const [logicalId, resource] of Object.entries(template.Resources)) {
|
|
34
|
+
if (resource.Type !== "AWS::EKS::Addon") continue;
|
|
35
|
+
|
|
36
|
+
const props = resource.Properties ?? {};
|
|
37
|
+
const addonName = typeof props.AddonName === "string" ? props.AddonName : null;
|
|
38
|
+
if (!addonName) continue;
|
|
39
|
+
|
|
40
|
+
const reason = ADDONS_REQUIRING_IRSA[addonName];
|
|
41
|
+
if (!reason) continue;
|
|
42
|
+
|
|
43
|
+
// Check if ServiceAccountRoleArn is set (could be a string, Ref, or GetAtt)
|
|
44
|
+
if (!props.ServiceAccountRoleArn) {
|
|
45
|
+
diagnostics.push({
|
|
46
|
+
checkId: "WAW031",
|
|
47
|
+
severity: "warning",
|
|
48
|
+
message: `EKS Addon "${logicalId}" (${addonName}) has no ServiceAccountRoleArn — it needs an IRSA role for ${reason}. Without it, the addon will hang in CREATING status.`,
|
|
49
|
+
entity: logicalId,
|
|
50
|
+
lexicon: "aws",
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return diagnostics;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const waw031: PostSynthCheck = {
|
|
60
|
+
id: "WAW031",
|
|
61
|
+
description: "EKS Addon missing ServiceAccountRoleArn for addons that require IRSA",
|
|
62
|
+
|
|
63
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
64
|
+
return checkAddonMissingRole(ctx);
|
|
65
|
+
},
|
|
66
|
+
};
|