From 077474b46cf2ced2e8dd87dca20462fe8566a08e Mon Sep 17 00:00:00 2001 From: Bryant Biggs Date: Sat, 17 Oct 2026 11:24:07 -0500 Subject: [PATCH 1/4] feat: support EKS 1.36 and 1.37 --- docs/info/checks.md | 12 +++--------- eksup/src/finding.rs | 2 +- eksup/src/k8s/checks.rs | 18 ++++++------------ eksup/src/version.rs | 2 +- eksup/templates/data.yaml | 6 ++++++ 5 files changed, 17 insertions(+), 23 deletions(-) diff --git a/docs/info/checks.md b/docs/info/checks.md index 7ce2ba5..2d03797 100644 --- a/docs/info/checks.md +++ b/docs/info/checks.md @@ -41,7 +41,7 @@ See the [symbol table](https://clowdhaus.github.io/eksup/#symbol-table) for furt | `K8S009` | Pod security policies present (removed in 1.25) | Retired | Up to 1.24 | | `K8S010` | EBS CSI driver not installed | Retired | Up to 1.24 | | `K8S011` | kube-proxy version skew with kubelet | Active | All versions | -| `K8S012` | kube-proxy IPVS mode deprecated (1.35+, removed 1.36) | Active | 1.35+ | +| `K8S012` | kube-proxy IPVS mode deprecated (1.35+, removal planned 1.43) | Active | 1.35+ | | `K8S013` | Ingress NGINX controller retirement (1.35+) | Active | 1.35+ | @@ -346,17 +346,11 @@ If you recently updated your cluster to a new Kubernetes minor version, then upd #### K8S012 -`kube-proxy` IPVS proxy mode is deprecated starting in Kubernetes `v1.35` and will be removed in `v1.36`. Clusters using IPVS mode MUST migrate to `iptables` or `nftables` proxy mode. - -**❌ Remediation required** - -For clusters upgrading to Kubernetes `v1.36` or later — IPVS proxy mode MUST NOT be used as it is removed. - **⚠️ Remediation recommended** -For clusters upgrading to Kubernetes `v1.35` — IPVS proxy mode is deprecated and SHOULD be migrated. +`kube-proxy` IPVS proxy mode is deprecated starting in Kubernetes `v1.35`. It is expected to be disabled by default in `v1.40` and removed in `v1.43`. Clusters using IPVS mode SHOULD migrate to `nftables` or `iptables` proxy mode. -[Kubernetes kube-proxy documentation](https://kubernetes.io/docs/reference/command-line-tools-reference/kube-proxy/) +[Kubernetes kube-proxy documentation](https://kubernetes.io/docs/reference/command-line-tools-reference/kube-proxy/), [KEP-5495](https://github.com/kubernetes/enhancements/issues/5495) #### K8S013 diff --git a/eksup/src/finding.rs b/eksup/src/finding.rs index d358de2..e2fdc86 100644 --- a/eksup/src/finding.rs +++ b/eksup/src/finding.rs @@ -174,7 +174,7 @@ define_codes! { K8S009 => { desc: "Pod security policies present (removed in 1.25)", from: None, until: Some(24) }, K8S010 => { desc: "EBS CSI driver not installed", from: None, until: Some(24) }, K8S011 => { desc: "kube-proxy version skew with kubelet", from: None, until: None }, - K8S012 => { desc: "kube-proxy IPVS mode deprecated (1.35+, removed 1.36)", from: Some(35), until: None }, + K8S012 => { desc: "kube-proxy IPVS mode deprecated (1.35+, removal planned 1.43)",from: Some(35), until: None }, K8S013 => { desc: "Ingress NGINX controller retirement (1.35+)", from: Some(35), until: None }, } diff --git a/eksup/src/k8s/checks.rs b/eksup/src/k8s/checks.rs index 6a17219..a6ad30b 100644 --- a/eksup/src/k8s/checks.rs +++ b/eksup/src/k8s/checks.rs @@ -366,8 +366,8 @@ finding::impl_findings!( "✅ - `kube-proxy` is not using the deprecated IPVS mode" ); -/// Check if kube-proxy is configured with IPVS mode, which is deprecated in 1.35 and -/// removed in 1.36 +/// Check if kube-proxy is configured with IPVS mode, which is deprecated from 1.35, expected +/// to be disabled by default in 1.40 and removed in 1.43 (KEP-5495) pub fn kube_proxy_ipvs_mode(configmap: Option<&ConfigMap>, target_minor: i32) -> Result> { if target_minor < 35 { return Ok(vec![]); @@ -394,14 +394,8 @@ pub fn kube_proxy_ipvs_mode(configmap: Option<&ConfigMap>, target_minor: i32) -> let mode = config.get("mode").and_then(|v| v.as_str()).unwrap_or(""); if mode == "ipvs" { - let remediation = if target_minor >= 36 { - Remediation::Required - } else { - Remediation::Recommended - }; - Ok(vec![KubeProxyIpvsMode { - finding: Finding::new(Code::K8S012, remediation), + finding: Finding::new(Code::K8S012, Remediation::Recommended), current_mode: mode.to_owned(), }]) } else { @@ -897,11 +891,11 @@ mod tests { } #[test] - fn kube_proxy_ipvs_mode_ipvs_target_36_required() { + fn kube_proxy_ipvs_mode_ipvs_target_37_recommended() { let cm = make_configmap("mode: ipvs"); - let result = kube_proxy_ipvs_mode(Some(&cm), 36).unwrap(); + let result = kube_proxy_ipvs_mode(Some(&cm), 37).unwrap(); assert_eq!(result.len(), 1); - assert!(matches!(result[0].finding.remediation, Remediation::Required)); + assert!(matches!(result[0].finding.remediation, Remediation::Recommended)); assert_eq!(result[0].current_mode, "ipvs"); } diff --git a/eksup/src/version.rs b/eksup/src/version.rs index 5bb0eb4..31a522b 100644 --- a/eksup/src/version.rs +++ b/eksup/src/version.rs @@ -4,7 +4,7 @@ use anyhow::{Context, Result, bail}; pub const MINIMUM: i32 = 30; /// Latest supported minor version -pub const LATEST: i32 = 35; +pub const LATEST: i32 = 37; /// Format a minor version number as a full version string (e.g. 30 → "1.30") pub fn format_version(minor: i32) -> String { diff --git a/eksup/templates/data.yaml b/eksup/templates/data.yaml index ec26a56..d7778f6 100644 --- a/eksup/templates/data.yaml +++ b/eksup/templates/data.yaml @@ -16,3 +16,9 @@ '1.35': release_url: https://kubernetes.io/blog/2025/12/17/kubernetes-v1-35-release/ + +'1.36': + release_url: https://kubernetes.io/blog/2026/04/22/kubernetes-v1-36-release/ + +'1.37': + release_url: https://kubernetes.io/blog/2026/08/26/kubernetes-v1-37-release/ From f554babf91e4c07b1b5e9f25d6bcfab9a4ffdf27 Mon Sep 17 00:00:00 2001 From: Bryant Biggs Date: Sat, 17 Oct 2026 11:35:15 -0500 Subject: [PATCH 2/4] feat: raise the minimum supported version to 1.32 --- eksup/src/playbook.rs | 17 +++++++ eksup/src/version.rs | 2 +- eksup/templates/data.yaml | 10 ---- eksup/tests/common/fixtures.rs | 2 +- eksup/tests/common/mock_aws.rs | 2 +- eksup/tests/e2e.rs | 39 ++++----------- eksup/tests/integration.rs | 50 +++++++++---------- .../snapshots/e2e__mixed_findings_json.snap | 7 +-- .../snapshots/e2e__mixed_findings_text.snap | 3 +- .../e2e__playbook_eks_managed_nodegroups.snap | 17 ++++--- .../e2e__playbook_fargate_profiles.snap | 15 +++--- .../snapshots/e2e__playbook_healthy.snap | 15 +++--- .../tests/snapshots/e2e__playbook_mixed.snap | 21 ++++---- .../snapshots/e2e__version_skew_text.snap | 5 +- 14 files changed, 101 insertions(+), 104 deletions(-) diff --git a/eksup/src/playbook.rs b/eksup/src/playbook.rs index b09164b..fa3a9a6 100644 --- a/eksup/src/playbook.rs +++ b/eksup/src/playbook.rs @@ -280,3 +280,20 @@ pub(crate) fn create( fs::write(filename, rendered)?; Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn release_data_covers_every_supported_target() { + let data = get_release_data().unwrap(); + for minor in (version::MINIMUM + 1)..=version::LATEST { + let target = version::format_version(minor); + assert!( + data.contains_key(&target), + "data.yaml is missing release data for {target}" + ); + } + } +} diff --git a/eksup/src/version.rs b/eksup/src/version.rs index 31a522b..cfdc5f2 100644 --- a/eksup/src/version.rs +++ b/eksup/src/version.rs @@ -1,7 +1,7 @@ use anyhow::{Context, Result, bail}; /// Minimum supported minor version — clusters below this cannot be analyzed -pub const MINIMUM: i32 = 30; +pub const MINIMUM: i32 = 32; /// Latest supported minor version pub const LATEST: i32 = 37; diff --git a/eksup/templates/data.yaml b/eksup/templates/data.yaml index d7778f6..c194761 100644 --- a/eksup/templates/data.yaml +++ b/eksup/templates/data.yaml @@ -1,13 +1,3 @@ -'1.30': - release_url: https://kubernetes.io/blog/2024/04/17/kubernetes-v1-30-release/ - -'1.31': - release_url: https://kubernetes.io/blog/2024/08/13/kubernetes-v1-31-release/ - -'1.32': - release_url: https://kubernetes.io/blog/2024/12/11/kubernetes-v1-32-release/ - deprecation_url: https://kubernetes.io/docs/reference/using-api/deprecation-guide/#v1-32 - '1.33': release_url: https://kubernetes.io/blog/2025/04/23/kubernetes-v1-33-release/ diff --git a/eksup/tests/common/fixtures.rs b/eksup/tests/common/fixtures.rs index 327f888..40e2b0a 100644 --- a/eksup/tests/common/fixtures.rs +++ b/eksup/tests/common/fixtures.rs @@ -16,7 +16,7 @@ pub fn healthy_aws() -> MockAwsClients { MockAwsClients { cluster: Cluster::builder() .name("test-cluster") - .version("1.30") + .version("1.32") .health(ClusterHealth::builder().build()) .resources_vpc_config( VpcConfigResponse::builder() diff --git a/eksup/tests/common/mock_aws.rs b/eksup/tests/common/mock_aws.rs index bd790fc..81933a5 100644 --- a/eksup/tests/common/mock_aws.rs +++ b/eksup/tests/common/mock_aws.rs @@ -31,7 +31,7 @@ pub struct MockAwsClients { impl Default for MockAwsClients { fn default() -> Self { Self { - cluster: Cluster::builder().name("test-cluster").version("1.30").build(), + cluster: Cluster::builder().name("test-cluster").version("1.32").build(), subnet_ips: vec![], addons: vec![], addon_versions: HashMap::new(), diff --git a/eksup/tests/e2e.rs b/eksup/tests/e2e.rs index 90b792f..35b05be 100644 --- a/eksup/tests/e2e.rs +++ b/eksup/tests/e2e.rs @@ -112,7 +112,7 @@ async fn snapshot_workload_issues_json() { async fn snapshot_version_skew_text() { let aws = fixtures::healthy_aws(); let k8s = MockK8sClients { - nodes: vec![fixtures::make_node("node-1", 28), fixtures::make_node("node-2", 27)], + nodes: vec![fixtures::make_node("node-1", 30), fixtures::make_node("node-2", 29)], ..Default::default() }; let results = run_analysis(&aws, &k8s).await; @@ -140,7 +140,7 @@ async fn snapshot_mixed_findings_text() { }, ]; let k8s = MockK8sClients { - nodes: vec![fixtures::make_node("node-1", 28)], + nodes: vec![fixtures::make_node("node-1", 30)], resources: vec![fixtures::make_deployment("web", "default", 1)], ..Default::default() }; @@ -166,7 +166,7 @@ async fn snapshot_mixed_findings_json() { }, ]; let k8s = MockK8sClients { - nodes: vec![fixtures::make_node("node-1", 28)], + nodes: vec![fixtures::make_node("node-1", 30)], resources: vec![fixtures::make_deployment("web", "default", 1)], ..Default::default() }; @@ -184,7 +184,7 @@ async fn snapshot_mixed_findings_json() { async fn snapshot_filter_recommended_text() { let aws = fixtures::healthy_aws(); let k8s = MockK8sClients { - nodes: vec![fixtures::make_node("node-1", 29)], + nodes: vec![fixtures::make_node("node-1", 31)], resources: vec![fixtures::make_deployment("web", "default", 2)], ..Default::default() }; @@ -271,7 +271,7 @@ async fn snapshot_playbook_mixed() { aws.nodegroups = vec![Nodegroup::builder().nodegroup_name("mng-1").build()]; aws.fargate_profiles = vec![FargateProfile::builder().fargate_profile_name("fp-1").build()]; let k8s = MockK8sClients { - nodes: vec![fixtures::make_node("node-1", 28)], + nodes: vec![fixtures::make_node("node-1", 30)], resources: vec![fixtures::make_deployment("web", "default", 1)], ..Default::default() }; @@ -351,8 +351,8 @@ async fn playbook_template_variables_populated() { assert!(output.contains("test-cluster"), "cluster name should appear"); assert!(output.contains("us-east-1"), "region should appear"); - assert!(output.contains("v1.30"), "current version should appear"); - assert!(output.contains("v1.31"), "target version should appear"); + assert!(output.contains("v1.32"), "current version should appear"); + assert!(output.contains("v1.33"), "target version should appear"); assert!(output.contains("kubernetes.io/blog/"), "release URL should appear"); } @@ -374,31 +374,14 @@ async fn playbook_pod_ips_healthy_when_no_custom_networking() { } #[tokio::test] -async fn playbook_deprecation_url_absent_for_1_31_target() { - // Cluster at 1.30 -> target 1.31 (no deprecation_url in data.yaml) - let aws = aws_at_version("1.30"); +async fn playbook_deprecation_url_absent_for_1_33_target() { + // Cluster at 1.32 -> target 1.33 (no deprecation_url in data.yaml) + let aws = aws_at_version("1.32"); let k8s = fixtures::healthy_k8s(); let output = render_playbook(&aws, &k8s).await; assert!( !output.contains("API deprecations"), - "deprecation link should be absent for 1.31 target" - ); -} - -#[tokio::test] -async fn playbook_deprecation_url_present_for_1_32_target() { - // Cluster at 1.31 -> target 1.32 (has deprecation_url in data.yaml) - let aws = aws_at_version("1.31"); - let k8s = fixtures::healthy_k8s(); - let output = render_playbook(&aws, &k8s).await; - - assert!( - output.contains("API deprecations"), - "deprecation link should be present for 1.32 target" - ); - assert!( - output.contains("deprecation-guide/#v1-32"), - "deprecation URL should point to 1.32 guide" + "deprecation link should be absent for 1.33 target" ); } diff --git a/eksup/tests/integration.rs b/eksup/tests/integration.rs index c539b69..aaed389 100644 --- a/eksup/tests/integration.rs +++ b/eksup/tests/integration.rs @@ -82,7 +82,7 @@ async fn subnet_findings_insufficient_control_plane_ips() { #[tokio::test] async fn addon_findings_no_addons() { let aws = fixtures::healthy_aws(); - let result = eksup::eks::get_addon_findings(&aws, "test-cluster", "1.30", 31) + let result = eksup::eks::get_addon_findings(&aws, "test-cluster", "1.32", 33) .await .unwrap(); assert!(result.version_compatibility.is_empty()); @@ -99,16 +99,16 @@ async fn addon_findings_version_incompatible() { aws.addons = vec![Addon::builder().addon_name("vpc-cni").addon_version("v1.12.0").build()]; aws.addon_versions = HashMap::from([ ( - ("vpc-cni".into(), "1.30".into()), + ("vpc-cni".into(), "1.32".into()), fixtures::make_addon_version("v1.15.0", "v1.14.0", &["v1.15.0", "v1.14.0"]), ), ( - ("vpc-cni".into(), "1.31".into()), + ("vpc-cni".into(), "1.33".into()), fixtures::make_addon_version("v1.16.0", "v1.15.0", &["v1.16.0", "v1.15.0"]), ), ]); - let result = eksup::eks::get_addon_findings(&aws, "test-cluster", "1.30", 31) + let result = eksup::eks::get_addon_findings(&aws, "test-cluster", "1.32", 33) .await .unwrap(); assert_eq!(result.version_compatibility.len(), 1); @@ -121,7 +121,7 @@ async fn addon_findings_version_incompatible() { #[tokio::test] async fn data_plane_findings_empty() { let aws = fixtures::healthy_aws(); - let result = eksup::eks::get_data_plane_findings(&aws, &aws.cluster, 31) + let result = eksup::eks::get_data_plane_findings(&aws, &aws.cluster, 33) .await .unwrap(); assert!(result.eks_managed_nodegroup_health.is_empty()); @@ -154,7 +154,7 @@ async fn data_plane_findings_node_ips() { }, ]; - let result = eksup::eks::get_data_plane_findings(&aws, &aws.cluster, 31) + let result = eksup::eks::get_data_plane_findings(&aws, &aws.cluster, 33) .await .unwrap(); assert!(!result.node_ips.is_empty(), "low IP subnets should produce findings"); @@ -167,7 +167,7 @@ async fn data_plane_findings_node_ips() { #[tokio::test] async fn kubernetes_findings_empty() { let k8s = fixtures::healthy_k8s(); - let (result, _suppressed) = eksup::k8s::get_kubernetes_findings(&k8s, 30, 31, &ChecksConfig::default()) + let (result, _suppressed) = eksup::k8s::get_kubernetes_findings(&k8s, 32, 33, &ChecksConfig::default()) .await .unwrap(); assert!(result.version_skew.is_empty()); @@ -177,11 +177,11 @@ async fn kubernetes_findings_empty() { #[tokio::test] async fn kubernetes_findings_version_skew() { let k8s = MockK8sClients { - nodes: vec![fixtures::make_node("node-1", 28)], + nodes: vec![fixtures::make_node("node-1", 30)], ..Default::default() }; - let (result, _suppressed) = eksup::k8s::get_kubernetes_findings(&k8s, 30, 31, &ChecksConfig::default()) + let (result, _suppressed) = eksup::k8s::get_kubernetes_findings(&k8s, 32, 33, &ChecksConfig::default()) .await .unwrap(); assert_eq!(result.version_skew.len(), 1); @@ -194,7 +194,7 @@ async fn kubernetes_findings_workload_issues() { ..Default::default() }; - let (result, _suppressed) = eksup::k8s::get_kubernetes_findings(&k8s, 30, 31, &ChecksConfig::default()) + let (result, _suppressed) = eksup::k8s::get_kubernetes_findings(&k8s, 32, 33, &ChecksConfig::default()) .await .unwrap(); assert!(!result.min_replicas.is_empty(), "1 replica should trigger finding"); @@ -244,7 +244,7 @@ async fn kubernetes_findings_missing_pdb() { ..Default::default() }; - let (result, _suppressed) = eksup::k8s::get_kubernetes_findings(&k8s, 30, 31, &ChecksConfig::default()) + let (result, _suppressed) = eksup::k8s::get_kubernetes_findings(&k8s, 32, 33, &ChecksConfig::default()) .await .unwrap(); assert!( @@ -261,7 +261,7 @@ async fn kubernetes_findings_missing_pdb() { async fn analyze_healthy_cluster() { let aws = fixtures::healthy_aws(); let k8s = fixtures::healthy_k8s(); - let results = eksup::analysis::analyze(&aws, &k8s, &aws.cluster, 31, &Config::default()) + let results = eksup::analysis::analyze(&aws, &k8s, &aws.cluster, 33, &Config::default()) .await .unwrap(); @@ -275,12 +275,12 @@ async fn analyze_healthy_cluster() { async fn analyze_filter_recommended() { let k8s = MockK8sClients { resources: vec![fixtures::make_deployment("web", "default", 2)], - nodes: vec![fixtures::make_node("node-1", 29)], + nodes: vec![fixtures::make_node("node-1", 31)], ..Default::default() }; let aws = fixtures::healthy_aws(); - let mut results = eksup::analysis::analyze(&aws, &k8s, &aws.cluster, 31, &Config::default()) + let mut results = eksup::analysis::analyze(&aws, &k8s, &aws.cluster, 33, &Config::default()) .await .unwrap(); let before_skew = results.kubernetes.version_skew.len(); @@ -295,11 +295,11 @@ async fn analyze_filter_recommended() { #[tokio::test] async fn analyze_with_explicit_target() { - let aws = fixtures::healthy_aws(); // cluster version "1.30" + let aws = fixtures::healthy_aws(); // cluster version "1.32" let k8s = fixtures::healthy_k8s(); - // Jump from 1.30 → 1.33 - let results = eksup::analysis::analyze(&aws, &k8s, &aws.cluster, 33, &Config::default()) + // Jump from 1.32 → 1.35 + let results = eksup::analysis::analyze(&aws, &k8s, &aws.cluster, 35, &Config::default()) .await .unwrap(); @@ -479,14 +479,14 @@ async fn analyze_aws_error_propagates() { let cluster = aws_sdk_eks::types::Cluster::builder() .name("test") - .version("1.30") + .version("1.32") .build(); let result = eksup::analysis::analyze( &MockAwsClientsError, &MockK8sClientsError, &cluster, - 31, + 33, &Config::default(), ) .await; @@ -504,7 +504,7 @@ async fn kubernetes_findings_min_replicas_default_threshold() { resources: vec![fixtures::make_deployment("web", "default", 2)], ..Default::default() }; - let (result, _suppressed) = eksup::k8s::get_kubernetes_findings(&k8s, 30, 31, &ChecksConfig::default()) + let (result, _suppressed) = eksup::k8s::get_kubernetes_findings(&k8s, 32, 33, &ChecksConfig::default()) .await .unwrap(); assert!( @@ -525,7 +525,7 @@ async fn kubernetes_findings_min_replicas_strict_threshold() { min_replicas: 3, ..Default::default() }; - let (result, _suppressed) = eksup::k8s::get_kubernetes_findings(&k8s, 30, 31, &checks) + let (result, _suppressed) = eksup::k8s::get_kubernetes_findings(&k8s, 32, 33, &checks) .await .unwrap(); assert_eq!( @@ -549,7 +549,7 @@ async fn kubernetes_findings_min_replicas_ignored() { }], ..Default::default() }; - let (result, suppressed) = eksup::k8s::get_kubernetes_findings(&k8s, 30, 31, &checks) + let (result, suppressed) = eksup::k8s::get_kubernetes_findings(&k8s, 32, 33, &checks) .await .unwrap(); assert!( @@ -581,7 +581,7 @@ async fn kubernetes_findings_min_replicas_per_workload_override() { }], ..Default::default() }; - let (result, _suppressed) = eksup::k8s::get_kubernetes_findings(&k8s, 30, 31, &checks) + let (result, _suppressed) = eksup::k8s::get_kubernetes_findings(&k8s, 32, 33, &checks) .await .unwrap(); assert_eq!(result.min_replicas.len(), 1, "only etcd should fail (3 < 5)"); @@ -633,7 +633,7 @@ async fn kubernetes_findings_pdb_ignored() { }; // Without ignore: should trigger PDB finding - let (result, _suppressed) = eksup::k8s::get_kubernetes_findings(&k8s, 30, 31, &ChecksConfig::default()) + let (result, _suppressed) = eksup::k8s::get_kubernetes_findings(&k8s, 32, 33, &ChecksConfig::default()) .await .unwrap(); assert!( @@ -649,7 +649,7 @@ async fn kubernetes_findings_pdb_ignored() { namespace: "default".into(), }], }; - let (result, suppressed) = eksup::k8s::get_kubernetes_findings(&k8s, 30, 31, &checks) + let (result, suppressed) = eksup::k8s::get_kubernetes_findings(&k8s, 32, 33, &checks) .await .unwrap(); assert!( diff --git a/eksup/tests/snapshots/e2e__mixed_findings_json.snap b/eksup/tests/snapshots/e2e__mixed_findings_json.snap index 2d3fe8a..ef376c8 100644 --- a/eksup/tests/snapshots/e2e__mixed_findings_json.snap +++ b/eksup/tests/snapshots/e2e__mixed_findings_json.snap @@ -1,5 +1,6 @@ --- source: eksup/tests/e2e.rs +assertion_line: 176 expression: output --- { @@ -52,9 +53,9 @@ expression: output "remediation": "Recommended" }, "name": "node-1", - "kubelet_version": "v1.28.0", - "kubernetes_version": "v1.28", - "control_plane_version": "v1.30", + "kubelet_version": "v1.30.0", + "kubernetes_version": "v1.30", + "control_plane_version": "v1.32", "version_skew": "+2" } ], diff --git a/eksup/tests/snapshots/e2e__mixed_findings_text.snap b/eksup/tests/snapshots/e2e__mixed_findings_text.snap index e3ca29e..00a7fdb 100644 --- a/eksup/tests/snapshots/e2e__mixed_findings_text.snap +++ b/eksup/tests/snapshots/e2e__mixed_findings_text.snap @@ -1,5 +1,6 @@ --- source: eksup/tests/e2e.rs +assertion_line: 150 expression: output --- ┌────────┬────┬──────────┬──────────────┐ @@ -11,7 +12,7 @@ expression: output ┌────────┬────┬────────┬───────┬───────────────┬──────┐ │ CHECK │ │ NAME │ NODE │ CONTROL PLANE │ SKEW │ ├────────┼────┼────────┼───────┼───────────────┼──────┤ -│ K8S001 │ ⚠️ │ node-1 │ v1.28 │ v1.30 │ +2 │ +│ K8S001 │ ⚠️ │ node-1 │ v1.30 │ v1.32 │ +2 │ └────────┴────┴────────┴───────┴───────────────┴──────┘ ┌────────┬────┬──────┬───────────┬────────────┬──────────┐ │ CHECK │ │ NAME │ NAMESPACE │ KIND │ REPLICAS │ diff --git a/eksup/tests/snapshots/e2e__playbook_eks_managed_nodegroups.snap b/eksup/tests/snapshots/e2e__playbook_eks_managed_nodegroups.snap index 61c0ad3..7791ff1 100644 --- a/eksup/tests/snapshots/e2e__playbook_eks_managed_nodegroups.snap +++ b/eksup/tests/snapshots/e2e__playbook_eks_managed_nodegroups.snap @@ -1,5 +1,6 @@ --- source: eksup/tests/e2e.rs +assertion_line: 256 expression: output --- # EKS Cluster Upgrade @@ -7,8 +8,8 @@ expression: output | | Value | | :------------------------- | :-------------------------------------------------------: | | Amazon EKS cluster | `test-cluster` | -| Current version | `v1.30` | -| Target version | `v1.31` | +| Current version | `v1.32` | +| Target version | `v1.33` | | EKS Managed nodegroup(s) | ✅ | | Self-Managed nodegroup(s) | ➖ | | Fargate profile(s) | ➖ | @@ -34,8 +35,8 @@ expression: output 1. Review the following resources for affected changes in the next version of Kubernetes: - - ℹ️ [Kubernetes `1.31` release announcement](https://kubernetes.io/blog/2024/08/13/kubernetes-v1-31-release/) - - ℹ️ [EKS `1.31` release notes](https://docs.aws.amazon.com/eks/latest/userguide/kubernetes-versions.html#kubernetes-1.31) + - ℹ️ [Kubernetes `1.33` release announcement](https://kubernetes.io/blog/2025/04/23/kubernetes-v1-33-release/) + - ℹ️ [EKS `1.33` release notes](https://docs.aws.amazon.com/eks/latest/userguide/kubernetes-versions.html#kubernetes-1.33) 2. Per the [Kubernetes version skew policy](https://kubernetes.io/releases/version-skew-policy/#supported-version-skew), the `kubelet` version must not be newer than `kube-apiserver`, and may be up to two minor versions older. It is recommended that the nodes in the data plane are aligned with the same minor version as the control plane before upgrading. @@ -118,9 +119,9 @@ expression: output CURRENT=$(aws eks describe-addon --cluster-name test-cluster --region us-east-1 \ --addon-name ${ADDON} --query 'addon.addonVersion' --output text) LATEST=$(aws eks describe-addon-versions --region us-east-1 --addon-name ${ADDON} \ - --kubernetes-version 1.31 --query 'addons[0].addonVersions[0].addonVersion' --output text) + --kubernetes-version 1.33 --query 'addons[0].addonVersions[0].addonVersion' --output text) LIST=$(aws eks describe-addon-versions --region us-east-1 --addon-name ${ADDON} \ - --kubernetes-version 1.31 --query 'addons[0].addonVersions[*].addonVersion') + --kubernetes-version 1.33 --query 'addons[0].addonVersions[*].addonVersion') echo "${ADDON} current version: ${CURRENT}" echo "${ADDON} next latest version: ${LATEST}" @@ -165,7 +166,7 @@ When upgrading the control plane, Amazon EKS performs standard infrastructure an ```sh aws eks update-cluster-version --region us-east-1 --name test-cluster \ - --kubernetes-version 1.31 + --kubernetes-version 1.33 ``` 2. Wait for the control plane to finish upgrading before proceeding with any further modifications. The cluster status will change to `ACTIVE` once the upgrade is complete. @@ -324,7 +325,7 @@ EKS optimized AMI provided by Amazon EKS: ```sh aws eks update-nodegroup-version --region us-east-1 --cluster-name test-cluster \ - --nodegroup-name --kubernetes-version 1.31 + --nodegroup-name --kubernetes-version 1.33 ``` ##### Process diff --git a/eksup/tests/snapshots/e2e__playbook_fargate_profiles.snap b/eksup/tests/snapshots/e2e__playbook_fargate_profiles.snap index 13748c4..d58181e 100644 --- a/eksup/tests/snapshots/e2e__playbook_fargate_profiles.snap +++ b/eksup/tests/snapshots/e2e__playbook_fargate_profiles.snap @@ -1,5 +1,6 @@ --- source: eksup/tests/e2e.rs +assertion_line: 265 expression: output --- # EKS Cluster Upgrade @@ -7,8 +8,8 @@ expression: output | | Value | | :------------------------- | :-------------------------------------------------------: | | Amazon EKS cluster | `test-cluster` | -| Current version | `v1.30` | -| Target version | `v1.31` | +| Current version | `v1.32` | +| Target version | `v1.33` | | EKS Managed nodegroup(s) | ➖ | | Self-Managed nodegroup(s) | ➖ | | Fargate profile(s) | ✅ | @@ -33,8 +34,8 @@ expression: output 1. Review the following resources for affected changes in the next version of Kubernetes: - - ℹ️ [Kubernetes `1.31` release announcement](https://kubernetes.io/blog/2024/08/13/kubernetes-v1-31-release/) - - ℹ️ [EKS `1.31` release notes](https://docs.aws.amazon.com/eks/latest/userguide/kubernetes-versions.html#kubernetes-1.31) + - ℹ️ [Kubernetes `1.33` release announcement](https://kubernetes.io/blog/2025/04/23/kubernetes-v1-33-release/) + - ℹ️ [EKS `1.33` release notes](https://docs.aws.amazon.com/eks/latest/userguide/kubernetes-versions.html#kubernetes-1.33) 2. Per the [Kubernetes version skew policy](https://kubernetes.io/releases/version-skew-policy/#supported-version-skew), the `kubelet` version must not be newer than `kube-apiserver`, and may be up to two minor versions older. It is recommended that the nodes in the data plane are aligned with the same minor version as the control plane before upgrading. @@ -117,9 +118,9 @@ expression: output CURRENT=$(aws eks describe-addon --cluster-name test-cluster --region us-east-1 \ --addon-name ${ADDON} --query 'addon.addonVersion' --output text) LATEST=$(aws eks describe-addon-versions --region us-east-1 --addon-name ${ADDON} \ - --kubernetes-version 1.31 --query 'addons[0].addonVersions[0].addonVersion' --output text) + --kubernetes-version 1.33 --query 'addons[0].addonVersions[0].addonVersion' --output text) LIST=$(aws eks describe-addon-versions --region us-east-1 --addon-name ${ADDON} \ - --kubernetes-version 1.31 --query 'addons[0].addonVersions[*].addonVersion') + --kubernetes-version 1.33 --query 'addons[0].addonVersions[*].addonVersion') echo "${ADDON} current version: ${CURRENT}" echo "${ADDON} next latest version: ${LATEST}" @@ -164,7 +165,7 @@ When upgrading the control plane, Amazon EKS performs standard infrastructure an ```sh aws eks update-cluster-version --region us-east-1 --name test-cluster \ - --kubernetes-version 1.31 + --kubernetes-version 1.33 ``` 2. Wait for the control plane to finish upgrading before proceeding with any further modifications. The cluster status will change to `ACTIVE` once the upgrade is complete. diff --git a/eksup/tests/snapshots/e2e__playbook_healthy.snap b/eksup/tests/snapshots/e2e__playbook_healthy.snap index 0416398..ed04437 100644 --- a/eksup/tests/snapshots/e2e__playbook_healthy.snap +++ b/eksup/tests/snapshots/e2e__playbook_healthy.snap @@ -1,5 +1,6 @@ --- source: eksup/tests/e2e.rs +assertion_line: 247 expression: output --- # EKS Cluster Upgrade @@ -7,8 +8,8 @@ expression: output | | Value | | :------------------------- | :-------------------------------------------------------: | | Amazon EKS cluster | `test-cluster` | -| Current version | `v1.30` | -| Target version | `v1.31` | +| Current version | `v1.32` | +| Target version | `v1.33` | | EKS Managed nodegroup(s) | ➖ | | Self-Managed nodegroup(s) | ➖ | | Fargate profile(s) | ➖ | @@ -32,8 +33,8 @@ expression: output 1. Review the following resources for affected changes in the next version of Kubernetes: - - ℹ️ [Kubernetes `1.31` release announcement](https://kubernetes.io/blog/2024/08/13/kubernetes-v1-31-release/) - - ℹ️ [EKS `1.31` release notes](https://docs.aws.amazon.com/eks/latest/userguide/kubernetes-versions.html#kubernetes-1.31) + - ℹ️ [Kubernetes `1.33` release announcement](https://kubernetes.io/blog/2025/04/23/kubernetes-v1-33-release/) + - ℹ️ [EKS `1.33` release notes](https://docs.aws.amazon.com/eks/latest/userguide/kubernetes-versions.html#kubernetes-1.33) 2. Per the [Kubernetes version skew policy](https://kubernetes.io/releases/version-skew-policy/#supported-version-skew), the `kubelet` version must not be newer than `kube-apiserver`, and may be up to two minor versions older. It is recommended that the nodes in the data plane are aligned with the same minor version as the control plane before upgrading. @@ -116,9 +117,9 @@ expression: output CURRENT=$(aws eks describe-addon --cluster-name test-cluster --region us-east-1 \ --addon-name ${ADDON} --query 'addon.addonVersion' --output text) LATEST=$(aws eks describe-addon-versions --region us-east-1 --addon-name ${ADDON} \ - --kubernetes-version 1.31 --query 'addons[0].addonVersions[0].addonVersion' --output text) + --kubernetes-version 1.33 --query 'addons[0].addonVersions[0].addonVersion' --output text) LIST=$(aws eks describe-addon-versions --region us-east-1 --addon-name ${ADDON} \ - --kubernetes-version 1.31 --query 'addons[0].addonVersions[*].addonVersion') + --kubernetes-version 1.33 --query 'addons[0].addonVersions[*].addonVersion') echo "${ADDON} current version: ${CURRENT}" echo "${ADDON} next latest version: ${LATEST}" @@ -163,7 +164,7 @@ When upgrading the control plane, Amazon EKS performs standard infrastructure an ```sh aws eks update-cluster-version --region us-east-1 --name test-cluster \ - --kubernetes-version 1.31 + --kubernetes-version 1.33 ``` 2. Wait for the control plane to finish upgrading before proceeding with any further modifications. The cluster status will change to `ACTIVE` once the upgrade is complete. diff --git a/eksup/tests/snapshots/e2e__playbook_mixed.snap b/eksup/tests/snapshots/e2e__playbook_mixed.snap index 3fb53f8..d1fdf93 100644 --- a/eksup/tests/snapshots/e2e__playbook_mixed.snap +++ b/eksup/tests/snapshots/e2e__playbook_mixed.snap @@ -1,5 +1,6 @@ --- source: eksup/tests/e2e.rs +assertion_line: 279 expression: output --- # EKS Cluster Upgrade @@ -7,8 +8,8 @@ expression: output | | Value | | :------------------------- | :-------------------------------------------------------: | | Amazon EKS cluster | `test-cluster` | -| Current version | `v1.30` | -| Target version | `v1.31` | +| Current version | `v1.32` | +| Target version | `v1.33` | | EKS Managed nodegroup(s) | ✅ | | Self-Managed nodegroup(s) | ➖ | | Fargate profile(s) | ✅ | @@ -35,8 +36,8 @@ expression: output 1. Review the following resources for affected changes in the next version of Kubernetes: - - ℹ️ [Kubernetes `1.31` release announcement](https://kubernetes.io/blog/2024/08/13/kubernetes-v1-31-release/) - - ℹ️ [EKS `1.31` release notes](https://docs.aws.amazon.com/eks/latest/userguide/kubernetes-versions.html#kubernetes-1.31) + - ℹ️ [Kubernetes `1.33` release announcement](https://kubernetes.io/blog/2025/04/23/kubernetes-v1-33-release/) + - ℹ️ [EKS `1.33` release notes](https://docs.aws.amazon.com/eks/latest/userguide/kubernetes-versions.html#kubernetes-1.33) 2. Per the [Kubernetes version skew policy](https://kubernetes.io/releases/version-skew-policy/#supported-version-skew), the `kubelet` version must not be newer than `kube-apiserver`, and may be up to two minor versions older. It is recommended that the nodes in the data plane are aligned with the same minor version as the control plane before upgrading. @@ -76,11 +77,11 @@ expression: output #### Check [[K8S001]](https://clowdhaus.github.io/eksup/info/checks/#k8s001) | CHECK | | NODE | CONTROL PLANE | SKEW | QUANTITY | |--------|----|-------|---------------|------|----------| - | K8S001 | ⚠️ | v1.28 | v1.30 | +2 | 1 | + | K8S001 | ⚠️ | v1.30 | v1.32 | +2 | 1 | | | NAME | NODE | CONTROL PLANE | SKEW | |----|--------|-------|---------------|------| - | ⚠️ | node-1 | v1.28 | v1.30 | +2 | + | ⚠️ | node-1 | v1.30 | v1.32 | +2 | 3. Verify that there are at least 5 free IPs in the VPC subnets used by the control plane. Amazon EKS creates new elastic network interfaces (ENIs) in any of the subnets specified for the control plane. If there are not enough available IPs, then the upgrade will fail (your control plane will stay on the prior version). @@ -126,9 +127,9 @@ expression: output CURRENT=$(aws eks describe-addon --cluster-name test-cluster --region us-east-1 \ --addon-name ${ADDON} --query 'addon.addonVersion' --output text) LATEST=$(aws eks describe-addon-versions --region us-east-1 --addon-name ${ADDON} \ - --kubernetes-version 1.31 --query 'addons[0].addonVersions[0].addonVersion' --output text) + --kubernetes-version 1.33 --query 'addons[0].addonVersions[0].addonVersion' --output text) LIST=$(aws eks describe-addon-versions --region us-east-1 --addon-name ${ADDON} \ - --kubernetes-version 1.31 --query 'addons[0].addonVersions[*].addonVersion') + --kubernetes-version 1.33 --query 'addons[0].addonVersions[*].addonVersion') echo "${ADDON} current version: ${CURRENT}" echo "${ADDON} next latest version: ${LATEST}" @@ -173,7 +174,7 @@ When upgrading the control plane, Amazon EKS performs standard infrastructure an ```sh aws eks update-cluster-version --region us-east-1 --name test-cluster \ - --kubernetes-version 1.31 + --kubernetes-version 1.33 ``` 2. Wait for the control plane to finish upgrading before proceeding with any further modifications. The cluster status will change to `ACTIVE` once the upgrade is complete. @@ -344,7 +345,7 @@ EKS optimized AMI provided by Amazon EKS: ```sh aws eks update-nodegroup-version --region us-east-1 --cluster-name test-cluster \ - --nodegroup-name --kubernetes-version 1.31 + --nodegroup-name --kubernetes-version 1.33 ``` ##### Process diff --git a/eksup/tests/snapshots/e2e__version_skew_text.snap b/eksup/tests/snapshots/e2e__version_skew_text.snap index 1d96b0b..e0f262a 100644 --- a/eksup/tests/snapshots/e2e__version_skew_text.snap +++ b/eksup/tests/snapshots/e2e__version_skew_text.snap @@ -1,10 +1,11 @@ --- source: eksup/tests/e2e.rs +assertion_line: 120 expression: output --- ┌────────┬────┬────────┬───────┬───────────────┬──────┐ │ CHECK │ │ NAME │ NODE │ CONTROL PLANE │ SKEW │ ├────────┼────┼────────┼───────┼───────────────┼──────┤ -│ K8S001 │ ⚠️ │ node-1 │ v1.28 │ v1.30 │ +2 │ -│ K8S001 │ ❌ │ node-2 │ v1.27 │ v1.30 │ +3 │ +│ K8S001 │ ⚠️ │ node-1 │ v1.30 │ v1.32 │ +2 │ +│ K8S001 │ ❌ │ node-2 │ v1.29 │ v1.32 │ +3 │ └────────┴────┴────────┴───────┴───────────────┴──────┘ From 738d5fa2611f1fbbf305dab8319c803fd16adbbf Mon Sep 17 00:00:00 2001 From: Bryant Biggs Date: Sat, 17 Oct 2026 12:05:59 -0500 Subject: [PATCH 3/4] feat: add checks for containerd 1.x, gitRepo volumes and Service externalIPs --- docs/info/checks.md | 32 ++ docs/info/usage.md | 5 +- eksup/src/analysis.rs | 17 ++ eksup/src/clients.rs | 7 +- eksup/src/config.rs | 23 +- eksup/src/finding.rs | 11 + eksup/src/k8s/checks.rs | 274 +++++++++++++++++- eksup/src/k8s/findings.rs | 26 +- eksup/src/k8s/resources.rs | 10 + eksup/src/playbook.rs | 21 ++ eksup/templates/data.yaml | 3 + eksup/templates/playbook.md | 12 + eksup/tests/common/fixtures.rs | 1 + eksup/tests/common/mock_k8s.rs | 10 +- eksup/tests/e2e.rs | 25 ++ eksup/tests/integration.rs | 40 +++ .../snapshots/e2e__healthy_cluster_json.snap | 10 +- .../snapshots/e2e__mixed_findings_json.snap | 9 +- .../e2e__playbook_eks_managed_nodegroups.snap | 9 + .../e2e__playbook_fargate_profiles.snap | 9 + .../snapshots/e2e__playbook_healthy.snap | 9 + .../tests/snapshots/e2e__playbook_mixed.snap | 9 + .../snapshots/e2e__workload_issues_json.snap | 10 +- 23 files changed, 565 insertions(+), 17 deletions(-) diff --git a/docs/info/checks.md b/docs/info/checks.md index 2d03797..e551e56 100644 --- a/docs/info/checks.md +++ b/docs/info/checks.md @@ -43,6 +43,9 @@ See the [symbol table](https://clowdhaus.github.io/eksup/#symbol-table) for furt | `K8S011` | kube-proxy version skew with kubelet | Active | All versions | | `K8S012` | kube-proxy IPVS mode deprecated (1.35+, removal planned 1.43) | Active | 1.35+ | | `K8S013` | Ingress NGINX controller retirement (1.35+) | Active | 1.35+ | +| `K8S014` | Nodes running containerd 1.x (unsupported from 1.36) | Active | 1.36+ | +| `K8S015` | gitRepo volumes in use (disabled in 1.36) | Active | 1.36+ | +| `K8S016` | Service externalIPs deprecated (1.36+, removal planned 1.43) | Active | 1.36+ | @@ -201,6 +204,9 @@ Table below shows the checks that are applicable, or not, to the respective Kube | `K8S011` | ➖ | ➖ | ➖ | ➖ | ➖ | ➖ | ➖ | | `K8S012` | ➖ | ➖ | ➖ | ➖ | ➖ | ➖ | ➖ | | `K8S013` | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | +| `K8S014` | ➖ | ➖ | ➖ | ➖ | ➖ | ➖ | ➖ | +| `K8S015` | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | +| `K8S016` | ➖ | ➖ | ➖ | ➖ | ➖ | ➖ | ➖ | #### K8S001 @@ -362,6 +368,32 @@ This check scans Deployments and DaemonSets for container images referencing the [Ingress NGINX Controller](https://kubernetes.github.io/ingress-nginx/) +#### K8S014 + +**❌ Remediation required** + +Kubernetes `v1.35` is the last release that supports containerd 1.x. Nodes MUST run containerd 2.0 or later before upgrading to Kubernetes `v1.36` or later. The check reads the container runtime version each node reports. + +EKS optimized AMIs ship containerd 2.x from Kubernetes `v1.34`, so findings usually point at nodes from older or custom AMIs. The replacement nodes MUST also come from an AMI that ships containerd 2.0 or later. + +[EKS Kubernetes 1.35 release notes](https://docs.aws.amazon.com/eks/latest/userguide/kubernetes-versions-standard.html) + +#### K8S015 + +**❌ Remediation required** + +The `gitRepo` volume type is permanently disabled in Kubernetes `v1.36`. The API server still accepts pods that use it, but the kubelet refuses to run them. Workloads using `gitRepo` volumes MUST migrate to an init container or a git-sync sidecar before upgrading to Kubernetes `v1.36` or later. + +[KEP-5040](https://github.com/kubernetes/enhancements/issues/5040) + +#### K8S016 + +**⚠️ Remediation recommended** + +The `spec.externalIPs` field of a Service is deprecated starting in Kubernetes `v1.36`, and its removal is planned for `v1.43`. Services using `externalIPs` SHOULD migrate to a `LoadBalancer` or `NodePort` Service, or to the Gateway API. + +[KEP-5707](https://github.com/kubernetes/enhancements/issues/5707) + --- ## Retired Checks diff --git a/docs/info/usage.md b/docs/info/usage.md index 6364790..6864d29 100644 --- a/docs/info/usage.md +++ b/docs/info/usage.md @@ -175,7 +175,7 @@ checks: K8S003: { ignore: [{ name: "*-batch", namespace: "*" }] } K8S004: { ignore: [{ name: "singleton-*", namespace: "default" }] } K8S005: { ignore: [{ name: "*-logger*", namespace: "*monitoring*" }] } - # ... K8S006, K8S007, K8S008, K8S013 all use the same `ignore:` shape + # ... K8S006, K8S007, K8S008, K8S013, K8S015, K8S016 all use the same `ignore:` shape ``` - `K8S002.min_replicas`: Global minimum replica threshold (default: 2). Must be >= 1. @@ -210,6 +210,8 @@ The following workload checks accept the `ignore:` shape shown above (each rule - `K8S007` — termination grace period - `K8S008` — Docker socket mount - `K8S013` — `ingress-nginx` controller retirement +- `K8S015` — `gitRepo` volumes +- `K8S016` — Service `externalIPs` (matched against the Service `name` + `namespace`) #### Cluster-level checks (no `ignore` support) @@ -222,6 +224,7 @@ Cluster-level checks do not support `ignore` because their findings have no reso - `K8S010` — EBS CSI driver - `K8S011` — kube-proxy / kubelet version skew - `K8S012` — kube-proxy IPVS mode (singleton DaemonSet; finding has no per-resource fields) +- `K8S014` — containerd version (node-level finding) The right control for a cluster-level check is "disable the check entirely" — a separate, future feature. diff --git a/eksup/src/analysis.rs b/eksup/src/analysis.rs index 2646668..997d490 100644 --- a/eksup/src/analysis.rs +++ b/eksup/src/analysis.rs @@ -104,6 +104,18 @@ impl Results { .kubernetes .pod_disruption_budgets .retain(|f| !f.finding.remediation.is_recommended()); + self + .kubernetes + .containerd_version + .retain(|f| !f.finding.remediation.is_recommended()); + self + .kubernetes + .git_repo_volumes + .retain(|f| !f.finding.remediation.is_recommended()); + self + .kubernetes + .service_external_ips + .retain(|f| !f.finding.remediation.is_recommended()); self .service_limits .ec2_limits @@ -153,6 +165,9 @@ impl Results { output.push_str(&self.kubernetes.kube_proxy_ipvs_mode.to_stdout_table()?); output.push_str(&self.kubernetes.ingress_nginx_retirement.to_stdout_table()?); output.push_str(&self.kubernetes.pod_disruption_budgets.to_stdout_table()?); + output.push_str(&self.kubernetes.containerd_version.to_stdout_table()?); + output.push_str(&self.kubernetes.git_repo_volumes.to_stdout_table()?); + output.push_str(&self.kubernetes.service_external_ips.to_stdout_table()?); output.push_str(&self.service_limits.ec2_limits.to_stdout_table()?); output.push_str(&self.service_limits.ebs_gp2_limits.to_stdout_table()?); output.push_str(&self.service_limits.ebs_gp3_limits.to_stdout_table()?); @@ -171,6 +186,8 @@ impl Results { output.push_str(&self.kubernetes_suppressed.docker_socket.to_stdout_table()?); output.push_str(&self.kubernetes_suppressed.ingress_nginx_retirement.to_stdout_table()?); output.push_str(&self.kubernetes_suppressed.pod_disruption_budgets.to_stdout_table()?); + output.push_str(&self.kubernetes_suppressed.git_repo_volumes.to_stdout_table()?); + output.push_str(&self.kubernetes_suppressed.service_external_ips.to_stdout_table()?); } else { writeln!( output, diff --git a/eksup/src/clients.rs b/eksup/src/clients.rs index 25b8241..025d588 100644 --- a/eksup/src/clients.rs +++ b/eksup/src/clients.rs @@ -1,7 +1,7 @@ use anyhow::Result; use aws_sdk_autoscaling::types::AutoScalingGroup; use aws_sdk_eks::types::{Addon, Cluster, FargateProfile, Nodegroup}; -use k8s_openapi::api::core::v1::ConfigMap; +use k8s_openapi::api::core::v1::{ConfigMap, Service}; use crate::{ eks::resources::{self as eks_resources, AddonVersion, ClusterInsight, LaunchTemplate, VpcSubnet}, @@ -56,6 +56,7 @@ pub trait K8sClients { fn get_eniconfigs(&self) -> impl std::future::Future>> + Send; fn get_resources(&self) -> impl std::future::Future>> + Send; fn get_pod_disruption_budgets(&self) -> impl std::future::Future>> + Send; + fn get_services(&self) -> impl std::future::Future>> + Send; } /// Real AWS client implementation wrapping the SDK clients @@ -167,4 +168,8 @@ impl K8sClients for RealK8sClients { async fn get_pod_disruption_budgets(&self) -> Result> { k8s_resources::get_pod_disruption_budgets(&self.client).await } + + async fn get_services(&self) -> Result> { + k8s_resources::get_services(&self.client).await + } } diff --git a/eksup/src/config.rs b/eksup/src/config.rs index 39b10ef..146a44e 100644 --- a/eksup/src/config.rs +++ b/eksup/src/config.rs @@ -49,8 +49,15 @@ pub struct ChecksConfig { #[serde(default, rename = "K8S013")] pub k8s013: WorkloadCheckConfig, - // K8S012 (KubeProxyIpvsMode) is intentionally absent — its finding type has - // no `resource` field, so name+namespace ignore doesn't apply. + #[serde(default, rename = "K8S015")] + pub k8s015: WorkloadCheckConfig, + + #[serde(default, rename = "K8S016")] + pub k8s016: WorkloadCheckConfig, + + // K8S012 (KubeProxyIpvsMode) and K8S014 (ContainerdVersion) are intentionally + // absent — their findings are cluster or node level, so name+namespace ignore + // doesn't apply. #[serde(skip)] compiled: OnceLock, } @@ -69,6 +76,8 @@ impl Clone for ChecksConfig { k8s007: self.k8s007.clone(), k8s008: self.k8s008.clone(), k8s013: self.k8s013.clone(), + k8s015: self.k8s015.clone(), + k8s016: self.k8s016.clone(), compiled: OnceLock::new(), } } @@ -181,6 +190,8 @@ impl ChecksConfig { (Code::K8S007, &self.k8s007.ignore), (Code::K8S008, &self.k8s008.ignore), (Code::K8S013, &self.k8s013.ignore), + (Code::K8S015, &self.k8s015.ignore), + (Code::K8S016, &self.k8s016.ignore), ] { let compiled = selectors.iter().map(|s| s.compile()).collect::>>()?; if !compiled.is_empty() { @@ -639,6 +650,8 @@ checks: assert!(cfg.k8s007.ignore.is_empty()); assert!(cfg.k8s008.ignore.is_empty()); assert!(cfg.k8s013.ignore.is_empty()); + assert!(cfg.k8s015.ignore.is_empty()); + assert!(cfg.k8s016.ignore.is_empty()); } #[test] @@ -657,7 +670,7 @@ checks: } #[test] - fn deserialize_k8s003_through_k8s013() { + fn deserialize_k8s003_through_k8s016() { let yaml = r#" checks: K8S003: { ignore: [{ name: "a", namespace: "x" }] } @@ -666,6 +679,8 @@ checks: K8S007: { ignore: [{ name: "d", namespace: "w" }] } K8S008: { ignore: [{ name: "e", namespace: "v" }] } K8S013: { ignore: [{ name: "g", namespace: "t" }] } + K8S015: { ignore: [{ name: "h", namespace: "s" }] } + K8S016: { ignore: [{ name: "i", namespace: "r" }] } "#; let cfg: Config = serde_yaml::from_str(yaml).unwrap(); assert_eq!(cfg.checks.k8s003.ignore[0].name, "a"); @@ -674,6 +689,8 @@ checks: assert_eq!(cfg.checks.k8s007.ignore[0].name, "d"); assert_eq!(cfg.checks.k8s008.ignore[0].name, "e"); assert_eq!(cfg.checks.k8s013.ignore[0].name, "g"); + assert_eq!(cfg.checks.k8s015.ignore[0].name, "h"); + assert_eq!(cfg.checks.k8s016.ignore[0].name, "i"); } #[test] diff --git a/eksup/src/finding.rs b/eksup/src/finding.rs index e2fdc86..ff1ca15 100644 --- a/eksup/src/finding.rs +++ b/eksup/src/finding.rs @@ -176,6 +176,9 @@ define_codes! { K8S011 => { desc: "kube-proxy version skew with kubelet", from: None, until: None }, K8S012 => { desc: "kube-proxy IPVS mode deprecated (1.35+, removal planned 1.43)",from: Some(35), until: None }, K8S013 => { desc: "Ingress NGINX controller retirement (1.35+)", from: Some(35), until: None }, + K8S014 => { desc: "Nodes running containerd 1.x (unsupported from 1.36)", from: Some(36), until: None }, + K8S015 => { desc: "gitRepo volumes in use (disabled in 1.36)", from: Some(36), until: None }, + K8S016 => { desc: "Service externalIPs deprecated (1.36+, removal planned 1.43)", from: Some(36), until: None }, } #[allow(dead_code)] @@ -276,6 +279,14 @@ mod tests { assert!(Code::K8S013.is_applicable(35)); } + #[test] + fn k8s014_through_k8s016_applicable_from_1_36() { + for code in [Code::K8S014, Code::K8S015, Code::K8S016] { + assert!(!code.is_applicable(35), "{code} should not be applicable at 1.35"); + assert!(code.is_applicable(36), "{code} should be applicable at 1.36"); + } + } + #[test] fn code_display() { assert_eq!(Code::AWS001.to_string(), "AWS001"); diff --git a/eksup/src/k8s/checks.rs b/eksup/src/k8s/checks.rs index a6ad30b..420d28e 100644 --- a/eksup/src/k8s/checks.rs +++ b/eksup/src/k8s/checks.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use anyhow::{Context, Result}; -use k8s_openapi::api::core::v1::ConfigMap; +use k8s_openapi::api::core::v1::{ConfigMap, Service}; use serde::{Deserialize, Serialize}; use tabled::{ Table, Tabled, @@ -482,6 +482,150 @@ pub fn ingress_nginx_retirement( Ok(findings) } +/// Prefix the kubelet reports in `nodeInfo.containerRuntimeVersion` for containerd +const CONTAINERD_RUNTIME_PREFIX: &str = "containerd://"; + +/// Oldest containerd major version supported from Kubernetes 1.36 +const CONTAINERD_MIN_MAJOR: u32 = 2; + +#[derive(Debug, Serialize, Deserialize, Tabled)] +#[tabled(rename_all = "UpperCase")] +pub struct ContainerdVersion { + #[tabled(inline)] + pub finding: finding::Finding, + pub name: String, + #[tabled(rename = "CONTAINER RUNTIME")] + pub container_runtime_version: String, +} + +finding::impl_findings!( + ContainerdVersion, + "✅ - No nodes are running a containerd version older than 2.0" +); + +/// Check for nodes running containerd 1.x, which is not supported from Kubernetes 1.36 +pub fn containerd_version(nodes: &[resources::Node], target_minor: i32) -> Vec { + if !Code::K8S014.is_applicable(target_minor) { + return vec![]; + } + + nodes + .iter() + .filter(|node| { + let Some(version) = node.container_runtime_version.strip_prefix(CONTAINERD_RUNTIME_PREFIX) else { + return false; + }; + let version = version.strip_prefix('v').unwrap_or(version); + matches!( + version.split('.').next().and_then(|major| major.parse::().ok()), + Some(major) if major < CONTAINERD_MIN_MAJOR + ) + }) + .map(|node| ContainerdVersion { + finding: Finding::new(Code::K8S014, Remediation::Required), + name: node.name.to_owned(), + container_runtime_version: node.container_runtime_version.to_owned(), + }) + .collect() +} + +#[derive(Debug, Serialize, Deserialize, Tabled)] +#[tabled(rename_all = "UpperCase")] +pub struct GitRepoVolume { + #[tabled(inline)] + pub finding: finding::Finding, + #[tabled(inline)] + pub resource: Resource, + #[tabled(rename = "VOLUME")] + pub volume: String, +} + +finding::impl_findings!(GitRepoVolume, "✅ - No workloads are using `gitRepo` volumes"); + +impl WorkloadFinding for GitRepoVolume { + fn resource(&self) -> (&str, &str) { + (&self.resource.name, &self.resource.namespace) + } + fn code(&self) -> Code { + Code::K8S015 + } +} + +/// Check for workloads using `gitRepo` volumes, which the kubelet refuses to run from 1.36 +pub fn git_repo_volumes(resources: &[resources::StdResource], target_minor: i32) -> Vec { + if !Code::K8S015.is_applicable(target_minor) { + return vec![]; + } + + let mut findings = Vec::new(); + + for resource in resources { + let volumes = resource + .spec + .template + .as_ref() + .and_then(|t| t.spec.as_ref()) + .and_then(|s| s.volumes.as_deref()) + .unwrap_or_default(); + + for volume in volumes.iter().filter(|v| v.git_repo.is_some()) { + findings.push(GitRepoVolume { + finding: Finding::new(Code::K8S015, Remediation::Required), + resource: resource.get_resource(), + volume: volume.name.to_owned(), + }); + } + } + + findings +} + +#[derive(Debug, Serialize, Deserialize, Tabled)] +#[tabled(rename_all = "UpperCase")] +pub struct ServiceExternalIps { + #[tabled(inline)] + pub finding: finding::Finding, + pub name: String, + pub namespace: String, + #[tabled(rename = "EXTERNAL IPS")] + pub external_ips: String, +} + +finding::impl_findings!(ServiceExternalIps, "✅ - No Services are using `externalIPs`"); + +impl WorkloadFinding for ServiceExternalIps { + fn resource(&self) -> (&str, &str) { + (&self.name, &self.namespace) + } + fn code(&self) -> Code { + Code::K8S016 + } +} + +/// Check for Services that set `spec.externalIPs`, which is deprecated from 1.36 +pub fn service_external_ips(services: &[Service], target_minor: i32) -> Vec { + if !Code::K8S016.is_applicable(target_minor) { + return vec![]; + } + + services + .iter() + .filter_map(|service| { + let external_ips = service.spec.as_ref()?.external_ips.as_deref()?; + if external_ips.is_empty() { + return None; + } + + Some(ServiceExternalIps { + finding: Finding::new(Code::K8S016, Remediation::Recommended), + name: service.metadata.name.clone().unwrap_or_default(), + namespace: service.metadata.namespace.clone().unwrap_or_default(), + external_ips: external_ips.join(", "), + }) + }) + .collect() +} + #[derive(Debug, Serialize, Deserialize, Tabled)] #[tabled(rename_all = "UpperCase")] pub struct MissingPdb { @@ -634,6 +778,7 @@ mod tests { labels: None, kubelet_version: format!("v1.{minor_version}.0"), minor_version, + container_runtime_version: "containerd://2.1.4".to_string(), } } @@ -974,6 +1119,133 @@ mod tests { assert_eq!(result[1].resource.name, "ingress-nginx-controller-legacy"); } + // =========================================================================== + // containerd_version + // =========================================================================== + + fn make_node_with_runtime(name: &str, runtime: &str) -> resources::Node { + resources::Node { + container_runtime_version: runtime.to_string(), + ..make_node(name, 35) + } + } + + #[test] + fn containerd_version_not_applicable_before_1_36() { + let nodes = vec![make_node_with_runtime("old", "containerd://1.7.27")]; + assert!(containerd_version(&nodes, 35).is_empty()); + } + + #[test] + fn containerd_version_flags_1x_only() { + let nodes = vec![ + make_node_with_runtime("old", "containerd://1.7.27"), + make_node_with_runtime("old-v-prefix", "containerd://v1.6.0"), + make_node_with_runtime("new", "containerd://2.1.4"), + make_node_with_runtime("other-runtime", "cri-o://1.30.0"), + make_node_with_runtime("unparseable", "containerd://unknown"), + ]; + let result = containerd_version(&nodes, 36); + let names: Vec<&str> = result.iter().map(|f| f.name.as_str()).collect(); + assert_eq!(names, ["old", "old-v-prefix"]); + assert!(matches!(result[0].finding.remediation, Remediation::Required)); + assert_eq!(result[0].container_runtime_version, "containerd://1.7.27"); + } + + // =========================================================================== + // git_repo_volumes + // =========================================================================== + + fn make_deployment_with_volumes( + name: &str, + volumes: Vec, + ) -> resources::StdResource { + let mut resource = make_deployment_with_image(name, "default", "app:v1"); + if let Some(spec) = resource.spec.template.as_mut().and_then(|t| t.spec.as_mut()) { + spec.volumes = Some(volumes); + } + resource + } + + #[test] + fn git_repo_volumes_flags_only_git_repo() { + use k8s_openapi::api::core::v1::{EmptyDirVolumeSource, GitRepoVolumeSource, Volume}; + + let with_git = make_deployment_with_volumes( + "with-git", + vec![ + Volume { + name: "scratch".to_string(), + empty_dir: Some(EmptyDirVolumeSource::default()), + ..Default::default() + }, + Volume { + name: "source".to_string(), + git_repo: Some(GitRepoVolumeSource { + repository: "https://example.com/repo.git".to_string(), + ..Default::default() + }), + ..Default::default() + }, + ], + ); + let without_git = make_deployment_with_volumes( + "without-git", + vec![Volume { + name: "scratch".to_string(), + empty_dir: Some(EmptyDirVolumeSource::default()), + ..Default::default() + }], + ); + let resources = [with_git, without_git]; + + assert!(git_repo_volumes(&resources, 35).is_empty()); + + let result = git_repo_volumes(&resources, 36); + assert_eq!(result.len(), 1); + assert_eq!(result[0].resource.name, "with-git"); + assert_eq!(result[0].volume, "source"); + assert!(matches!(result[0].finding.remediation, Remediation::Required)); + } + + // =========================================================================== + // service_external_ips + // =========================================================================== + + fn make_service(name: &str, external_ips: Option>) -> Service { + use k8s_openapi::{api::core::v1::ServiceSpec, apimachinery::pkg::apis::meta::v1::ObjectMeta}; + + Service { + metadata: ObjectMeta { + name: Some(name.to_string()), + namespace: Some("default".to_string()), + ..Default::default() + }, + spec: Some(ServiceSpec { + external_ips: external_ips.map(|ips| ips.into_iter().map(String::from).collect()), + ..Default::default() + }), + ..Default::default() + } + } + + #[test] + fn service_external_ips_flags_only_set_external_ips() { + let services = [ + make_service("with-ips", Some(vec!["192.0.2.10", "192.0.2.11"])), + make_service("empty-ips", Some(vec![])), + make_service("no-ips", None), + ]; + + assert!(service_external_ips(&services, 35).is_empty()); + + let result = service_external_ips(&services, 36); + assert_eq!(result.len(), 1); + assert_eq!(result[0].name, "with-ips"); + assert_eq!(result[0].external_ips, "192.0.2.10, 192.0.2.11"); + assert!(matches!(result[0].finding.remediation, Remediation::Recommended)); + } + // =========================================================================== // pod_disruption_budgets // =========================================================================== diff --git a/eksup/src/k8s/findings.rs b/eksup/src/k8s/findings.rs index 7189426..4352855 100644 --- a/eksup/src/k8s/findings.rs +++ b/eksup/src/k8s/findings.rs @@ -20,11 +20,14 @@ pub struct KubernetesFindings { pub kube_proxy_ipvs_mode: Vec, pub ingress_nginx_retirement: Vec, pub pod_disruption_budgets: Vec, + pub containerd_version: Vec, + pub git_repo_volumes: Vec, + pub service_external_ips: Vec, } /// Findings that were dropped by the ignore filter — same shape as /// `KubernetesFindings`, minus the cluster-level checks (version_skew, -/// kube_proxy_version_skew, kube_proxy_ipvs_mode) which the filter never +/// kube_proxy_version_skew, kube_proxy_ipvs_mode, containerd_version) which the filter never /// touches. The wrapper struct always serializes (no `skip_serializing_if` /// anywhere) so the JSON `suppressed:` key is always present. #[derive(Debug, Serialize, Deserialize, Default)] @@ -37,6 +40,8 @@ pub struct KubernetesSuppressed { pub docker_socket: Vec, pub ingress_nginx_retirement: Vec, pub pod_disruption_budgets: Vec, + pub git_repo_volumes: Vec, + pub service_external_ips: Vec, } impl KubernetesSuppressed { @@ -49,6 +54,8 @@ impl KubernetesSuppressed { + self.docker_socket.len() + self.ingress_nginx_retirement.len() + self.pod_disruption_budgets.len() + + self.git_repo_volumes.len() + + self.service_external_ips.len() } } @@ -64,6 +71,7 @@ pub async fn get_kubernetes_findings( let nodes = k8s.get_nodes().await?; let kube_proxy_config = k8s.get_configmap("kube-system", "kube-proxy-config").await?; let pdbs = k8s.get_pod_disruption_budgets().await?; + let services = k8s.get_services().await?; let version_skew = checks::version_skew(&nodes, control_plane_minor); let compiled = checks_config.compiled()?; @@ -100,10 +108,13 @@ pub async fn get_kubernetes_findings( let kube_proxy_ipvs_mode = checks::kube_proxy_ipvs_mode(kube_proxy_config.as_ref(), target_minor)?; let ingress_nginx_retirement = checks::ingress_nginx_retirement(&resources, target_minor)?; let pod_disruption_budgets = checks::pod_disruption_budgets(&resources, &pdbs); + let containerd_version = checks::containerd_version(&nodes, target_minor); + let git_repo_volumes = checks::git_repo_volumes(&resources, target_minor); + let service_external_ips = checks::service_external_ips(&services, target_minor); - // Apply ignores to the 8 workload-level finding Vecs. Cluster-level Vecs - // (version_skew, kube_proxy_version_skew, kube_proxy_ipvs_mode) skip the - // filter — their finding structs don't impl WorkloadFinding. + // Apply ignores to the 10 workload-level finding Vecs. Cluster-level Vecs + // (version_skew, kube_proxy_version_skew, kube_proxy_ipvs_mode, + // containerd_version) skip the filter — their finding structs don't impl WorkloadFinding. let (min_replicas, sup_min_replicas) = apply_ignores(min_replicas, compiled); let (min_ready_seconds, sup_min_ready_seconds) = apply_ignores(min_ready_seconds, compiled); let (readiness_probe, sup_readiness_probe) = apply_ignores(readiness_probe, compiled); @@ -112,6 +123,8 @@ pub async fn get_kubernetes_findings( let (docker_socket, sup_docker_socket) = apply_ignores(docker_socket, compiled); let (ingress_nginx_retirement, sup_ingress_nginx_retirement) = apply_ignores(ingress_nginx_retirement, compiled); let (pod_disruption_budgets, sup_pod_disruption_budgets) = apply_ignores(pod_disruption_budgets, compiled); + let (git_repo_volumes, sup_git_repo_volumes) = apply_ignores(git_repo_volumes, compiled); + let (service_external_ips, sup_service_external_ips) = apply_ignores(service_external_ips, compiled); Ok(( KubernetesFindings { @@ -126,6 +139,9 @@ pub async fn get_kubernetes_findings( kube_proxy_ipvs_mode, ingress_nginx_retirement, pod_disruption_budgets, + containerd_version, + git_repo_volumes, + service_external_ips, }, KubernetesSuppressed { min_replicas: sup_min_replicas, @@ -136,6 +152,8 @@ pub async fn get_kubernetes_findings( docker_socket: sup_docker_socket, ingress_nginx_retirement: sup_ingress_nginx_retirement, pod_disruption_budgets: sup_pod_disruption_budgets, + git_repo_volumes: sup_git_repo_volumes, + service_external_ips: sup_service_external_ips, }, )) } diff --git a/eksup/src/k8s/resources.rs b/eksup/src/k8s/resources.rs index 90f0dc6..88de633 100644 --- a/eksup/src/k8s/resources.rs +++ b/eksup/src/k8s/resources.rs @@ -104,6 +104,8 @@ pub struct Node { pub labels: Option>, pub kubelet_version: String, pub minor_version: i32, + /// Runtime and version as reported by the kubelet, e.g. `containerd://2.1.4` + pub container_runtime_version: String, } pub async fn get_nodes(client: &Client) -> Result> { @@ -124,6 +126,7 @@ pub async fn get_nodes(client: &Client) -> Result> { labels: node.metadata.labels.to_owned(), kubelet_version, minor_version, + container_runtime_version: node_info.container_runtime_version.to_owned(), }) }) .collect(), @@ -617,6 +620,11 @@ pub struct StdPdb { pub max_unavailable: Option, } +pub async fn get_services(client: &Client) -> Result> { + let api: Api = Api::all(client.to_owned()); + list_all(&api, LIST_PAGE_SIZE).await.context("Failed to list Services") +} + pub async fn get_pod_disruption_budgets(client: &Client) -> Result> { let api: Api = Api::all(client.to_owned()); let pdb_list = list_all(&api, LIST_PAGE_SIZE) @@ -1198,6 +1206,7 @@ mod tests { )])), kubelet_version: "v1.28.3".to_string(), minor_version: 28, + container_runtime_version: "containerd://2.1.4".to_string(), }; assert_eq!(node.name, "ip-10-0-1-42"); assert!( @@ -1217,6 +1226,7 @@ mod tests { labels: None, kubelet_version: "v1.30.0".to_string(), minor_version: 30, + container_runtime_version: "containerd://2.1.4".to_string(), }; assert_eq!(node.minor_version, 30); } diff --git a/eksup/src/playbook.rs b/eksup/src/playbook.rs index fa3a9a6..c34587e 100644 --- a/eksup/src/playbook.rs +++ b/eksup/src/playbook.rs @@ -25,6 +25,9 @@ struct Templates; struct Release { release_url: String, deprecation_url: Option, + /// Upgrade changes that eksup cannot detect from the cluster, shown in the playbook as is + #[serde(default)] + notes: Vec, } /// Type alias for Kubernetes version string (i.e. - "1.21") @@ -44,6 +47,7 @@ pub struct TemplateData { target_version: String, k8s_release_url: String, k8s_deprecation_url: String, + k8s_release_notes: Vec, control_plane_ips: String, pod_ips: String, cluster_health: String, @@ -65,6 +69,9 @@ pub struct TemplateData { kube_proxy_ipvs_mode: String, ingress_nginx_retirement: String, pod_disruption_budgets: String, + containerd_version: String, + git_repo_volumes: String, + service_external_ips: String, ec2_limits: String, ebs_gp2_limits: String, ebs_gp3_limits: String, @@ -177,6 +184,7 @@ pub fn render( Some(url) => url.to_string(), None => "".to_string(), }, + k8s_release_notes: release.notes.clone(), control_plane_ips: subnet_findings.control_plane_ips.to_markdown_table("\t")?, pod_ips: subnet_findings.pod_ips.to_markdown_table("\t")?, cluster_health: cluster_findings.cluster_health.to_markdown_table("\t")?, @@ -198,6 +206,9 @@ pub fn render( kube_proxy_ipvs_mode: kubernetes_findings.kube_proxy_ipvs_mode.to_markdown_table("\t")?, ingress_nginx_retirement: kubernetes_findings.ingress_nginx_retirement.to_markdown_table("\t")?, pod_disruption_budgets: kubernetes_findings.pod_disruption_budgets.to_markdown_table("\t")?, + containerd_version: kubernetes_findings.containerd_version.to_markdown_table("\t")?, + git_repo_volumes: kubernetes_findings.git_repo_volumes.to_markdown_table("\t")?, + service_external_ips: kubernetes_findings.service_external_ips.to_markdown_table("\t")?, ec2_limits: analysis.service_limits.ec2_limits.to_markdown_table("\t")?, ebs_gp2_limits: analysis.service_limits.ebs_gp2_limits.to_markdown_table("\t")?, ebs_gp3_limits: analysis.service_limits.ebs_gp3_limits.to_markdown_table("\t")?, @@ -253,6 +264,16 @@ pub fn render( rendered.push_str(&kubernetes_suppressed.ingress_nginx_retirement.to_markdown_table("")?); rendered.push('\n'); } + if !kubernetes_suppressed.git_repo_volumes.is_empty() { + rendered.push_str("#### Check [[K8S015]](https://clowdhaus.github.io/eksup/info/checks/#k8s015)\n"); + rendered.push_str(&kubernetes_suppressed.git_repo_volumes.to_markdown_table("")?); + rendered.push('\n'); + } + if !kubernetes_suppressed.service_external_ips.is_empty() { + rendered.push_str("#### Check [[K8S016]](https://clowdhaus.github.io/eksup/info/checks/#k8s016)\n"); + rendered.push_str(&kubernetes_suppressed.service_external_ips.to_markdown_table("")?); + rendered.push('\n'); + } } else { rendered.push_str(&format!( "\n_{suppressed_total} additional finding(s) suppressed by `.eksup.yaml`. Run with `--show-suppressed` to view._\n" diff --git a/eksup/templates/data.yaml b/eksup/templates/data.yaml index c194761..968f3b7 100644 --- a/eksup/templates/data.yaml +++ b/eksup/templates/data.yaml @@ -12,3 +12,6 @@ '1.37': release_url: https://kubernetes.io/blog/2026/08/26/kubernetes-v1-37-release/ + notes: + - "SELinux mount labeling is on by default. Pods that share a volume from a CSI driver with `seLinuxMount: true` must use the same `seLinuxOptions.level` and `seLinuxChangePolicy`, otherwise they stay in `ContainerCreating`. List the drivers with `kubectl get csidriver -o custom-columns=NAME:.metadata.name,SELINUXMOUNT:.spec.seLinuxMount`." + - "The kubelet no longer starts when a deprecated cAdvisor flag is set (other than `--housekeeping-interval`). EKS optimized AMIs do not set them; remove them from custom AMIs and extra kubelet arguments." diff --git a/eksup/templates/playbook.md b/eksup/templates/playbook.md index 567c0ff..a5decaa 100644 --- a/eksup/templates/playbook.md +++ b/eksup/templates/playbook.md @@ -43,6 +43,9 @@ {{/if}} - ℹ️ [Kubernetes `{{ target_version }}` release announcement]({{ k8s_release_url }}) - ℹ️ [EKS `{{ target_version }}` release notes](https://docs.aws.amazon.com/eks/latest/userguide/kubernetes-versions.html#kubernetes-{{ target_version }}) +{{#each k8s_release_notes }} + - ⚠️ {{ this }} +{{/each}} 2. Per the [Kubernetes version skew policy](https://kubernetes.io/releases/version-skew-policy/#supported-version-skew), the `kubelet` version must not be newer than `kube-apiserver`, and may be up to two minor versions older. It is recommended that the nodes in the data plane are aligned with the same minor version as the control plane before upgrading. @@ -220,6 +223,15 @@ When upgrading the control plane, Amazon EKS performs standard infrastructure an #### Check [[K8S013]](https://clowdhaus.github.io/eksup/info/checks/#k8s013) {{ ingress_nginx_retirement }} + #### Check [[K8S014]](https://clowdhaus.github.io/eksup/info/checks/#k8s014) +{{ containerd_version }} + + #### Check [[K8S015]](https://clowdhaus.github.io/eksup/info/checks/#k8s015) +{{ git_repo_volumes }} + + #### Check [[K8S016]](https://clowdhaus.github.io/eksup/info/checks/#k8s016) +{{ service_external_ips }} + #### Check [[AWS001]](https://clowdhaus.github.io/eksup/info/checks/#aws001) {{ node_ips }} diff --git a/eksup/tests/common/fixtures.rs b/eksup/tests/common/fixtures.rs index 40e2b0a..5c5862c 100644 --- a/eksup/tests/common/fixtures.rs +++ b/eksup/tests/common/fixtures.rs @@ -53,6 +53,7 @@ pub fn make_node(name: &str, minor: i32) -> Node { labels: None, kubelet_version: format!("v1.{minor}.0"), minor_version: minor, + container_runtime_version: "containerd://2.1.4".into(), } } diff --git a/eksup/tests/common/mock_k8s.rs b/eksup/tests/common/mock_k8s.rs index 7c59675..726b4d7 100644 --- a/eksup/tests/common/mock_k8s.rs +++ b/eksup/tests/common/mock_k8s.rs @@ -5,7 +5,7 @@ use eksup::{ clients::K8sClients, k8s::resources::{ENIConfig, Node, StdPdb, StdResource}, }; -use k8s_openapi::api::core::v1::ConfigMap; +use k8s_openapi::api::core::v1::{ConfigMap, Service}; /// Mock K8s client for testing #[derive(Clone, Default)] @@ -15,6 +15,7 @@ pub struct MockK8sClients { pub eniconfigs: Vec, pub resources: Vec, pub pdbs: Vec, + pub services: Vec, } impl K8sClients for MockK8sClients { @@ -37,6 +38,10 @@ impl K8sClients for MockK8sClients { async fn get_pod_disruption_budgets(&self) -> Result> { Ok(self.pdbs.clone()) } + + async fn get_services(&self) -> Result> { + Ok(self.services.clone()) + } } /// Mock that returns errors for all methods @@ -58,4 +63,7 @@ impl K8sClients for MockK8sClientsError { async fn get_pod_disruption_budgets(&self) -> Result> { bail!("mock K8s error") } + async fn get_services(&self) -> Result> { + bail!("mock K8s error") + } } diff --git a/eksup/tests/e2e.rs b/eksup/tests/e2e.rs index 35b05be..231d695 100644 --- a/eksup/tests/e2e.rs +++ b/eksup/tests/e2e.rs @@ -373,6 +373,31 @@ async fn playbook_pod_ips_healthy_when_no_custom_networking() { ); } +#[tokio::test] +async fn playbook_release_notes_rendered_for_1_37_target() { + let aws = aws_at_version("1.36"); + let k8s = fixtures::healthy_k8s(); + let output = render_playbook(&aws, &k8s).await; + + assert!( + output.contains("SELinux mount labeling"), + "1.37 notes should be rendered" + ); + assert!( + output.contains("deprecated cAdvisor flag"), + "1.37 notes should be rendered" + ); +} + +#[tokio::test] +async fn playbook_release_notes_absent_without_notes() { + let aws = aws_at_version("1.35"); + let k8s = fixtures::healthy_k8s(); + let output = render_playbook(&aws, &k8s).await; + + assert!(!output.contains("SELinux mount labeling"), "1.36 has no notes"); +} + #[tokio::test] async fn playbook_deprecation_url_absent_for_1_33_target() { // Cluster at 1.32 -> target 1.33 (no deprecation_url in data.yaml) diff --git a/eksup/tests/integration.rs b/eksup/tests/integration.rs index aaed389..8d6d8e4 100644 --- a/eksup/tests/integration.rs +++ b/eksup/tests/integration.rs @@ -187,6 +187,46 @@ async fn kubernetes_findings_version_skew() { assert_eq!(result.version_skew.len(), 1); } +#[tokio::test] +async fn kubernetes_findings_1_36_checks() { + use k8s_openapi::{ + api::core::v1::{Service, ServiceSpec}, + apimachinery::pkg::apis::meta::v1::ObjectMeta, + }; + + let k8s = MockK8sClients { + nodes: vec![eksup::k8s::resources::Node { + container_runtime_version: "containerd://1.7.27".into(), + ..fixtures::make_node("node-1", 35) + }], + services: vec![Service { + metadata: ObjectMeta { + name: Some("legacy".into()), + namespace: Some("default".into()), + ..Default::default() + }, + spec: Some(ServiceSpec { + external_ips: Some(vec!["192.0.2.10".into()]), + ..Default::default() + }), + ..Default::default() + }], + ..Default::default() + }; + + let (result, _suppressed) = eksup::k8s::get_kubernetes_findings(&k8s, 35, 36, &ChecksConfig::default()) + .await + .unwrap(); + assert_eq!(result.containerd_version.len(), 1); + assert_eq!(result.service_external_ips.len(), 1); + + let (result, _suppressed) = eksup::k8s::get_kubernetes_findings(&k8s, 34, 35, &ChecksConfig::default()) + .await + .unwrap(); + assert!(result.containerd_version.is_empty(), "K8S014 starts at 1.36"); + assert!(result.service_external_ips.is_empty(), "K8S016 starts at 1.36"); +} + #[tokio::test] async fn kubernetes_findings_workload_issues() { let k8s = MockK8sClients { diff --git a/eksup/tests/snapshots/e2e__healthy_cluster_json.snap b/eksup/tests/snapshots/e2e__healthy_cluster_json.snap index 7900bfb..ff2f033 100644 --- a/eksup/tests/snapshots/e2e__healthy_cluster_json.snap +++ b/eksup/tests/snapshots/e2e__healthy_cluster_json.snap @@ -1,5 +1,6 @@ --- source: eksup/tests/e2e.rs +assertion_line: 45 expression: output --- { @@ -35,7 +36,10 @@ expression: output "kube_proxy_version_skew": [], "kube_proxy_ipvs_mode": [], "ingress_nginx_retirement": [], - "pod_disruption_budgets": [] + "pod_disruption_budgets": [], + "containerd_version": [], + "git_repo_volumes": [], + "service_external_ips": [] }, "suppressed": { "min_replicas": [], @@ -45,7 +49,9 @@ expression: output "termination_grace_period": [], "docker_socket": [], "ingress_nginx_retirement": [], - "pod_disruption_budgets": [] + "pod_disruption_budgets": [], + "git_repo_volumes": [], + "service_external_ips": [] }, "service_limits": { "ec2_limits": [], diff --git a/eksup/tests/snapshots/e2e__mixed_findings_json.snap b/eksup/tests/snapshots/e2e__mixed_findings_json.snap index ef376c8..2a011cd 100644 --- a/eksup/tests/snapshots/e2e__mixed_findings_json.snap +++ b/eksup/tests/snapshots/e2e__mixed_findings_json.snap @@ -125,7 +125,10 @@ expression: output "kube_proxy_version_skew": [], "kube_proxy_ipvs_mode": [], "ingress_nginx_retirement": [], - "pod_disruption_budgets": [] + "pod_disruption_budgets": [], + "containerd_version": [], + "git_repo_volumes": [], + "service_external_ips": [] }, "suppressed": { "min_replicas": [], @@ -135,7 +138,9 @@ expression: output "termination_grace_period": [], "docker_socket": [], "ingress_nginx_retirement": [], - "pod_disruption_budgets": [] + "pod_disruption_budgets": [], + "git_repo_volumes": [], + "service_external_ips": [] }, "service_limits": { "ec2_limits": [], diff --git a/eksup/tests/snapshots/e2e__playbook_eks_managed_nodegroups.snap b/eksup/tests/snapshots/e2e__playbook_eks_managed_nodegroups.snap index 7791ff1..af744d6 100644 --- a/eksup/tests/snapshots/e2e__playbook_eks_managed_nodegroups.snap +++ b/eksup/tests/snapshots/e2e__playbook_eks_managed_nodegroups.snap @@ -214,6 +214,15 @@ When upgrading the control plane, Amazon EKS performs standard infrastructure an #### Check [[K8S013]](https://clowdhaus.github.io/eksup/info/checks/#k8s013) ✅ - No Ingress NGINX controller images detected that require migration + #### Check [[K8S014]](https://clowdhaus.github.io/eksup/info/checks/#k8s014) + ✅ - No nodes are running a containerd version older than 2.0 + + #### Check [[K8S015]](https://clowdhaus.github.io/eksup/info/checks/#k8s015) + ✅ - No workloads are using `gitRepo` volumes + + #### Check [[K8S016]](https://clowdhaus.github.io/eksup/info/checks/#k8s016) + ✅ - No Services are using `externalIPs` + #### Check [[AWS001]](https://clowdhaus.github.io/eksup/info/checks/#aws001) ✅ - There is sufficient IP space in the subnets provided diff --git a/eksup/tests/snapshots/e2e__playbook_fargate_profiles.snap b/eksup/tests/snapshots/e2e__playbook_fargate_profiles.snap index d58181e..2a698f2 100644 --- a/eksup/tests/snapshots/e2e__playbook_fargate_profiles.snap +++ b/eksup/tests/snapshots/e2e__playbook_fargate_profiles.snap @@ -213,6 +213,15 @@ When upgrading the control plane, Amazon EKS performs standard infrastructure an #### Check [[K8S013]](https://clowdhaus.github.io/eksup/info/checks/#k8s013) ✅ - No Ingress NGINX controller images detected that require migration + #### Check [[K8S014]](https://clowdhaus.github.io/eksup/info/checks/#k8s014) + ✅ - No nodes are running a containerd version older than 2.0 + + #### Check [[K8S015]](https://clowdhaus.github.io/eksup/info/checks/#k8s015) + ✅ - No workloads are using `gitRepo` volumes + + #### Check [[K8S016]](https://clowdhaus.github.io/eksup/info/checks/#k8s016) + ✅ - No Services are using `externalIPs` + #### Check [[AWS001]](https://clowdhaus.github.io/eksup/info/checks/#aws001) ✅ - There is sufficient IP space in the subnets provided diff --git a/eksup/tests/snapshots/e2e__playbook_healthy.snap b/eksup/tests/snapshots/e2e__playbook_healthy.snap index ed04437..7be8572 100644 --- a/eksup/tests/snapshots/e2e__playbook_healthy.snap +++ b/eksup/tests/snapshots/e2e__playbook_healthy.snap @@ -212,6 +212,15 @@ When upgrading the control plane, Amazon EKS performs standard infrastructure an #### Check [[K8S013]](https://clowdhaus.github.io/eksup/info/checks/#k8s013) ✅ - No Ingress NGINX controller images detected that require migration + #### Check [[K8S014]](https://clowdhaus.github.io/eksup/info/checks/#k8s014) + ✅ - No nodes are running a containerd version older than 2.0 + + #### Check [[K8S015]](https://clowdhaus.github.io/eksup/info/checks/#k8s015) + ✅ - No workloads are using `gitRepo` volumes + + #### Check [[K8S016]](https://clowdhaus.github.io/eksup/info/checks/#k8s016) + ✅ - No Services are using `externalIPs` + #### Check [[AWS001]](https://clowdhaus.github.io/eksup/info/checks/#aws001) ✅ - There is sufficient IP space in the subnets provided diff --git a/eksup/tests/snapshots/e2e__playbook_mixed.snap b/eksup/tests/snapshots/e2e__playbook_mixed.snap index d1fdf93..26c9bc3 100644 --- a/eksup/tests/snapshots/e2e__playbook_mixed.snap +++ b/eksup/tests/snapshots/e2e__playbook_mixed.snap @@ -234,6 +234,15 @@ When upgrading the control plane, Amazon EKS performs standard infrastructure an #### Check [[K8S013]](https://clowdhaus.github.io/eksup/info/checks/#k8s013) ✅ - No Ingress NGINX controller images detected that require migration + #### Check [[K8S014]](https://clowdhaus.github.io/eksup/info/checks/#k8s014) + ✅ - No nodes are running a containerd version older than 2.0 + + #### Check [[K8S015]](https://clowdhaus.github.io/eksup/info/checks/#k8s015) + ✅ - No workloads are using `gitRepo` volumes + + #### Check [[K8S016]](https://clowdhaus.github.io/eksup/info/checks/#k8s016) + ✅ - No Services are using `externalIPs` + #### Check [[AWS001]](https://clowdhaus.github.io/eksup/info/checks/#aws001) ✅ - There is sufficient IP space in the subnets provided diff --git a/eksup/tests/snapshots/e2e__workload_issues_json.snap b/eksup/tests/snapshots/e2e__workload_issues_json.snap index 2ad15ef..f9fec86 100644 --- a/eksup/tests/snapshots/e2e__workload_issues_json.snap +++ b/eksup/tests/snapshots/e2e__workload_issues_json.snap @@ -1,5 +1,6 @@ --- source: eksup/tests/e2e.rs +assertion_line: 104 expression: output --- { @@ -148,7 +149,10 @@ expression: output "has_min_available": false, "has_max_unavailable": false } - ] + ], + "containerd_version": [], + "git_repo_volumes": [], + "service_external_ips": [] }, "suppressed": { "min_replicas": [], @@ -158,7 +162,9 @@ expression: output "termination_grace_period": [], "docker_socket": [], "ingress_nginx_retirement": [], - "pod_disruption_budgets": [] + "pod_disruption_budgets": [], + "git_repo_volumes": [], + "service_external_ips": [] }, "service_limits": { "ec2_limits": [], From e9d3a4861a11ec6b744e1821f6870532bf82ebc6 Mon Sep 17 00:00:00 2001 From: Bryant Biggs Date: Sat, 17 Oct 2026 12:33:59 -0500 Subject: [PATCH 4/4] fix: address review findings on the 1.36 checks --- docs/info/checks.md | 4 ++- eksup/src/config.rs | 2 +- eksup/src/k8s/checks.rs | 32 ++++++++++++++++++- eksup/src/k8s/findings.rs | 10 ++++-- .../snapshots/e2e__healthy_cluster_json.snap | 1 - .../snapshots/e2e__mixed_findings_json.snap | 1 - .../snapshots/e2e__mixed_findings_text.snap | 1 - .../e2e__playbook_eks_managed_nodegroups.snap | 1 - .../e2e__playbook_fargate_profiles.snap | 1 - .../snapshots/e2e__playbook_healthy.snap | 1 - .../tests/snapshots/e2e__playbook_mixed.snap | 1 - .../snapshots/e2e__version_skew_text.snap | 1 - .../snapshots/e2e__workload_issues_json.snap | 1 - 13 files changed, 43 insertions(+), 14 deletions(-) diff --git a/docs/info/checks.md b/docs/info/checks.md index e551e56..b116b5e 100644 --- a/docs/info/checks.md +++ b/docs/info/checks.md @@ -374,7 +374,7 @@ This check scans Deployments and DaemonSets for container images referencing the Kubernetes `v1.35` is the last release that supports containerd 1.x. Nodes MUST run containerd 2.0 or later before upgrading to Kubernetes `v1.36` or later. The check reads the container runtime version each node reports. -EKS optimized AMIs ship containerd 2.x from Kubernetes `v1.34`, so findings usually point at nodes from older or custom AMIs. The replacement nodes MUST also come from an AMI that ships containerd 2.0 or later. +EKS optimized AMIs ship containerd 2.x from Kubernetes `v1.34`, so findings usually point at nodes from older or custom AMIs. The replacement nodes MUST also come from an AMI that ships containerd 2.0 or later. Fargate and EKS Auto Mode nodes are skipped because AWS manages their container runtime. [EKS Kubernetes 1.35 release notes](https://docs.aws.amazon.com/eks/latest/userguide/kubernetes-versions-standard.html) @@ -384,6 +384,8 @@ EKS optimized AMIs ship containerd 2.x from Kubernetes `v1.34`, so findings usua The `gitRepo` volume type is permanently disabled in Kubernetes `v1.36`. The API server still accepts pods that use it, but the kubelet refuses to run them. Workloads using `gitRepo` volumes MUST migrate to an init container or a git-sync sidecar before upgrading to Kubernetes `v1.36` or later. +Like the other workload checks, this check reads the pod templates of the workload controllers in the table above. Pods created directly, or by controllers outside that table, are not inspected. + [KEP-5040](https://github.com/kubernetes/enhancements/issues/5040) #### K8S016 diff --git a/eksup/src/config.rs b/eksup/src/config.rs index 146a44e..07da811 100644 --- a/eksup/src/config.rs +++ b/eksup/src/config.rs @@ -56,7 +56,7 @@ pub struct ChecksConfig { pub k8s016: WorkloadCheckConfig, // K8S012 (KubeProxyIpvsMode) and K8S014 (ContainerdVersion) are intentionally - // absent — their findings are cluster or node level, so name+namespace ignore + // absent: their findings are cluster or node level, so name+namespace ignore // doesn't apply. #[serde(skip)] compiled: OnceLock, diff --git a/eksup/src/k8s/checks.rs b/eksup/src/k8s/checks.rs index 420d28e..5795d36 100644 --- a/eksup/src/k8s/checks.rs +++ b/eksup/src/k8s/checks.rs @@ -488,6 +488,12 @@ const CONTAINERD_RUNTIME_PREFIX: &str = "containerd://"; /// Oldest containerd major version supported from Kubernetes 1.36 const CONTAINERD_MIN_MAJOR: u32 = 2; +/// Node label EKS sets to identify the compute type of a node +const COMPUTE_TYPE_LABEL: &str = "eks.amazonaws.com/compute-type"; + +/// Compute types where AWS manages the container runtime, so the customer cannot change it +const AWS_MANAGED_COMPUTE_TYPES: [&str; 2] = ["fargate", "auto"]; + #[derive(Debug, Serialize, Deserialize, Tabled)] #[tabled(rename_all = "UpperCase")] pub struct ContainerdVersion { @@ -503,7 +509,8 @@ finding::impl_findings!( "✅ - No nodes are running a containerd version older than 2.0" ); -/// Check for nodes running containerd 1.x, which is not supported from Kubernetes 1.36 +/// Check for nodes running containerd 1.x, which is not supported from Kubernetes 1.36. +/// Fargate and EKS Auto Mode nodes are skipped because AWS manages their runtime. pub fn containerd_version(nodes: &[resources::Node], target_minor: i32) -> Vec { if !Code::K8S014.is_applicable(target_minor) { return vec![]; @@ -511,6 +518,10 @@ pub fn containerd_version(nodes: &[resources::Node], target_minor: i32) -> Vec = result.iter().map(|f| f.name.as_str()).collect(); + assert_eq!(names, ["hybrid"]); + } + // =========================================================================== // git_repo_volumes // =========================================================================== diff --git a/eksup/src/k8s/findings.rs b/eksup/src/k8s/findings.rs index 4352855..430324c 100644 --- a/eksup/src/k8s/findings.rs +++ b/eksup/src/k8s/findings.rs @@ -4,6 +4,7 @@ use tracing::warn; use crate::{ clients::K8sClients, + finding::Code, k8s::checks::{self, K8sFindings}, }; @@ -71,7 +72,12 @@ pub async fn get_kubernetes_findings( let nodes = k8s.get_nodes().await?; let kube_proxy_config = k8s.get_configmap("kube-system", "kube-proxy-config").await?; let pdbs = k8s.get_pod_disruption_budgets().await?; - let services = k8s.get_services().await?; + // Services are only read for K8S016, so earlier targets do not need list access to them + let services = if Code::K8S016.is_applicable(target_minor) { + k8s.get_services().await? + } else { + vec![] + }; let version_skew = checks::version_skew(&nodes, control_plane_minor); let compiled = checks_config.compiled()?; @@ -114,7 +120,7 @@ pub async fn get_kubernetes_findings( // Apply ignores to the 10 workload-level finding Vecs. Cluster-level Vecs // (version_skew, kube_proxy_version_skew, kube_proxy_ipvs_mode, - // containerd_version) skip the filter — their finding structs don't impl WorkloadFinding. + // containerd_version) skip the filter because their finding structs don't impl WorkloadFinding. let (min_replicas, sup_min_replicas) = apply_ignores(min_replicas, compiled); let (min_ready_seconds, sup_min_ready_seconds) = apply_ignores(min_ready_seconds, compiled); let (readiness_probe, sup_readiness_probe) = apply_ignores(readiness_probe, compiled); diff --git a/eksup/tests/snapshots/e2e__healthy_cluster_json.snap b/eksup/tests/snapshots/e2e__healthy_cluster_json.snap index ff2f033..19f9df8 100644 --- a/eksup/tests/snapshots/e2e__healthy_cluster_json.snap +++ b/eksup/tests/snapshots/e2e__healthy_cluster_json.snap @@ -1,6 +1,5 @@ --- source: eksup/tests/e2e.rs -assertion_line: 45 expression: output --- { diff --git a/eksup/tests/snapshots/e2e__mixed_findings_json.snap b/eksup/tests/snapshots/e2e__mixed_findings_json.snap index 2a011cd..9a21035 100644 --- a/eksup/tests/snapshots/e2e__mixed_findings_json.snap +++ b/eksup/tests/snapshots/e2e__mixed_findings_json.snap @@ -1,6 +1,5 @@ --- source: eksup/tests/e2e.rs -assertion_line: 176 expression: output --- { diff --git a/eksup/tests/snapshots/e2e__mixed_findings_text.snap b/eksup/tests/snapshots/e2e__mixed_findings_text.snap index 00a7fdb..559576f 100644 --- a/eksup/tests/snapshots/e2e__mixed_findings_text.snap +++ b/eksup/tests/snapshots/e2e__mixed_findings_text.snap @@ -1,6 +1,5 @@ --- source: eksup/tests/e2e.rs -assertion_line: 150 expression: output --- ┌────────┬────┬──────────┬──────────────┐ diff --git a/eksup/tests/snapshots/e2e__playbook_eks_managed_nodegroups.snap b/eksup/tests/snapshots/e2e__playbook_eks_managed_nodegroups.snap index af744d6..88b043b 100644 --- a/eksup/tests/snapshots/e2e__playbook_eks_managed_nodegroups.snap +++ b/eksup/tests/snapshots/e2e__playbook_eks_managed_nodegroups.snap @@ -1,6 +1,5 @@ --- source: eksup/tests/e2e.rs -assertion_line: 256 expression: output --- # EKS Cluster Upgrade diff --git a/eksup/tests/snapshots/e2e__playbook_fargate_profiles.snap b/eksup/tests/snapshots/e2e__playbook_fargate_profiles.snap index 2a698f2..a4a0544 100644 --- a/eksup/tests/snapshots/e2e__playbook_fargate_profiles.snap +++ b/eksup/tests/snapshots/e2e__playbook_fargate_profiles.snap @@ -1,6 +1,5 @@ --- source: eksup/tests/e2e.rs -assertion_line: 265 expression: output --- # EKS Cluster Upgrade diff --git a/eksup/tests/snapshots/e2e__playbook_healthy.snap b/eksup/tests/snapshots/e2e__playbook_healthy.snap index 7be8572..4a900e6 100644 --- a/eksup/tests/snapshots/e2e__playbook_healthy.snap +++ b/eksup/tests/snapshots/e2e__playbook_healthy.snap @@ -1,6 +1,5 @@ --- source: eksup/tests/e2e.rs -assertion_line: 247 expression: output --- # EKS Cluster Upgrade diff --git a/eksup/tests/snapshots/e2e__playbook_mixed.snap b/eksup/tests/snapshots/e2e__playbook_mixed.snap index 26c9bc3..b8635d0 100644 --- a/eksup/tests/snapshots/e2e__playbook_mixed.snap +++ b/eksup/tests/snapshots/e2e__playbook_mixed.snap @@ -1,6 +1,5 @@ --- source: eksup/tests/e2e.rs -assertion_line: 279 expression: output --- # EKS Cluster Upgrade diff --git a/eksup/tests/snapshots/e2e__version_skew_text.snap b/eksup/tests/snapshots/e2e__version_skew_text.snap index e0f262a..1665f50 100644 --- a/eksup/tests/snapshots/e2e__version_skew_text.snap +++ b/eksup/tests/snapshots/e2e__version_skew_text.snap @@ -1,6 +1,5 @@ --- source: eksup/tests/e2e.rs -assertion_line: 120 expression: output --- ┌────────┬────┬────────┬───────┬───────────────┬──────┐ diff --git a/eksup/tests/snapshots/e2e__workload_issues_json.snap b/eksup/tests/snapshots/e2e__workload_issues_json.snap index f9fec86..f05fa95 100644 --- a/eksup/tests/snapshots/e2e__workload_issues_json.snap +++ b/eksup/tests/snapshots/e2e__workload_issues_json.snap @@ -1,6 +1,5 @@ --- source: eksup/tests/e2e.rs -assertion_line: 104 expression: output --- {