@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
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GkeFluentBitAgent composite — DaemonSet + RBAC + ConfigMap for Fluent Bit on GKE.
|
|
3
|
+
*
|
|
4
|
+
* @gke Like FluentBitAgent but targets Cloud Logging via the stackdriver
|
|
5
|
+
* output plugin and uses GKE Workload Identity instead of IRSA.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface GkeFluentBitAgentProps {
|
|
9
|
+
/** GKE cluster name — used as log stream prefix. */
|
|
10
|
+
clusterName: string;
|
|
11
|
+
/** GCP project ID. */
|
|
12
|
+
projectId: string;
|
|
13
|
+
/** GCP service account email for Workload Identity. */
|
|
14
|
+
gcpServiceAccountEmail?: string;
|
|
15
|
+
/** Agent name (default: "fluent-bit"). */
|
|
16
|
+
name?: string;
|
|
17
|
+
/** Fluent Bit image (default: "fluent/fluent-bit:latest"). */
|
|
18
|
+
image?: string;
|
|
19
|
+
/** Namespace (default: "gke-logging"). */
|
|
20
|
+
namespace?: string;
|
|
21
|
+
/** Additional labels. */
|
|
22
|
+
labels?: Record<string, string>;
|
|
23
|
+
/** CPU request (default: "50m"). */
|
|
24
|
+
cpuRequest?: string;
|
|
25
|
+
/** Memory request (default: "64Mi"). */
|
|
26
|
+
memoryRequest?: string;
|
|
27
|
+
/** CPU limit (default: "200m"). */
|
|
28
|
+
cpuLimit?: string;
|
|
29
|
+
/** Memory limit (default: "128Mi"). */
|
|
30
|
+
memoryLimit?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface GkeFluentBitAgentResult {
|
|
34
|
+
daemonSet: Record<string, unknown>;
|
|
35
|
+
serviceAccount: Record<string, unknown>;
|
|
36
|
+
clusterRole: Record<string, unknown>;
|
|
37
|
+
clusterRoleBinding: Record<string, unknown>;
|
|
38
|
+
configMap: Record<string, unknown>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Create a GkeFluentBitAgent composite — returns prop objects for
|
|
43
|
+
* a DaemonSet, ServiceAccount, ClusterRole, ClusterRoleBinding, and ConfigMap.
|
|
44
|
+
*
|
|
45
|
+
* @gke
|
|
46
|
+
* @example
|
|
47
|
+
* ```ts
|
|
48
|
+
* import { GkeFluentBitAgent } from "@intentius/chant-lexicon-k8s";
|
|
49
|
+
*
|
|
50
|
+
* const { daemonSet, serviceAccount, clusterRole, clusterRoleBinding, configMap } = GkeFluentBitAgent({
|
|
51
|
+
* clusterName: "my-cluster",
|
|
52
|
+
* projectId: "my-project",
|
|
53
|
+
* gcpServiceAccountEmail: "fluent-bit@my-project.iam.gserviceaccount.com",
|
|
54
|
+
* });
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export function GkeFluentBitAgent(props: GkeFluentBitAgentProps): GkeFluentBitAgentResult {
|
|
58
|
+
const {
|
|
59
|
+
clusterName,
|
|
60
|
+
projectId,
|
|
61
|
+
gcpServiceAccountEmail,
|
|
62
|
+
name = "fluent-bit",
|
|
63
|
+
image = "fluent/fluent-bit:latest",
|
|
64
|
+
namespace = "gke-logging",
|
|
65
|
+
labels: extraLabels = {},
|
|
66
|
+
cpuRequest = "50m",
|
|
67
|
+
memoryRequest = "64Mi",
|
|
68
|
+
cpuLimit = "200m",
|
|
69
|
+
memoryLimit = "128Mi",
|
|
70
|
+
} = props;
|
|
71
|
+
|
|
72
|
+
const saName = `${name}-sa`;
|
|
73
|
+
const clusterRoleName = `${name}-role`;
|
|
74
|
+
const bindingName = `${name}-binding`;
|
|
75
|
+
const configMapName = `${name}-config`;
|
|
76
|
+
|
|
77
|
+
const commonLabels: Record<string, string> = {
|
|
78
|
+
"app.kubernetes.io/name": name,
|
|
79
|
+
"app.kubernetes.io/managed-by": "chant",
|
|
80
|
+
...extraLabels,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const fluentBitConfig = `[SERVICE]
|
|
84
|
+
Flush 5
|
|
85
|
+
Log_Level info
|
|
86
|
+
Daemon off
|
|
87
|
+
Parsers_File parsers.conf
|
|
88
|
+
|
|
89
|
+
[INPUT]
|
|
90
|
+
Name tail
|
|
91
|
+
Tag kube.*
|
|
92
|
+
Path /var/log/containers/*.log
|
|
93
|
+
Parser docker
|
|
94
|
+
DB /var/fluent-bit/state/flb_container.db
|
|
95
|
+
Mem_Buf_Limit 5MB
|
|
96
|
+
Skip_Long_Lines On
|
|
97
|
+
Refresh_Interval 10
|
|
98
|
+
|
|
99
|
+
[FILTER]
|
|
100
|
+
Name kubernetes
|
|
101
|
+
Match kube.*
|
|
102
|
+
Kube_URL https://kubernetes.default.svc:443
|
|
103
|
+
Kube_CA_File /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
|
|
104
|
+
Kube_Token_File /var/run/secrets/kubernetes.io/serviceaccount/token
|
|
105
|
+
Merge_Log On
|
|
106
|
+
K8S-Logging.Parser On
|
|
107
|
+
K8S-Logging.Exclude Off
|
|
108
|
+
|
|
109
|
+
[OUTPUT]
|
|
110
|
+
Name stackdriver
|
|
111
|
+
Match *
|
|
112
|
+
google_service_credentials /var/run/secrets/kubernetes.io/serviceaccount/token
|
|
113
|
+
resource k8s_container
|
|
114
|
+
k8s_cluster_name ${clusterName}
|
|
115
|
+
k8s_cluster_location ${projectId}
|
|
116
|
+
`;
|
|
117
|
+
|
|
118
|
+
const container: Record<string, unknown> = {
|
|
119
|
+
name,
|
|
120
|
+
image,
|
|
121
|
+
resources: {
|
|
122
|
+
requests: { cpu: cpuRequest, memory: memoryRequest },
|
|
123
|
+
limits: { cpu: cpuLimit, memory: memoryLimit },
|
|
124
|
+
},
|
|
125
|
+
volumeMounts: [
|
|
126
|
+
{ name: "varlog", mountPath: "/var/log", readOnly: true },
|
|
127
|
+
{ name: "config", mountPath: `/etc/${name}`, readOnly: true },
|
|
128
|
+
{ name: "state", mountPath: "/var/fluent-bit/state" },
|
|
129
|
+
],
|
|
130
|
+
securityContext: {
|
|
131
|
+
runAsUser: 0,
|
|
132
|
+
readOnlyRootFilesystem: true,
|
|
133
|
+
allowPrivilegeEscalation: false,
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const daemonSetProps: Record<string, unknown> = {
|
|
138
|
+
metadata: {
|
|
139
|
+
name,
|
|
140
|
+
namespace,
|
|
141
|
+
labels: { ...commonLabels, "app.kubernetes.io/component": "agent" },
|
|
142
|
+
},
|
|
143
|
+
spec: {
|
|
144
|
+
selector: { matchLabels: { "app.kubernetes.io/name": name } },
|
|
145
|
+
template: {
|
|
146
|
+
metadata: { labels: { "app.kubernetes.io/name": name, ...extraLabels } },
|
|
147
|
+
spec: {
|
|
148
|
+
serviceAccountName: saName,
|
|
149
|
+
containers: [container],
|
|
150
|
+
volumes: [
|
|
151
|
+
{ name: "varlog", hostPath: { path: "/var/log" } },
|
|
152
|
+
{ name: "config", configMap: { name: configMapName } },
|
|
153
|
+
{ name: "state", hostPath: { path: `/var/fluent-bit/state/${name}` } },
|
|
154
|
+
],
|
|
155
|
+
tolerations: [{ operator: "Exists" }],
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const serviceAccountProps: Record<string, unknown> = {
|
|
162
|
+
metadata: {
|
|
163
|
+
name: saName,
|
|
164
|
+
namespace,
|
|
165
|
+
labels: { ...commonLabels, "app.kubernetes.io/component": "agent" },
|
|
166
|
+
...(gcpServiceAccountEmail
|
|
167
|
+
? { annotations: { "iam.gke.io/gcp-service-account": gcpServiceAccountEmail } }
|
|
168
|
+
: {}),
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const clusterRoleProps: Record<string, unknown> = {
|
|
173
|
+
metadata: {
|
|
174
|
+
name: clusterRoleName,
|
|
175
|
+
labels: { ...commonLabels, "app.kubernetes.io/component": "rbac" },
|
|
176
|
+
},
|
|
177
|
+
rules: [
|
|
178
|
+
{ apiGroups: [""], resources: ["namespaces", "pods"], verbs: ["get", "list", "watch"] },
|
|
179
|
+
],
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const clusterRoleBindingProps: Record<string, unknown> = {
|
|
183
|
+
metadata: {
|
|
184
|
+
name: bindingName,
|
|
185
|
+
labels: { ...commonLabels, "app.kubernetes.io/component": "rbac" },
|
|
186
|
+
},
|
|
187
|
+
roleRef: {
|
|
188
|
+
apiGroup: "rbac.authorization.k8s.io",
|
|
189
|
+
kind: "ClusterRole",
|
|
190
|
+
name: clusterRoleName,
|
|
191
|
+
},
|
|
192
|
+
subjects: [
|
|
193
|
+
{
|
|
194
|
+
kind: "ServiceAccount",
|
|
195
|
+
name: saName,
|
|
196
|
+
namespace,
|
|
197
|
+
},
|
|
198
|
+
],
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const configMapProps: Record<string, unknown> = {
|
|
202
|
+
metadata: {
|
|
203
|
+
name: configMapName,
|
|
204
|
+
namespace,
|
|
205
|
+
labels: { ...commonLabels, "app.kubernetes.io/component": "config" },
|
|
206
|
+
},
|
|
207
|
+
data: {
|
|
208
|
+
"fluent-bit.conf": fluentBitConfig,
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
daemonSet: daemonSetProps,
|
|
214
|
+
serviceAccount: serviceAccountProps,
|
|
215
|
+
clusterRole: clusterRoleProps,
|
|
216
|
+
clusterRoleBinding: clusterRoleBindingProps,
|
|
217
|
+
configMap: configMapProps,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GkeOtelCollector composite — DaemonSet + RBAC + ConfigMap for OpenTelemetry on GKE.
|
|
3
|
+
*
|
|
4
|
+
* @gke Like AdotCollector but targets Cloud Trace + Cloud Monitoring via the
|
|
5
|
+
* googlecloud exporter and uses GKE Workload Identity instead of IRSA.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface GkeOtelCollectorProps {
|
|
9
|
+
/** GKE cluster name. */
|
|
10
|
+
clusterName: string;
|
|
11
|
+
/** GCP project ID. */
|
|
12
|
+
projectId: string;
|
|
13
|
+
/** GCP service account email for Workload Identity. */
|
|
14
|
+
gcpServiceAccountEmail?: string;
|
|
15
|
+
/** Agent name (default: "gke-otel-collector"). */
|
|
16
|
+
name?: string;
|
|
17
|
+
/** OTel Collector image (default: "otel/opentelemetry-collector-contrib:latest"). */
|
|
18
|
+
image?: string;
|
|
19
|
+
/** Namespace (default: "gke-monitoring"). */
|
|
20
|
+
namespace?: string;
|
|
21
|
+
/** Additional labels. */
|
|
22
|
+
labels?: Record<string, string>;
|
|
23
|
+
/** CPU request (default: "100m"). */
|
|
24
|
+
cpuRequest?: string;
|
|
25
|
+
/** Memory request (default: "256Mi"). */
|
|
26
|
+
memoryRequest?: string;
|
|
27
|
+
/** CPU limit (default: "500m"). */
|
|
28
|
+
cpuLimit?: string;
|
|
29
|
+
/** Memory limit (default: "512Mi"). */
|
|
30
|
+
memoryLimit?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface GkeOtelCollectorResult {
|
|
34
|
+
daemonSet: Record<string, unknown>;
|
|
35
|
+
serviceAccount: Record<string, unknown>;
|
|
36
|
+
clusterRole: Record<string, unknown>;
|
|
37
|
+
clusterRoleBinding: Record<string, unknown>;
|
|
38
|
+
configMap: Record<string, unknown>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Create a GkeOtelCollector composite — returns prop objects for
|
|
43
|
+
* a DaemonSet, ServiceAccount, ClusterRole, ClusterRoleBinding, and ConfigMap.
|
|
44
|
+
*
|
|
45
|
+
* @gke
|
|
46
|
+
* @example
|
|
47
|
+
* ```ts
|
|
48
|
+
* import { GkeOtelCollector } from "@intentius/chant-lexicon-k8s";
|
|
49
|
+
*
|
|
50
|
+
* const { daemonSet, serviceAccount, clusterRole, clusterRoleBinding, configMap } = GkeOtelCollector({
|
|
51
|
+
* clusterName: "my-cluster",
|
|
52
|
+
* projectId: "my-project",
|
|
53
|
+
* gcpServiceAccountEmail: "otel@my-project.iam.gserviceaccount.com",
|
|
54
|
+
* });
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export function GkeOtelCollector(props: GkeOtelCollectorProps): GkeOtelCollectorResult {
|
|
58
|
+
const {
|
|
59
|
+
clusterName,
|
|
60
|
+
projectId,
|
|
61
|
+
gcpServiceAccountEmail,
|
|
62
|
+
name = "gke-otel-collector",
|
|
63
|
+
image = "otel/opentelemetry-collector-contrib:latest",
|
|
64
|
+
namespace = "gke-monitoring",
|
|
65
|
+
labels: extraLabels = {},
|
|
66
|
+
cpuRequest = "100m",
|
|
67
|
+
memoryRequest = "256Mi",
|
|
68
|
+
cpuLimit = "500m",
|
|
69
|
+
memoryLimit = "512Mi",
|
|
70
|
+
} = props;
|
|
71
|
+
|
|
72
|
+
const saName = `${name}-sa`;
|
|
73
|
+
const clusterRoleName = `${name}-role`;
|
|
74
|
+
const bindingName = `${name}-binding`;
|
|
75
|
+
const configMapName = `${name}-config`;
|
|
76
|
+
|
|
77
|
+
const commonLabels: Record<string, string> = {
|
|
78
|
+
"app.kubernetes.io/name": name,
|
|
79
|
+
"app.kubernetes.io/managed-by": "chant",
|
|
80
|
+
...extraLabels,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const otelConfig = `receivers:
|
|
84
|
+
otlp:
|
|
85
|
+
protocols:
|
|
86
|
+
grpc:
|
|
87
|
+
endpoint: 0.0.0.0:4317
|
|
88
|
+
http:
|
|
89
|
+
endpoint: 0.0.0.0:4318
|
|
90
|
+
|
|
91
|
+
processors:
|
|
92
|
+
batch:
|
|
93
|
+
timeout: 30s
|
|
94
|
+
send_batch_size: 8192
|
|
95
|
+
resourcedetection:
|
|
96
|
+
detectors: [gcp]
|
|
97
|
+
timeout: 10s
|
|
98
|
+
|
|
99
|
+
exporters:
|
|
100
|
+
googlecloud:
|
|
101
|
+
project: ${projectId}
|
|
102
|
+
metric:
|
|
103
|
+
prefix: custom.googleapis.com/${clusterName}
|
|
104
|
+
trace:
|
|
105
|
+
attribute_mappings:
|
|
106
|
+
- key: service.name
|
|
107
|
+
replacement: g.co/r/service/name
|
|
108
|
+
|
|
109
|
+
service:
|
|
110
|
+
pipelines:
|
|
111
|
+
metrics:
|
|
112
|
+
receivers: [otlp]
|
|
113
|
+
processors: [batch, resourcedetection]
|
|
114
|
+
exporters: [googlecloud]
|
|
115
|
+
traces:
|
|
116
|
+
receivers: [otlp]
|
|
117
|
+
processors: [batch, resourcedetection]
|
|
118
|
+
exporters: [googlecloud]
|
|
119
|
+
`;
|
|
120
|
+
|
|
121
|
+
const container: Record<string, unknown> = {
|
|
122
|
+
name,
|
|
123
|
+
image,
|
|
124
|
+
args: ["--config=/etc/otel/config.yaml"],
|
|
125
|
+
ports: [
|
|
126
|
+
{ containerPort: 4317, name: "otlp-grpc" },
|
|
127
|
+
{ containerPort: 4318, name: "otlp-http" },
|
|
128
|
+
],
|
|
129
|
+
resources: {
|
|
130
|
+
requests: { cpu: cpuRequest, memory: memoryRequest },
|
|
131
|
+
limits: { cpu: cpuLimit, memory: memoryLimit },
|
|
132
|
+
},
|
|
133
|
+
volumeMounts: [
|
|
134
|
+
{ name: "config", mountPath: "/etc/otel", readOnly: true },
|
|
135
|
+
],
|
|
136
|
+
securityContext: {
|
|
137
|
+
runAsNonRoot: true,
|
|
138
|
+
runAsUser: 10001,
|
|
139
|
+
readOnlyRootFilesystem: true,
|
|
140
|
+
allowPrivilegeEscalation: false,
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const daemonSetProps: Record<string, unknown> = {
|
|
145
|
+
metadata: {
|
|
146
|
+
name,
|
|
147
|
+
namespace,
|
|
148
|
+
labels: { ...commonLabels, "app.kubernetes.io/component": "agent" },
|
|
149
|
+
},
|
|
150
|
+
spec: {
|
|
151
|
+
selector: { matchLabels: { "app.kubernetes.io/name": name } },
|
|
152
|
+
template: {
|
|
153
|
+
metadata: { labels: { "app.kubernetes.io/name": name, ...extraLabels } },
|
|
154
|
+
spec: {
|
|
155
|
+
serviceAccountName: saName,
|
|
156
|
+
containers: [container],
|
|
157
|
+
volumes: [
|
|
158
|
+
{ name: "config", configMap: { name: configMapName } },
|
|
159
|
+
],
|
|
160
|
+
tolerations: [{ operator: "Exists" }],
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const serviceAccountProps: Record<string, unknown> = {
|
|
167
|
+
metadata: {
|
|
168
|
+
name: saName,
|
|
169
|
+
namespace,
|
|
170
|
+
labels: { ...commonLabels, "app.kubernetes.io/component": "agent" },
|
|
171
|
+
...(gcpServiceAccountEmail
|
|
172
|
+
? { annotations: { "iam.gke.io/gcp-service-account": gcpServiceAccountEmail } }
|
|
173
|
+
: {}),
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const clusterRoleProps: Record<string, unknown> = {
|
|
178
|
+
metadata: {
|
|
179
|
+
name: clusterRoleName,
|
|
180
|
+
labels: { ...commonLabels, "app.kubernetes.io/component": "rbac" },
|
|
181
|
+
},
|
|
182
|
+
rules: [
|
|
183
|
+
{ apiGroups: [""], resources: ["pods", "nodes", "endpoints"], verbs: ["get", "list", "watch"] },
|
|
184
|
+
{ apiGroups: ["apps"], resources: ["replicasets"], verbs: ["get", "list", "watch"] },
|
|
185
|
+
{ apiGroups: ["batch"], resources: ["jobs"], verbs: ["get", "list", "watch"] },
|
|
186
|
+
{ apiGroups: [""], resources: ["nodes/proxy"], verbs: ["get"] },
|
|
187
|
+
{ apiGroups: [""], resources: ["nodes/stats", "configmaps", "events"], verbs: ["create", "get"] },
|
|
188
|
+
{ apiGroups: [""], resources: ["configmaps"], verbs: ["get", "update", "create"], resourceNames: ["otel-container-insight-clusterleader"] },
|
|
189
|
+
],
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const clusterRoleBindingProps: Record<string, unknown> = {
|
|
193
|
+
metadata: {
|
|
194
|
+
name: bindingName,
|
|
195
|
+
labels: { ...commonLabels, "app.kubernetes.io/component": "rbac" },
|
|
196
|
+
},
|
|
197
|
+
roleRef: {
|
|
198
|
+
apiGroup: "rbac.authorization.k8s.io",
|
|
199
|
+
kind: "ClusterRole",
|
|
200
|
+
name: clusterRoleName,
|
|
201
|
+
},
|
|
202
|
+
subjects: [
|
|
203
|
+
{
|
|
204
|
+
kind: "ServiceAccount",
|
|
205
|
+
name: saName,
|
|
206
|
+
namespace,
|
|
207
|
+
},
|
|
208
|
+
],
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const configMapProps: Record<string, unknown> = {
|
|
212
|
+
metadata: {
|
|
213
|
+
name: configMapName,
|
|
214
|
+
namespace,
|
|
215
|
+
labels: { ...commonLabels, "app.kubernetes.io/component": "config" },
|
|
216
|
+
},
|
|
217
|
+
data: {
|
|
218
|
+
"config.yaml": otelConfig,
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
daemonSet: daemonSetProps,
|
|
224
|
+
serviceAccount: serviceAccountProps,
|
|
225
|
+
clusterRole: clusterRoleProps,
|
|
226
|
+
clusterRoleBinding: clusterRoleBindingProps,
|
|
227
|
+
configMap: configMapProps,
|
|
228
|
+
};
|
|
229
|
+
}
|
package/src/composites/index.ts
CHANGED
|
@@ -51,6 +51,8 @@ export { GkeGateway } from "./gke-gateway";
|
|
|
51
51
|
export type { GkeGatewayProps, GkeGatewayResult } from "./gke-gateway";
|
|
52
52
|
export { ConfigConnectorContext } from "./config-connector-context";
|
|
53
53
|
export type { ConfigConnectorContextProps, ConfigConnectorContextResult } from "./config-connector-context";
|
|
54
|
+
export { GceIngress } from "./gce-ingress";
|
|
55
|
+
export type { GceIngressProps, GceIngressResult } from "./gce-ingress";
|
|
54
56
|
export { AgicIngress } from "./agic-ingress";
|
|
55
57
|
export type { AgicIngressProps, AgicIngressResult } from "./agic-ingress";
|
|
56
58
|
export { AzureDiskStorageClass } from "./azure-disk-storage-class";
|
|
@@ -59,3 +61,13 @@ export { AzureFileStorageClass } from "./azure-file-storage-class";
|
|
|
59
61
|
export type { AzureFileStorageClassProps, AzureFileStorageClassResult } from "./azure-file-storage-class";
|
|
60
62
|
export { AzureMonitorCollector } from "./azure-monitor-collector";
|
|
61
63
|
export type { AzureMonitorCollectorProps, AzureMonitorCollectorResult } from "./azure-monitor-collector";
|
|
64
|
+
export { WorkloadIdentityServiceAccount as AksWorkloadIdentityServiceAccount } from "./workload-identity-sa";
|
|
65
|
+
export type { WorkloadIdentityServiceAccountProps as AksWorkloadIdentityServiceAccountProps, WorkloadIdentityServiceAccountResult as AksWorkloadIdentityServiceAccountResult } from "./workload-identity-sa";
|
|
66
|
+
export { GkeFluentBitAgent } from "./gke-fluent-bit-agent";
|
|
67
|
+
export type { GkeFluentBitAgentProps, GkeFluentBitAgentResult } from "./gke-fluent-bit-agent";
|
|
68
|
+
export { GkeOtelCollector } from "./gke-otel-collector";
|
|
69
|
+
export type { GkeOtelCollectorProps, GkeOtelCollectorResult } from "./gke-otel-collector";
|
|
70
|
+
export { GkeExternalDnsAgent } from "./gke-external-dns-agent";
|
|
71
|
+
export type { GkeExternalDnsAgentProps, GkeExternalDnsAgentResult } from "./gke-external-dns-agent";
|
|
72
|
+
export { AksExternalDnsAgent } from "./aks-external-dns-agent";
|
|
73
|
+
export type { AksExternalDnsAgentProps, AksExternalDnsAgentResult } from "./aks-external-dns-agent";
|
package/src/index.ts
CHANGED
|
@@ -21,6 +21,10 @@ export {
|
|
|
21
21
|
BatchJob, SecureIngress, ConfiguredApp, SidecarApp, MonitoredService, NetworkIsolatedApp,
|
|
22
22
|
IrsaServiceAccount, AlbIngress, EbsStorageClass, EfsStorageClass, FluentBitAgent, ExternalDnsAgent, AdotCollector,
|
|
23
23
|
MetricsServer, WorkloadIdentityServiceAccount, GcePdStorageClass, FilestoreStorageClass, GkeGateway, ConfigConnectorContext,
|
|
24
|
+
GceIngress,
|
|
25
|
+
AgicIngress, AzureDiskStorageClass, AzureFileStorageClass, AzureMonitorCollector,
|
|
26
|
+
AksWorkloadIdentityServiceAccount,
|
|
27
|
+
GkeFluentBitAgent, GkeOtelCollector, GkeExternalDnsAgent, AksExternalDnsAgent,
|
|
24
28
|
} from "./composites/index";
|
|
25
29
|
export type {
|
|
26
30
|
WebAppProps, WebAppResult, StatefulAppProps, StatefulAppResult, CronWorkloadProps, CronWorkloadResult,
|
|
@@ -39,6 +43,16 @@ export type {
|
|
|
39
43
|
FilestoreStorageClassProps, FilestoreStorageClassResult,
|
|
40
44
|
GkeGatewayProps, GkeGatewayResult,
|
|
41
45
|
ConfigConnectorContextProps, ConfigConnectorContextResult,
|
|
46
|
+
GceIngressProps, GceIngressResult,
|
|
47
|
+
AgicIngressProps, AgicIngressResult,
|
|
48
|
+
AzureDiskStorageClassProps, AzureDiskStorageClassResult,
|
|
49
|
+
AzureFileStorageClassProps, AzureFileStorageClassResult,
|
|
50
|
+
AzureMonitorCollectorProps, AzureMonitorCollectorResult,
|
|
51
|
+
AksWorkloadIdentityServiceAccountProps, AksWorkloadIdentityServiceAccountResult,
|
|
52
|
+
GkeFluentBitAgentProps, GkeFluentBitAgentResult,
|
|
53
|
+
GkeOtelCollectorProps, GkeOtelCollectorResult,
|
|
54
|
+
GkeExternalDnsAgentProps, GkeExternalDnsAgentResult,
|
|
55
|
+
AksExternalDnsAgentProps, AksExternalDnsAgentResult,
|
|
42
56
|
} from "./composites/index";
|
|
43
57
|
|
|
44
58
|
// RBAC verb constants
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { LintRule, LintDiagnostic, LintContext } from "@intentius/chant/lint/rule";
|
|
2
|
+
import * as ts from "typescript";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* WK8002: Latest Image Tag
|
|
6
|
+
*
|
|
7
|
+
* Detects when a K8s workload resource uses the `:latest` image tag or no tag
|
|
8
|
+
* at all in a container image string literal. Untagged or `:latest` images are
|
|
9
|
+
* non-deterministic and can cause unexpected rollouts.
|
|
10
|
+
*
|
|
11
|
+
* Bad: new Deployment({ spec: { template: { spec: { containers: [{ image: "nginx:latest" }] } } } })
|
|
12
|
+
* Bad: new Deployment({ spec: { template: { spec: { containers: [{ image: "nginx" }] } } } })
|
|
13
|
+
* Good: new Deployment({ spec: { template: { spec: { containers: [{ image: "nginx:1.25" }] } } } })
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const WORKLOAD_KINDS = new Set([
|
|
17
|
+
"Deployment",
|
|
18
|
+
"StatefulSet",
|
|
19
|
+
"DaemonSet",
|
|
20
|
+
"CronJob",
|
|
21
|
+
"Job",
|
|
22
|
+
"ReplicaSet",
|
|
23
|
+
"Pod",
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Returns true if the string looks like a container image reference that is
|
|
28
|
+
* either untagged or using `:latest`.
|
|
29
|
+
*
|
|
30
|
+
* A string is considered a container image if it:
|
|
31
|
+
* - Contains at least one alphabetic character
|
|
32
|
+
* - Does not contain spaces
|
|
33
|
+
* - Is not a simple keyword like "true", "false", etc.
|
|
34
|
+
*/
|
|
35
|
+
function isProblematicImage(value: string): boolean {
|
|
36
|
+
if (!value || value.includes(" ") || value.length === 0) return false;
|
|
37
|
+
|
|
38
|
+
// Skip values that are clearly not images
|
|
39
|
+
const nonImagePatterns = [
|
|
40
|
+
/^(true|false|null|undefined|yes|no)$/i,
|
|
41
|
+
/^\d+$/, // pure numbers
|
|
42
|
+
/^[.\/]/, // relative/absolute paths without image-like structure
|
|
43
|
+
];
|
|
44
|
+
for (const pat of nonImagePatterns) {
|
|
45
|
+
if (pat.test(value)) return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Check for :latest explicitly
|
|
49
|
+
if (value.endsWith(":latest")) return true;
|
|
50
|
+
|
|
51
|
+
// Check for untagged image: no colon at all, but looks like an image name
|
|
52
|
+
// Images contain alphanumeric chars and may have / for registry prefix
|
|
53
|
+
// Must have at least one alpha char and match image naming conventions
|
|
54
|
+
if (!value.includes(":") && !value.includes("@") && /^[a-zA-Z0-9._\-\/]+$/.test(value) && /[a-zA-Z]/.test(value)) {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const latestImageTagRule: LintRule = {
|
|
62
|
+
id: "WK8002",
|
|
63
|
+
severity: "warning",
|
|
64
|
+
category: "security",
|
|
65
|
+
description:
|
|
66
|
+
"Detects :latest or untagged container images — use explicit version tags for reproducibility",
|
|
67
|
+
|
|
68
|
+
check(context: LintContext): LintDiagnostic[] {
|
|
69
|
+
const { sourceFile } = context;
|
|
70
|
+
const diagnostics: LintDiagnostic[] = [];
|
|
71
|
+
|
|
72
|
+
function isInsideWorkloadConstructor(node: ts.Node): boolean {
|
|
73
|
+
let current: ts.Node | undefined = node.parent;
|
|
74
|
+
while (current) {
|
|
75
|
+
if (
|
|
76
|
+
ts.isNewExpression(current) &&
|
|
77
|
+
ts.isIdentifier(current.expression) &&
|
|
78
|
+
WORKLOAD_KINDS.has(current.expression.text)
|
|
79
|
+
) {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
current = current.parent;
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function visit(node: ts.Node): void {
|
|
88
|
+
// Look for property assignments like `image: "nginx:latest"` or `image: "nginx"`
|
|
89
|
+
if (
|
|
90
|
+
ts.isPropertyAssignment(node) &&
|
|
91
|
+
ts.isIdentifier(node.name) &&
|
|
92
|
+
node.name.text === "image" &&
|
|
93
|
+
ts.isStringLiteral(node.initializer) &&
|
|
94
|
+
isInsideWorkloadConstructor(node)
|
|
95
|
+
) {
|
|
96
|
+
const value = node.initializer.text;
|
|
97
|
+
if (isProblematicImage(value)) {
|
|
98
|
+
const { line, character } =
|
|
99
|
+
sourceFile.getLineAndCharacterOfPosition(
|
|
100
|
+
node.initializer.getStart(),
|
|
101
|
+
);
|
|
102
|
+
const isLatest = value.endsWith(":latest");
|
|
103
|
+
diagnostics.push({
|
|
104
|
+
file: sourceFile.fileName,
|
|
105
|
+
line: line + 1,
|
|
106
|
+
column: character + 1,
|
|
107
|
+
ruleId: "WK8002",
|
|
108
|
+
severity: "warning",
|
|
109
|
+
message: isLatest
|
|
110
|
+
? `Container image "${value}" uses the :latest tag. Pin to a specific version for reproducibility.`
|
|
111
|
+
: `Container image "${value}" has no tag. Pin to a specific version for reproducibility.`,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
ts.forEachChild(node, visit);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
visit(sourceFile);
|
|
119
|
+
return diagnostics;
|
|
120
|
+
},
|
|
121
|
+
};
|