@intentius/chant-lexicon-k8s 0.37.3 → 0.39.0
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/codegen/generate-list-map-keys.d.ts +53 -0
- package/dist/codegen/generate-list-map-keys.d.ts.map +1 -0
- package/dist/codegen/generate.d.ts.map +1 -1
- package/dist/composites/adot-collector.d.ts +7 -1
- package/dist/composites/adot-collector.d.ts.map +1 -1
- package/dist/composites/agic-ingress.d.ts +3 -1
- package/dist/composites/agic-ingress.d.ts.map +1 -1
- package/dist/composites/aks-external-dns-agent.d.ts +6 -1
- package/dist/composites/aks-external-dns-agent.d.ts.map +1 -1
- package/dist/composites/alb-ingress.d.ts +3 -1
- package/dist/composites/alb-ingress.d.ts.map +1 -1
- package/dist/composites/autoscaled-service.d.ts +6 -1
- package/dist/composites/autoscaled-service.d.ts.map +1 -1
- package/dist/composites/azure-disk-storage-class.d.ts +3 -1
- package/dist/composites/azure-disk-storage-class.d.ts.map +1 -1
- package/dist/composites/azure-file-storage-class.d.ts +3 -1
- package/dist/composites/azure-file-storage-class.d.ts.map +1 -1
- package/dist/composites/azure-monitor-collector.d.ts +7 -1
- package/dist/composites/azure-monitor-collector.d.ts.map +1 -1
- package/dist/composites/batch-job.d.ts +1 -1
- package/dist/composites/batch-job.d.ts.map +1 -1
- package/dist/composites/cockroachdb-cluster.d.ts +13 -1
- package/dist/composites/cockroachdb-cluster.d.ts.map +1 -1
- package/dist/composites/config-connector-context.d.ts +3 -1
- package/dist/composites/config-connector-context.d.ts.map +1 -1
- package/dist/composites/configured-app.d.ts +1 -1
- package/dist/composites/configured-app.d.ts.map +1 -1
- package/dist/composites/cron-workload.d.ts +6 -1
- package/dist/composites/cron-workload.d.ts.map +1 -1
- package/dist/composites/ebs-storage-class.d.ts +3 -1
- package/dist/composites/ebs-storage-class.d.ts.map +1 -1
- package/dist/composites/efs-storage-class.d.ts +3 -1
- package/dist/composites/efs-storage-class.d.ts.map +1 -1
- package/dist/composites/external-dns-agent.d.ts +6 -1
- package/dist/composites/external-dns-agent.d.ts.map +1 -1
- package/dist/composites/filestore-storage-class.d.ts +3 -1
- package/dist/composites/filestore-storage-class.d.ts.map +1 -1
- package/dist/composites/fluent-bit-agent.d.ts +7 -1
- package/dist/composites/fluent-bit-agent.d.ts.map +1 -1
- package/dist/composites/gce-ingress.d.ts +3 -1
- package/dist/composites/gce-ingress.d.ts.map +1 -1
- package/dist/composites/gce-pd-storage-class.d.ts +3 -1
- package/dist/composites/gce-pd-storage-class.d.ts.map +1 -1
- package/dist/composites/gke-external-dns-agent.d.ts +6 -1
- package/dist/composites/gke-external-dns-agent.d.ts.map +1 -1
- package/dist/composites/gke-fluent-bit-agent.d.ts +7 -1
- package/dist/composites/gke-fluent-bit-agent.d.ts.map +1 -1
- package/dist/composites/gke-gateway.d.ts +4 -1
- package/dist/composites/gke-gateway.d.ts.map +1 -1
- package/dist/composites/gke-otel-collector.d.ts +7 -1
- package/dist/composites/gke-otel-collector.d.ts.map +1 -1
- package/dist/composites/irsa-service-account.d.ts +1 -1
- package/dist/composites/irsa-service-account.d.ts.map +1 -1
- package/dist/composites/metrics-server.d.ts +10 -1
- package/dist/composites/metrics-server.d.ts.map +1 -1
- package/dist/composites/monitored-service.d.ts +1 -1
- package/dist/composites/monitored-service.d.ts.map +1 -1
- package/dist/composites/namespace-env.d.ts +1 -1
- package/dist/composites/namespace-env.d.ts.map +1 -1
- package/dist/composites/network-isolated-app.d.ts +5 -1
- package/dist/composites/network-isolated-app.d.ts.map +1 -1
- package/dist/composites/node-agent.d.ts +1 -1
- package/dist/composites/node-agent.d.ts.map +1 -1
- package/dist/composites/ray-cluster.d.ts +10 -1
- package/dist/composites/ray-cluster.d.ts.map +1 -1
- package/dist/composites/ray-job.d.ts +8 -1
- package/dist/composites/ray-job.d.ts.map +1 -1
- package/dist/composites/ray-service.d.ts +10 -1
- package/dist/composites/ray-service.d.ts.map +1 -1
- package/dist/composites/secure-ingress.d.ts +1 -1
- package/dist/composites/secure-ingress.d.ts.map +1 -1
- package/dist/composites/sidecar-app.d.ts +4 -1
- package/dist/composites/sidecar-app.d.ts.map +1 -1
- package/dist/composites/stateful-app.d.ts +1 -1
- package/dist/composites/stateful-app.d.ts.map +1 -1
- package/dist/composites/web-app.d.ts +1 -1
- package/dist/composites/web-app.d.ts.map +1 -1
- package/dist/composites/worker-pool.d.ts +1 -1
- package/dist/composites/worker-pool.d.ts.map +1 -1
- package/dist/composites/workload-identity-sa.d.ts +1 -1
- package/dist/composites/workload-identity-sa.d.ts.map +1 -1
- package/dist/composites/workload-identity-service-account.d.ts +1 -1
- package/dist/composites/workload-identity-service-account.d.ts.map +1 -1
- package/dist/config-schema.d.ts +40 -0
- package/dist/config-schema.d.ts.map +1 -0
- package/dist/config.d.ts +22 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/crd/crd-sources.d.ts.map +1 -1
- package/dist/crd/parser.d.ts.map +1 -1
- package/dist/deep-observe-hooks.d.ts +15 -18
- package/dist/deep-observe-hooks.d.ts.map +1 -1
- package/dist/deep-observe.d.ts.map +1 -1
- package/dist/describe-resources.d.ts +1 -0
- package/dist/describe-resources.d.ts.map +1 -1
- package/dist/generated/index.d.ts +134 -0
- package/dist/generated/index.d.ts.map +1 -1
- package/dist/integrity.json +4 -4
- package/dist/manifest.json +1 -1
- package/dist/meta.json +1086 -45
- package/dist/plugin.d.ts.map +1 -1
- package/dist/serializer.d.ts.map +1 -1
- package/dist/spec/parse.d.ts +17 -0
- package/dist/spec/parse.d.ts.map +1 -1
- package/dist/types/index.d.ts +229 -0
- package/package.json +3 -2
- package/src/codegen/generate-list-map-keys.test.ts +112 -0
- package/src/codegen/generate-list-map-keys.ts +99 -0
- package/src/codegen/generate.ts +9 -1
- package/src/composites/adot-collector.ts +1 -1
- package/src/composites/agic-ingress.ts +1 -1
- package/src/composites/aks-external-dns-agent.ts +1 -1
- package/src/composites/alb-ingress.ts +1 -1
- package/src/composites/autoscaled-service.ts +1 -1
- package/src/composites/azure-disk-storage-class.ts +1 -1
- package/src/composites/azure-file-storage-class.ts +1 -1
- package/src/composites/azure-monitor-collector.ts +1 -1
- package/src/composites/batch-job.ts +1 -1
- package/src/composites/cockroachdb-cluster.ts +1 -1
- package/src/composites/config-connector-context.ts +1 -1
- package/src/composites/configured-app.ts +1 -1
- package/src/composites/cron-workload.ts +1 -1
- package/src/composites/ebs-storage-class.ts +1 -1
- package/src/composites/efs-storage-class.ts +1 -1
- package/src/composites/external-dns-agent.ts +1 -1
- package/src/composites/filestore-storage-class.ts +1 -1
- package/src/composites/fluent-bit-agent.ts +1 -1
- package/src/composites/gce-ingress.ts +1 -1
- package/src/composites/gce-pd-storage-class.ts +1 -1
- package/src/composites/gke-external-dns-agent.ts +1 -1
- package/src/composites/gke-fluent-bit-agent.ts +1 -1
- package/src/composites/gke-gateway.ts +1 -1
- package/src/composites/gke-otel-collector.ts +1 -1
- package/src/composites/irsa-service-account.ts +1 -1
- package/src/composites/metrics-server.ts +1 -1
- package/src/composites/monitored-service.ts +1 -1
- package/src/composites/namespace-env.ts +1 -1
- package/src/composites/network-isolated-app.ts +1 -1
- package/src/composites/node-agent.ts +1 -1
- package/src/composites/ray-cluster.ts +1 -1
- package/src/composites/ray-job.ts +1 -1
- package/src/composites/ray-service.ts +1 -1
- package/src/composites/secure-ingress.ts +1 -1
- package/src/composites/sidecar-app.ts +1 -1
- package/src/composites/stateful-app.ts +1 -1
- package/src/composites/web-app.ts +1 -1
- package/src/composites/worker-pool.ts +1 -1
- package/src/composites/workload-identity-sa.ts +1 -1
- package/src/composites/workload-identity-service-account.ts +1 -1
- package/src/config-schema.ts +46 -0
- package/src/config.ts +25 -1
- package/src/crd/cnpg.test.ts +186 -0
- package/src/crd/crd-sources.ts +123 -0
- package/src/crd/infisical.test.ts +100 -0
- package/src/crd/parser.ts +8 -0
- package/src/crd/traefik.test.ts +108 -0
- package/src/deep-observe-hooks.ts +90 -10
- package/src/deep-observe.ts +7 -0
- package/src/describe-resources.test.ts +138 -1
- package/src/describe-resources.ts +98 -0
- package/src/generated/index.d.ts +229 -0
- package/src/generated/index.ts +134 -0
- package/src/generated/lexicon-k8s.json +1086 -45
- package/src/generated/list-map-keys.json +166 -0
- package/src/generated/operations.json +288 -0
- package/src/lifecycle-integration.test.ts +1 -0
- package/src/list-map-order-key.test.ts +119 -0
- package/src/plugin.ts +4 -0
- package/src/serializer.ts +4 -0
- package/src/spec/parse.test.ts +42 -0
- package/src/spec/parse.ts +51 -2
package/src/crd/crd-sources.ts
CHANGED
|
@@ -93,6 +93,111 @@ const COCKROACH_OPERATOR_CRD_BASE = `https://raw.githubusercontent.com/cockroach
|
|
|
93
93
|
const CERT_MANAGER_VERSION = "v1.16.2";
|
|
94
94
|
const CERT_MANAGER_CRD_BUNDLE = `https://github.com/cert-manager/cert-manager/releases/download/${CERT_MANAGER_VERSION}/cert-manager.crds.yaml`;
|
|
95
95
|
|
|
96
|
+
/**
|
|
97
|
+
* CloudNativePG CRDs — postgresql.cnpg.io/v1
|
|
98
|
+
*
|
|
99
|
+
* The Postgres operator. Produces, under the `Cnpg` namespace (both groups are
|
|
100
|
+
* mapped there explicitly in parser.ts; the first-segment rule would otherwise
|
|
101
|
+
* split them into `Postgresql` and `Barmancloud`):
|
|
102
|
+
* K8s::Cnpg::Cluster → postgresql.cnpg.io/v1
|
|
103
|
+
* K8s::Cnpg::ScheduledBackup → postgresql.cnpg.io/v1
|
|
104
|
+
* K8s::Cnpg::Backup → postgresql.cnpg.io/v1
|
|
105
|
+
* K8s::Cnpg::Pooler → postgresql.cnpg.io/v1
|
|
106
|
+
*
|
|
107
|
+
* Pinned to 1.29.1 rather than latest (1.30.0) because that is the operator a
|
|
108
|
+
* real consumer runs — BinaryBourbon/fountain's k8s overlay.
|
|
109
|
+
*
|
|
110
|
+
* Note on ScheduledBackup.schedule: it is a **six**-field cron with leading
|
|
111
|
+
* seconds, not the five-field form Kubernetes CronJob takes. The schema types
|
|
112
|
+
* it as a plain string, so a five-field value passes every check here and
|
|
113
|
+
* silently means something else on the cluster.
|
|
114
|
+
*
|
|
115
|
+
* Database, Publication and Subscription are deliberately left out — no known
|
|
116
|
+
* consumer needs them, and pulling a whole provider is what the note at the top
|
|
117
|
+
* of this file warns against.
|
|
118
|
+
*
|
|
119
|
+
* Operator install: kubectl apply --server-side -f
|
|
120
|
+
* https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/release-1.29/releases/cnpg-1.29.1.yaml
|
|
121
|
+
*/
|
|
122
|
+
const CNPG_VERSION = "v1.29.1";
|
|
123
|
+
const CNPG_CRD_BASE = `https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/${CNPG_VERSION}/config/crd/bases`;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* barman-cloud plugin CRD — barmancloud.cnpg.io/v1
|
|
127
|
+
*
|
|
128
|
+
* CNPG's WAL archiving / object-storage backend, shipped as a separate plugin
|
|
129
|
+
* with its own release train. Produces:
|
|
130
|
+
* K8s::Cnpg::ObjectStore → barmancloud.cnpg.io/v1
|
|
131
|
+
*
|
|
132
|
+
* A CNPG Cluster reaches it through `spec.plugins[]` — `name:
|
|
133
|
+
* barman-cloud.cloudnative-pg.io`, `isWALArchiver: true`,
|
|
134
|
+
* `parameters.barmanObjectName` naming the ObjectStore — so the two are only
|
|
135
|
+
* ever useful together.
|
|
136
|
+
*
|
|
137
|
+
* Pinned to 0.14.0 (latest). Its ObjectStore schema is a superset of 0.7.0's:
|
|
138
|
+
* same four spec properties, more detail inside them.
|
|
139
|
+
*
|
|
140
|
+
* Plugin install: kubectl apply -f
|
|
141
|
+
* https://github.com/cloudnative-pg/plugin-barman-cloud/releases/download/v0.14.0/manifest.yaml
|
|
142
|
+
*/
|
|
143
|
+
const BARMAN_PLUGIN_VERSION = "v0.14.0";
|
|
144
|
+
const BARMAN_PLUGIN_CRD_BASE = `https://raw.githubusercontent.com/cloudnative-pg/plugin-barman-cloud/${BARMAN_PLUGIN_VERSION}/config/crd/bases`;
|
|
145
|
+
/**
|
|
146
|
+
* Traefik proxy CRDs — traefik.io/v1alpha1
|
|
147
|
+
*
|
|
148
|
+
* Traefik's own routing surface. An `IngressRoute` is not a
|
|
149
|
+
* `networking.k8s.io` Ingress with annotations — it is a distinct CRD with its
|
|
150
|
+
* own matcher grammar, so an estate fronted by Traefik has no expressible edge
|
|
151
|
+
* without these. Produces, under the `Traefik` namespace (first-segment rule,
|
|
152
|
+
* no override needed):
|
|
153
|
+
* K8s::Traefik::IngressRoute K8s::Traefik::Middleware
|
|
154
|
+
* K8s::Traefik::IngressRouteTCP K8s::Traefik::MiddlewareTCP
|
|
155
|
+
* K8s::Traefik::IngressRouteUDP K8s::Traefik::ServersTransport
|
|
156
|
+
* K8s::Traefik::TLSOption K8s::Traefik::ServersTransportTCP
|
|
157
|
+
* K8s::Traefik::TLSStore K8s::Traefik::TraefikService
|
|
158
|
+
*
|
|
159
|
+
* Only the `traefik.io` group. The chart's crds/ directory also ships
|
|
160
|
+
* `hub.traefik.io_*` (Traefik Hub, a different commercial product, ~12 kinds,
|
|
161
|
+
* no known consumer) and a vendored copy of the Gateway API, which is already
|
|
162
|
+
* generated here from its upstream repo and would collide.
|
|
163
|
+
*
|
|
164
|
+
* Version footgun: the pin below is the **chart** version, and the chart
|
|
165
|
+
* version is not the Traefik version. Chart v41.1.0 ships Traefik v3.7.9.
|
|
166
|
+
* When bumping, record both.
|
|
167
|
+
*
|
|
168
|
+
* Controller install: helm repo add traefik https://traefik.github.io/charts
|
|
169
|
+
* && helm install traefik traefik/traefik --version 41.1.0
|
|
170
|
+
*/
|
|
171
|
+
const TRAEFIK_CHART_VERSION = "v41.1.0";
|
|
172
|
+
const TRAEFIK_CRD_BASE = `https://raw.githubusercontent.com/traefik/traefik-helm-chart/${TRAEFIK_CHART_VERSION}/traefik/crds`;
|
|
173
|
+
/**
|
|
174
|
+
* Infisical operator CRDs — secrets.infisical.com/v1alpha1
|
|
175
|
+
*
|
|
176
|
+
* The declarative form of chant's own secrets boundary. An `InfisicalSecret`
|
|
177
|
+
* says where a secret comes from and who may fetch it; it never carries a
|
|
178
|
+
* value. Without it an estate can declare the Deployment that consumes a
|
|
179
|
+
* Secret but not the thing that makes the Secret exist. Produces, under the
|
|
180
|
+
* `Infisical` namespace:
|
|
181
|
+
* K8s::Infisical::InfisicalSecret
|
|
182
|
+
* K8s::Infisical::InfisicalPushSecret
|
|
183
|
+
* K8s::Infisical::InfisicalDynamicSecret
|
|
184
|
+
*
|
|
185
|
+
* The group is overridden to `Infisical` in parser.ts. The first-segment rule
|
|
186
|
+
* would give `K8s::Secrets::InfisicalSecret`, which reads like a core Secret
|
|
187
|
+
* next to the `K8s::Core::Secret` that already exists.
|
|
188
|
+
*
|
|
189
|
+
* Two things about the URL, both easy to lose an hour to:
|
|
190
|
+
* - The operator lives in its own repo, `Infisical/kubernetes-operator`.
|
|
191
|
+
* The path under the `Infisical/infisical` monorepo 404s.
|
|
192
|
+
* - The tag is path-shaped — `infisical-k8-operator/v0.11.7`. Not a typo,
|
|
193
|
+
* and not a directory.
|
|
194
|
+
*
|
|
195
|
+
* Operator install: helm repo add infisical https://dl.cloudsmith.io/public/infisical/helm-charts/helm/charts/
|
|
196
|
+
* && helm install infisical-secrets-operator infisical/secrets-operator
|
|
197
|
+
*/
|
|
198
|
+
const INFISICAL_OPERATOR_VERSION = "infisical-k8-operator/v0.11.7";
|
|
199
|
+
const INFISICAL_CRD_BASE = `https://raw.githubusercontent.com/Infisical/kubernetes-operator/${INFISICAL_OPERATOR_VERSION}/config/crd/bases`;
|
|
200
|
+
|
|
96
201
|
/**
|
|
97
202
|
* Prometheus Operator CRDs — monitoring.coreos.com/v1
|
|
98
203
|
*
|
|
@@ -188,6 +293,24 @@ export const CRD_SOURCES: CRDSource[] = [
|
|
|
188
293
|
{ type: "url", url: CERT_MANAGER_CRD_BUNDLE },
|
|
189
294
|
{ type: "url", url: `${PROM_OPERATOR_CRD_BASE}/monitoring.coreos.com_servicemonitors.yaml` },
|
|
190
295
|
{ type: "url", url: `${PROM_OPERATOR_CRD_BASE}/monitoring.coreos.com_prometheusrules.yaml` },
|
|
296
|
+
{ type: "url", url: `${CNPG_CRD_BASE}/postgresql.cnpg.io_clusters.yaml` },
|
|
297
|
+
{ type: "url", url: `${CNPG_CRD_BASE}/postgresql.cnpg.io_scheduledbackups.yaml` },
|
|
298
|
+
{ type: "url", url: `${CNPG_CRD_BASE}/postgresql.cnpg.io_backups.yaml` },
|
|
299
|
+
{ type: "url", url: `${CNPG_CRD_BASE}/postgresql.cnpg.io_poolers.yaml` },
|
|
300
|
+
{ type: "url", url: `${BARMAN_PLUGIN_CRD_BASE}/barmancloud.cnpg.io_objectstores.yaml` },
|
|
301
|
+
{ type: "url", url: `${TRAEFIK_CRD_BASE}/traefik.io_ingressroutes.yaml` },
|
|
302
|
+
{ type: "url", url: `${TRAEFIK_CRD_BASE}/traefik.io_ingressroutetcps.yaml` },
|
|
303
|
+
{ type: "url", url: `${TRAEFIK_CRD_BASE}/traefik.io_ingressrouteudps.yaml` },
|
|
304
|
+
{ type: "url", url: `${TRAEFIK_CRD_BASE}/traefik.io_middlewares.yaml` },
|
|
305
|
+
{ type: "url", url: `${TRAEFIK_CRD_BASE}/traefik.io_middlewaretcps.yaml` },
|
|
306
|
+
{ type: "url", url: `${TRAEFIK_CRD_BASE}/traefik.io_serverstransports.yaml` },
|
|
307
|
+
{ type: "url", url: `${TRAEFIK_CRD_BASE}/traefik.io_serverstransporttcps.yaml` },
|
|
308
|
+
{ type: "url", url: `${TRAEFIK_CRD_BASE}/traefik.io_tlsoptions.yaml` },
|
|
309
|
+
{ type: "url", url: `${TRAEFIK_CRD_BASE}/traefik.io_tlsstores.yaml` },
|
|
310
|
+
{ type: "url", url: `${TRAEFIK_CRD_BASE}/traefik.io_traefikservices.yaml` },
|
|
311
|
+
{ type: "url", url: `${INFISICAL_CRD_BASE}/secrets.infisical.com_infisicalsecrets.yaml` },
|
|
312
|
+
{ type: "url", url: `${INFISICAL_CRD_BASE}/secrets.infisical.com_infisicalpushsecrets.yaml` },
|
|
313
|
+
{ type: "url", url: `${INFISICAL_CRD_BASE}/secrets.infisical.com_infisicaldynamicsecrets.yaml` },
|
|
191
314
|
{
|
|
192
315
|
type: "url",
|
|
193
316
|
url: FLUX_TOOLKIT_INSTALL,
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Infisical acceptance test.
|
|
3
|
+
*
|
|
4
|
+
* Transcribed from fountain's `k8s/infisicalsecret.yaml`. Every field in that
|
|
5
|
+
* manifest is policy — where the secret comes from, who may fetch it, what
|
|
6
|
+
* happens to the materialized Secret when the CR goes away. None of it is a
|
|
7
|
+
* value, which is why this kind is expressible in chant at all.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, test, expect } from "vitest";
|
|
11
|
+
import { InfisicalSecret } from "../generated";
|
|
12
|
+
import { k8sSerializer } from "../serializer";
|
|
13
|
+
import { parseYAML } from "@intentius/chant/yaml";
|
|
14
|
+
|
|
15
|
+
function synth(logicalName: string, resource: unknown): any {
|
|
16
|
+
const yaml = k8sSerializer.serialize(new Map([[logicalName, resource as never]]));
|
|
17
|
+
return parseYAML(yaml);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const fountainSecrets = new InfisicalSecret({
|
|
21
|
+
metadata: { name: "fountain-secrets", namespace: "fountain", labels: { app: "fountain" } },
|
|
22
|
+
spec: {
|
|
23
|
+
hostAPI: "http://infisical.infisical.svc.cluster.local:8080",
|
|
24
|
+
resyncInterval: 60,
|
|
25
|
+
authentication: {
|
|
26
|
+
kubernetesAuth: {
|
|
27
|
+
identityId: "6ccf748c-7fe1-4584-a206-b0a56d84530c",
|
|
28
|
+
autoCreateServiceAccountToken: true,
|
|
29
|
+
serviceAccountRef: { name: "fountain-infisical", namespace: "fountain" },
|
|
30
|
+
secretsScope: { projectSlug: "fountain-q-cwt", envSlug: "prod", secretsPath: "/" },
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
managedSecretReference: {
|
|
34
|
+
secretName: "fountain-secrets",
|
|
35
|
+
secretNamespace: "fountain",
|
|
36
|
+
creationPolicy: "Orphan",
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("InfisicalSecret", () => {
|
|
42
|
+
test("carries the Infisical apiVersion and kind", () => {
|
|
43
|
+
const doc = synth("fountainSecrets", fountainSecrets);
|
|
44
|
+
expect(doc.apiVersion).toBe("secrets.infisical.com/v1alpha1");
|
|
45
|
+
expect(doc.kind).toBe("InfisicalSecret");
|
|
46
|
+
expect(doc.metadata.name).toBe("fountain-secrets");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("reproduces fountain's infisicalsecret.yaml", () => {
|
|
50
|
+
const { spec } = synth("fountainSecrets", fountainSecrets);
|
|
51
|
+
expect(spec.hostAPI).toBe("http://infisical.infisical.svc.cluster.local:8080");
|
|
52
|
+
expect(spec.resyncInterval).toBe(60);
|
|
53
|
+
expect(spec.managedSecretReference).toEqual({
|
|
54
|
+
secretName: "fountain-secrets",
|
|
55
|
+
secretNamespace: "fountain",
|
|
56
|
+
creationPolicy: "Orphan",
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("serializes the whole kubernetesAuth block", () => {
|
|
61
|
+
const { spec } = synth("fountainSecrets", fountainSecrets);
|
|
62
|
+
// Kubernetes-native auth: the point is that no credential is stored
|
|
63
|
+
// anywhere. The operator presents a ServiceAccount token instead, so
|
|
64
|
+
// dropping any part of this block is what turns the deployment back into
|
|
65
|
+
// one that needs a secret to fetch its secrets.
|
|
66
|
+
expect(spec.authentication.kubernetesAuth).toEqual({
|
|
67
|
+
identityId: "6ccf748c-7fe1-4584-a206-b0a56d84530c",
|
|
68
|
+
autoCreateServiceAccountToken: true,
|
|
69
|
+
serviceAccountRef: { name: "fountain-infisical", namespace: "fountain" },
|
|
70
|
+
secretsScope: { projectSlug: "fountain-q-cwt", envSlug: "prod", secretsPath: "/" },
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("declares a source and never a value", () => {
|
|
75
|
+
// The reason this CRD fits chant at all: synthesis resolves nothing here.
|
|
76
|
+
// If a secret value could reach the manifest, the manifest would stop
|
|
77
|
+
// being safe to commit -- so assert on the serialized text, not the props.
|
|
78
|
+
const yaml = k8sSerializer.serialize(
|
|
79
|
+
new Map([["fountainSecrets", fountainSecrets as never]]),
|
|
80
|
+
);
|
|
81
|
+
expect(yaml).not.toMatch(/\bvalue\s*:/);
|
|
82
|
+
expect(yaml).not.toMatch(/\bdata\s*:/);
|
|
83
|
+
expect(yaml).not.toMatch(/\bstringData\s*:/);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("keeps resyncInterval a number", () => {
|
|
87
|
+
const { spec } = synth("fountainSecrets", fountainSecrets);
|
|
88
|
+
// Quoted, the operator rejects the CR rather than defaulting -- worth
|
|
89
|
+
// pinning because seconds-as-a-string is a habit from other tools.
|
|
90
|
+
expect(typeof spec.resyncInterval).toBe("number");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("creationPolicy Orphan survives, since the two policies differ on deletion", () => {
|
|
94
|
+
const { spec } = synth("fountainSecrets", fountainSecrets);
|
|
95
|
+
// Orphan leaves the materialized Secret behind when the CR is deleted;
|
|
96
|
+
// Owner takes it with it. Silently flipping to the default would make CR
|
|
97
|
+
// deletion destructive.
|
|
98
|
+
expect(spec.managedSecretReference.creationPolicy).toBe("Orphan");
|
|
99
|
+
});
|
|
100
|
+
});
|
package/src/crd/parser.ts
CHANGED
|
@@ -44,6 +44,14 @@ const GROUP_NAMESPACE_OVERRIDES: Record<string, string> = {
|
|
|
44
44
|
"image.toolkit.fluxcd.io": "Flux",
|
|
45
45
|
"fluxcd.controlplane.io": "Flux",
|
|
46
46
|
"lambda.aws.amazon.com": "KubeMicroVM",
|
|
47
|
+
// CNPG and its barman-cloud plugin ship under two groups but are one thing to
|
|
48
|
+
// an author: a Cluster's `plugins[]` names an ObjectStore. The first-segment
|
|
49
|
+
// rule would scatter them into `Postgresql` and `Barmancloud`.
|
|
50
|
+
"postgresql.cnpg.io": "Cnpg",
|
|
51
|
+
"barmancloud.cnpg.io": "Cnpg",
|
|
52
|
+
// Not `Secrets`: `K8s::Secrets::InfisicalSecret` reads like a core Secret,
|
|
53
|
+
// and `K8s::Core::Secret` is right there to be confused with.
|
|
54
|
+
"secrets.infisical.com": "Infisical",
|
|
47
55
|
};
|
|
48
56
|
|
|
49
57
|
/**
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Traefik acceptance test.
|
|
3
|
+
*
|
|
4
|
+
* Transcribed from fountain's `k8s/ingressroute.yaml` — the websecure route
|
|
5
|
+
* with `tls.secretName`, and the web route whose middleware lives in another
|
|
6
|
+
* namespace. The cross-namespace reference is the reason this file exists: it
|
|
7
|
+
* is one extra key, it is easy to leave off, and leaving it off produces a
|
|
8
|
+
* route that applies cleanly and drops traffic.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, test, expect } from "vitest";
|
|
12
|
+
import { IngressRoute, Middleware } from "../generated";
|
|
13
|
+
import { k8sSerializer } from "../serializer";
|
|
14
|
+
import { parseYAML } from "@intentius/chant/yaml";
|
|
15
|
+
|
|
16
|
+
function synth(logicalName: string, resource: unknown): any {
|
|
17
|
+
const yaml = k8sSerializer.serialize(new Map([[logicalName, resource as never]]));
|
|
18
|
+
return parseYAML(yaml);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe("Traefik IngressRoute", () => {
|
|
22
|
+
const secure = new IngressRoute({
|
|
23
|
+
metadata: { name: "fountain", namespace: "fountain", labels: { app: "fountain" } },
|
|
24
|
+
spec: {
|
|
25
|
+
entryPoints: ["websecure"],
|
|
26
|
+
routes: [
|
|
27
|
+
{
|
|
28
|
+
match: "Host(`fountain.inevitable.fyi`)",
|
|
29
|
+
kind: "Rule",
|
|
30
|
+
services: [{ name: "fountain", port: 80 }],
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
tls: { secretName: "fountain-tls" },
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const plain = new IngressRoute({
|
|
38
|
+
metadata: { name: "fountain-http", namespace: "fountain", labels: { app: "fountain" } },
|
|
39
|
+
spec: {
|
|
40
|
+
entryPoints: ["web"],
|
|
41
|
+
routes: [
|
|
42
|
+
{
|
|
43
|
+
match: "Host(`fountain.inevitable.fyi`)",
|
|
44
|
+
kind: "Rule",
|
|
45
|
+
middlewares: [{ name: "redirect-https", namespace: "default" }],
|
|
46
|
+
services: [{ name: "fountain", port: 80 }],
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("carries the Traefik apiVersion and kind", () => {
|
|
53
|
+
const doc = synth("fountain", secure);
|
|
54
|
+
expect(doc.apiVersion).toBe("traefik.io/v1alpha1");
|
|
55
|
+
expect(doc.kind).toBe("IngressRoute");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("reproduces the websecure route", () => {
|
|
59
|
+
const { spec } = synth("fountain", secure);
|
|
60
|
+
expect(spec.entryPoints).toEqual(["websecure"]);
|
|
61
|
+
expect(spec.tls).toEqual({ secretName: "fountain-tls" });
|
|
62
|
+
expect(spec.routes).toHaveLength(1);
|
|
63
|
+
expect(spec.routes[0].kind).toBe("Rule");
|
|
64
|
+
expect(spec.routes[0].services).toEqual([{ name: "fountain", port: 80 }]);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("preserves the matcher grammar verbatim", () => {
|
|
68
|
+
const { spec } = synth("fountain", secure);
|
|
69
|
+
// Traefik's matcher is its own language, and the backticks around the host
|
|
70
|
+
// are part of it -- not YAML quoting. Anything that strips or re-quotes
|
|
71
|
+
// them produces a rule that parses and matches nothing.
|
|
72
|
+
expect(spec.routes[0].match).toBe("Host(`fountain.inevitable.fyi`)");
|
|
73
|
+
expect(spec.routes[0].match).toContain("`fountain.inevitable.fyi`");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("keeps the namespace on a cross-namespace middleware ref", () => {
|
|
77
|
+
const { spec } = synth("fountainHttp", plain);
|
|
78
|
+
// `redirect-https` lives in `default`, the route in `fountain`. Without the
|
|
79
|
+
// qualifier Traefik looks in the route's own namespace, finds nothing, and
|
|
80
|
+
// serves the route with no redirect -- so plain HTTP quietly keeps working
|
|
81
|
+
// instead of upgrading.
|
|
82
|
+
expect(spec.routes[0].middlewares).toEqual([
|
|
83
|
+
{ name: "redirect-https", namespace: "default" },
|
|
84
|
+
]);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("a route with no TLS omits the key rather than emptying it", () => {
|
|
88
|
+
const { spec } = synth("fountainHttp", plain);
|
|
89
|
+
expect(spec.tls).toBeUndefined();
|
|
90
|
+
expect(spec.entryPoints).toEqual(["web"]);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe("Traefik Middleware", () => {
|
|
95
|
+
test("serializes the redirect the routes point at", () => {
|
|
96
|
+
const doc = synth(
|
|
97
|
+
"redirectHttps",
|
|
98
|
+
new Middleware({
|
|
99
|
+
metadata: { name: "redirect-https", namespace: "default" },
|
|
100
|
+
spec: { redirectScheme: { scheme: "https", permanent: true } },
|
|
101
|
+
}),
|
|
102
|
+
);
|
|
103
|
+
expect(doc.apiVersion).toBe("traefik.io/v1alpha1");
|
|
104
|
+
expect(doc.kind).toBe("Middleware");
|
|
105
|
+
expect(doc.metadata.namespace).toBe("default");
|
|
106
|
+
expect(doc.spec.redirectScheme).toEqual({ scheme: "https", permanent: true });
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
* #1074 made that package's reachability from the build path a structural
|
|
12
12
|
* property (`examples/k8s-client-boundary.test.ts`) rather than a lint rule —
|
|
13
13
|
* so nothing this file exports may pull that package in, directly or
|
|
14
|
-
* transitively. It imports
|
|
14
|
+
* transitively. It imports the core contract's own types, and one generated
|
|
15
|
+
* JSON data file (`./generated/list-map-keys.json`, chant #1441) — data, not a
|
|
16
|
+
* package, so the boundary property is unchanged.
|
|
15
17
|
*
|
|
16
18
|
* What lives here is deliberately the *entityType-keyed, resource-agnostic*
|
|
17
19
|
* half of the rules: which fields the API server always populates regardless
|
|
@@ -39,7 +41,7 @@
|
|
|
39
41
|
* keyed by chant's own k8s entityType catalog.
|
|
40
42
|
*/
|
|
41
43
|
|
|
42
|
-
import type { DeepNode, DeepNormalizationHooks } from "@intentius/chant/lexicon";
|
|
44
|
+
import type { DeepArrayElement, DeepNode, DeepNormalizationHooks } from "@intentius/chant/lexicon";
|
|
43
45
|
import { K8S_OBJECT_ENVELOPE_PRUNE_PATTERNS, k8sListMapOrderKey } from "@intentius/chant/managed-fields";
|
|
44
46
|
|
|
45
47
|
/**
|
|
@@ -96,13 +98,17 @@ function canonicalJson(value: unknown): string {
|
|
|
96
98
|
* The k8s lexicon's static noise rules: the generic Kubernetes object
|
|
97
99
|
* envelope (unconditional, by pattern, `@intentius/chant/managed-fields`) and
|
|
98
100
|
* Kubernetes-defaulted fields (gated on `counterpart === "absent"`), plus the
|
|
99
|
-
* array orderings
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
* `k8sListMapOrderKey`
|
|
101
|
+
* array orderings.
|
|
102
|
+
*
|
|
103
|
+
* The orderings are now taken from the spec itself (chant #1441): codegen
|
|
104
|
+
* carries `x-kubernetes-list-type`/`x-kubernetes-list-map-keys` through into
|
|
105
|
+
* `./generated/list-map-keys.json`, and {@link schemaListMapOrderKey} reads
|
|
106
|
+
* it. The hand-written conventions in
|
|
107
|
+
* `@intentius/chant/managed-fields`'s `k8sListMapOrderKey` — containers by
|
|
108
|
+
* `name`, `env` by `name`, `volumes` by `name`, container/service `ports` by
|
|
109
|
+
* `containerPort`/`port` + `protocol` — remain as the fallback for lists the
|
|
110
|
+
* spec does not annotate, and for readers that have no generated table at all
|
|
111
|
+
* (GCP's Config Connector hooks).
|
|
106
112
|
*
|
|
107
113
|
* This is the object `k8sPlugin.deepNormalizationHooks` is. It is also what
|
|
108
114
|
* `./deep-observe.ts` layers its per-resource managed-fields prune on top of,
|
|
@@ -111,6 +117,80 @@ function canonicalJson(value: unknown): string {
|
|
|
111
117
|
* — see `packages/core/src/lifecycle/deep-observe.ts`) apply the identical
|
|
112
118
|
* entityType-keyed rules either way.
|
|
113
119
|
*/
|
|
120
|
+
/** Property name → candidate key-field sets, generated from the spec (chant #1441). */
|
|
121
|
+
type ListMapKeyTable = Record<string, string[][]>;
|
|
122
|
+
|
|
123
|
+
let cachedListMapKeys: ListMapKeyTable | undefined;
|
|
124
|
+
|
|
125
|
+
function listMapKeyTable(): ListMapKeyTable {
|
|
126
|
+
cachedListMapKeys ??= require("./generated/list-map-keys.json") as ListMapKeyTable;
|
|
127
|
+
return cachedListMapKeys;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Last dotted segment of an index-erased path: `spec.template.spec.containers` → `containers`. */
|
|
131
|
+
function lastSegment(pattern: string): string {
|
|
132
|
+
const at = pattern.lastIndexOf(".");
|
|
133
|
+
return at === -1 ? pattern : pattern.slice(at + 1);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Render one key field's value into a sort key fragment. Numbers are
|
|
138
|
+
* zero-padded so the key orders numerically rather than lexicographically —
|
|
139
|
+
* `"443"` would otherwise sort before `"80"`. Cosmetic, since either order
|
|
140
|
+
* canonicalizes the two sides identically, but a human-sensible order is free
|
|
141
|
+
* to have here (the same reasoning `k8sListMapOrderKey` applies to ports).
|
|
142
|
+
*/
|
|
143
|
+
function keyFragment(value: unknown): string {
|
|
144
|
+
if (typeof value === "number") return String(value).padStart(5, "0");
|
|
145
|
+
return typeof value === "string" ? value : canonicalJson(value);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Join a key set's values the way `k8sListMapOrderKey` already does: the bare
|
|
150
|
+
* values, `/`-separated, with no field names.
|
|
151
|
+
*
|
|
152
|
+
* This is not only a sort key. Core renders it as the element's ADDRESS in
|
|
153
|
+
* drift output — `spec.template.spec.containers[#app].image` — so the format
|
|
154
|
+
* is user-visible and load-bearing. Emitting `name=app` here would rewrite
|
|
155
|
+
* every existing drift path. Bare values keep the six properties that were
|
|
156
|
+
* hardcoded byte-identical, and extend to the rest for free: a container port
|
|
157
|
+
* reads `[#08080/TCP]` exactly as before, a condition reads `[#Ready]`.
|
|
158
|
+
*/
|
|
159
|
+
function joinKeyValues(el: Record<string, unknown>, keys: string[]): string {
|
|
160
|
+
return keys.map((k) => keyFragment(el[k])).join("/");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Order an associative list's element by the identity the SPEC declares for it
|
|
165
|
+
* (chant #1441), falling back to `@intentius/chant/managed-fields`'s hand-written
|
|
166
|
+
* conventions for anything the generated table does not cover.
|
|
167
|
+
*
|
|
168
|
+
* The fallback is not vestigial. The table is built from the properties this
|
|
169
|
+
* lexicon's codegen parsed, so it covers core Kubernetes and any CRD that
|
|
170
|
+
* declares the extensions — but a CRD that declares an associative list
|
|
171
|
+
* WITHOUT `x-kubernetes-list-map-keys`, or a Config Connector resource read
|
|
172
|
+
* through GCP's hooks, still lands on the by-name conventions. Keeping both
|
|
173
|
+
* means widening the spec-derived table can never narrow what was already
|
|
174
|
+
* identified.
|
|
175
|
+
*/
|
|
176
|
+
export function schemaListMapOrderKey(element: DeepArrayElement): string | undefined {
|
|
177
|
+
const candidates = listMapKeyTable()[lastSegment(element.pattern)];
|
|
178
|
+
const el = element.element;
|
|
179
|
+
|
|
180
|
+
if (candidates && isRecordLike(el)) {
|
|
181
|
+
for (const keys of candidates) {
|
|
182
|
+
if (!keys.every((k) => el[k] !== undefined)) continue;
|
|
183
|
+
return joinKeyValues(el, keys);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return k8sListMapOrderKey(element);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function isRecordLike(value: unknown): value is Record<string, unknown> {
|
|
191
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
192
|
+
}
|
|
193
|
+
|
|
114
194
|
export const k8sDeepNormalizationHooks: DeepNormalizationHooks = {
|
|
115
195
|
prune(node: DeepNode): boolean {
|
|
116
196
|
if (K8S_OBJECT_ENVELOPE_PRUNE_PATTERNS.has(node.pattern)) return true;
|
|
@@ -121,5 +201,5 @@ export const k8sDeepNormalizationHooks: DeepNormalizationHooks = {
|
|
|
121
201
|
return canonicalJson(defaults[node.pattern]) === canonicalJson(node.value);
|
|
122
202
|
},
|
|
123
203
|
|
|
124
|
-
orderKey:
|
|
204
|
+
orderKey: schemaListMapOrderKey,
|
|
125
205
|
};
|
package/src/deep-observe.ts
CHANGED
|
@@ -284,6 +284,13 @@ export async function observeResourcesDeepK8s(
|
|
|
284
284
|
side: "live",
|
|
285
285
|
hooks: perResourceHooks(sets),
|
|
286
286
|
}),
|
|
287
|
+
// Who owns each surviving path (#1189). The prune above already
|
|
288
|
+
// dropped confidently-foreign undeclared noise, so what reaches the
|
|
289
|
+
// diff is chant's own fields and contested ones — and for a contested
|
|
290
|
+
// field, naming the manager is the whole question: an operator needs to
|
|
291
|
+
// tell `hpa-controller` doing its job from somebody running
|
|
292
|
+
// `kubectl edit`.
|
|
293
|
+
...(sets.owners.size > 0 ? { fieldOwners: Object.fromEntries(sets.owners) } : {}),
|
|
287
294
|
};
|
|
288
295
|
} catch (err) {
|
|
289
296
|
const outcome = classifyApiFailure(err);
|
|
@@ -15,7 +15,7 @@ vi.mock("@intentius/chant/config", () => ({
|
|
|
15
15
|
loadChantConfig: (...args: unknown[]) => loadChantConfigMock(...args),
|
|
16
16
|
}));
|
|
17
17
|
|
|
18
|
-
const { describeResources } = await import("./describe-resources");
|
|
18
|
+
const { describeResources, statusFromObject, unhappyConditions } = await import("./describe-resources");
|
|
19
19
|
const { fakeCluster, objectKey, ownedObject } = await import("./api/fake-cluster");
|
|
20
20
|
const { defaultK8sConnector } = await import("./api/connect");
|
|
21
21
|
const { fakeKubeconfig, statusBody } = await import("@intentius/chant-k8s-client/testing");
|
|
@@ -725,3 +725,140 @@ describe("k8s describeResources", () => {
|
|
|
725
725
|
});
|
|
726
726
|
});
|
|
727
727
|
});
|
|
728
|
+
|
|
729
|
+
// #1397 — a Pod's phase is `Running` from the moment the kubelet admits it, so
|
|
730
|
+
// the phase alone reported a crashlooping Pod as `Running`, which every
|
|
731
|
+
// consumer classifying that word read as healthy.
|
|
732
|
+
describe("statusFromObject — the most specific failing signal (#1397)", () => {
|
|
733
|
+
test("a crashlooping Pod reports the reason, not the phase", () => {
|
|
734
|
+
expect(
|
|
735
|
+
statusFromObject({
|
|
736
|
+
status: {
|
|
737
|
+
phase: "Running",
|
|
738
|
+
containerStatuses: [{ state: { waiting: { reason: "CrashLoopBackOff", message: "back-off 5m0s" } } }],
|
|
739
|
+
},
|
|
740
|
+
} as never),
|
|
741
|
+
).toBe("CrashLoopBackOff");
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
test("an image-pull failure reports the reason", () => {
|
|
745
|
+
expect(
|
|
746
|
+
statusFromObject({
|
|
747
|
+
status: { phase: "Pending", containerStatuses: [{ state: { waiting: { reason: "ImagePullBackOff" } } }] },
|
|
748
|
+
} as never),
|
|
749
|
+
).toBe("ImagePullBackOff");
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
test("an unschedulable Pod reports Unschedulable, not Pending", () => {
|
|
753
|
+
expect(
|
|
754
|
+
statusFromObject({
|
|
755
|
+
status: {
|
|
756
|
+
phase: "Pending",
|
|
757
|
+
conditions: [{ type: "PodScheduled", status: "False", reason: "Unschedulable" }],
|
|
758
|
+
},
|
|
759
|
+
} as never),
|
|
760
|
+
).toBe("Unschedulable");
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
test("ordinary startup is not promoted over the phase", () => {
|
|
764
|
+
// ContainerCreating and PodInitializing are not failure; surfacing them
|
|
765
|
+
// would be noise, and the phase already classifies as progressing.
|
|
766
|
+
for (const reason of ["ContainerCreating", "PodInitializing"]) {
|
|
767
|
+
expect(
|
|
768
|
+
statusFromObject({
|
|
769
|
+
status: { phase: "Pending", containerStatuses: [{ state: { waiting: { reason } } }] },
|
|
770
|
+
} as never),
|
|
771
|
+
).toBe("Pending");
|
|
772
|
+
}
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
test("a terminated container does not outrank the phase — a Job container exits cleanly", () => {
|
|
776
|
+
expect(
|
|
777
|
+
statusFromObject({
|
|
778
|
+
status: { phase: "Succeeded", containerStatuses: [{ state: { terminated: { reason: "Completed", exitCode: 0 } } }] },
|
|
779
|
+
} as never),
|
|
780
|
+
).toBe("Succeeded");
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
test("a healthy Pod and a ready Deployment are unchanged", () => {
|
|
784
|
+
expect(statusFromObject({ status: { phase: "Running", containerStatuses: [{ state: { running: {} } }] } } as never)).toBe("Running");
|
|
785
|
+
expect(statusFromObject({ status: { readyReplicas: 3, replicas: 3 } } as never)).toBe("READY");
|
|
786
|
+
expect(statusFromObject({ status: { readyReplicas: 1, replicas: 3 } } as never)).toBe("PROGRESSING(1/3)");
|
|
787
|
+
expect(statusFromObject({} as never)).toBe("PRESENT");
|
|
788
|
+
});
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
// #1401 — #1397 made the status WORD honest; this carries the part that says
|
|
792
|
+
// what to do about it. `Unschedulable` is the reason; "0/3 nodes are available"
|
|
793
|
+
// is the answer.
|
|
794
|
+
describe("unhappyConditions (#1401)", () => {
|
|
795
|
+
test("reports an unschedulable Pod's message, not just its reason", () => {
|
|
796
|
+
expect(
|
|
797
|
+
unhappyConditions({
|
|
798
|
+
status: {
|
|
799
|
+
conditions: [
|
|
800
|
+
{
|
|
801
|
+
type: "PodScheduled",
|
|
802
|
+
status: "False",
|
|
803
|
+
reason: "Unschedulable",
|
|
804
|
+
message: "0/3 nodes are available: 1 node(s) had untolerated taint.",
|
|
805
|
+
},
|
|
806
|
+
],
|
|
807
|
+
},
|
|
808
|
+
} as never),
|
|
809
|
+
).toEqual(["PodScheduled=Unschedulable: 0/3 nodes are available: 1 node(s) had untolerated taint."]);
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
test("is absent when every condition is happy — presence means the object has something to say", () => {
|
|
813
|
+
expect(
|
|
814
|
+
unhappyConditions({
|
|
815
|
+
status: { conditions: [{ type: "Ready", status: "True" }, { type: "PodScheduled", status: "True" }] },
|
|
816
|
+
} as never),
|
|
817
|
+
).toBeUndefined();
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
test("knows the happy polarity is per condition type", () => {
|
|
821
|
+
// Ready/Available/PodScheduled are good when True; the *Pressure and
|
|
822
|
+
// NetworkUnavailable node conditions are good when False. Treating them all
|
|
823
|
+
// the same would report every healthy node as unhappy, or hide a node
|
|
824
|
+
// genuinely under disk pressure.
|
|
825
|
+
const healthyNode = {
|
|
826
|
+
status: {
|
|
827
|
+
conditions: [
|
|
828
|
+
{ type: "Ready", status: "True" },
|
|
829
|
+
{ type: "MemoryPressure", status: "False" },
|
|
830
|
+
{ type: "DiskPressure", status: "False" },
|
|
831
|
+
],
|
|
832
|
+
},
|
|
833
|
+
};
|
|
834
|
+
expect(unhappyConditions(healthyNode as never)).toBeUndefined();
|
|
835
|
+
|
|
836
|
+
const pressured = {
|
|
837
|
+
status: {
|
|
838
|
+
conditions: [
|
|
839
|
+
{ type: "Ready", status: "True" },
|
|
840
|
+
{ type: "DiskPressure", status: "True", reason: "KubeletHasDiskPressure" },
|
|
841
|
+
],
|
|
842
|
+
},
|
|
843
|
+
};
|
|
844
|
+
expect(unhappyConditions(pressured as never)).toEqual(["DiskPressure=KubeletHasDiskPressure"]);
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
test("reports every unhappy condition, since a stuck Deployment has two", () => {
|
|
848
|
+
expect(
|
|
849
|
+
unhappyConditions({
|
|
850
|
+
status: {
|
|
851
|
+
conditions: [
|
|
852
|
+
{ type: "Available", status: "False", reason: "MinimumReplicasUnavailable" },
|
|
853
|
+
{ type: "Progressing", status: "False", reason: "ProgressDeadlineExceeded", message: "ReplicaSet has timed out." },
|
|
854
|
+
],
|
|
855
|
+
},
|
|
856
|
+
} as never),
|
|
857
|
+
).toHaveLength(2);
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
test("is absent for an object with no conditions at all", () => {
|
|
861
|
+
expect(unhappyConditions({ status: {} } as never)).toBeUndefined();
|
|
862
|
+
expect(unhappyConditions({} as never)).toBeUndefined();
|
|
863
|
+
});
|
|
864
|
+
});
|