Skip to content

STOR-2918: Rebase to upstream v1.62.0 for OCP 5.0 - #329

Open
dfajmon wants to merge 118 commits into
openshift:masterfrom
dfajmon:rebase-v1.62.0
Open

STOR-2918: Rebase to upstream v1.62.0 for OCP 5.0#329
dfajmon wants to merge 118 commits into
openshift:masterfrom
dfajmon:rebase-v1.62.0

Conversation

@dfajmon

@dfajmon dfajmon commented Jul 3, 2026

Copy link
Copy Markdown

STOR-2918: Rebase to upstream v1.62.0 for OCP 5.0

Issue link
https://redhat.atlassian.net/browse/STOR-2918

Diff to upstream v1.62.0
kubernetes-sigs/aws-ebs-csi-driver@v1.62.0...dfajmon:rebase-v1.62.0

Notes for reviewers

Upgrade notice (v1.49.0): The EBS CSI Driver Controller's readiness and liveness probes now periodically check AWS API access via a dry-run DescribeAvailabilityZones call. Clusters with broken networking/DNS or incorrectly configured IAM may fail to install.

Upgrade notice (v1.59.0): The driver now calls DescribeInstanceTypes at runtime for multi-card EBS instance handling. Custom IAM policies must add DescribeInstanceTypes. A fallback table is still present but will be removed in a future release.

Summary of changes

Breaking Changes

  • Remove -a1compat image for a1.* family Amazon EC2 instances; upgrade to a more recent AWS Graviton instance type (#2836)

Major Features

  • Add ability to provision volumes using AZ-ID (#2613)
  • Add dry-run EC2 API call to ebs-csi-driver controller readiness/liveness probes (#2590)
  • Retrieve maximum IOPS limit dynamically via CreateVolume dry-run to support increased IOPS limits for GP3 and other volume types (#2682)
  • Add support for creating instant, point-in-time copies of EBS volumes within the same Availability Zone (#2716)
  • Add ALPHA metadata-labeler sidecar and metadata source (#2591)
  • Add node-local volume support, enabling a single cluster-wide PV/PVC to mount node-specific EBS volumes with ReadWriteMany access (#2726)
  • Add support for custom build-time plugins to extend or modify EBS CSI Driver behavior (#2786)
  • Automatically assign EBS card index for instances with multiple EBS cards (#2835)
  • Detect, abort, and retry stuck attachments (#2833)
  • Adding SnapshotLock Capabilities (#2797)
  • Add encryption support ext4 format option to StorageClass params (#2820)
  • Add support for Windows Server 2025 (#2831)
  • When --k8s-tag-cluster-id is set, tag volumes and snapshots with ebs.csi.aws.com/cluster-name to support cluster-scoped IAM policies (#2899)
  • Call DescribeInstanceTypes at runtime for EBS multi-card instances (#2916)
  • Add plugin method to override health check (#2952)

Notable Bug Fixes

  • Fix cluster tags incorrectly overriding user-specified tags from StorageClass parameters (#2655)
  • Update several incorrect EBS volume limits via generating table from DescribeInstanceTypes API (#2660)
  • Fix throughput handling when creating volumes (#2733)
  • Fix off-by-one error in volume limits for default nitro instances which would get PVCs stuck attempting to attach to a full instance (#2749)
  • Improve error handling of taint removal; Fix last chance taint removal not functioning (#2699)
  • Fix error handling when re-fetching the node object during agent-not-ready taint removal attempts (#2847)
  • Handle duplicate CreateVolume RPCs for in-use volumes (#2908)
  • Prevent deletion of reserved tag keys during volume modification (#2919)
  • Fix init order bug that allowed modifying tags reserved for use by the EBS CSI Driver (#2932)
  • Fix possible nil pointer deref on clones path (#2931)
  • Add mutex to MetricRecorder to prevent crashes caused by concurrent access (#2933)
  • Fixing warn-on-invalid-tag validation bug (#2954)

CVE Fixes

CVE-2026-33814, CVE-2026-33186, CVE-2025-61726

Cherry-picked commits

Commit Subject Author
fdc2f42a2 UPSTREAM: <carry>: Add OpenShift files Daniel Fajmon
7e6a7798f UPSTREAM: <carry>: Updating ose-aws-ebs-csi-driver-container image to be consistent with ART for 5.0 AOS Automation Release Team
0a05883f1 UPSTREAM: <carry>: Updating ose-aws-ebs-csi-driver-container image to be consistent with ART for 5.0 Radomir Vagner

Upstream changelogs

The project uses a shared CHANGELOG.md covering all releases (no per-release changelog pages):

Full changelog

kubernetes-sigs/aws-ebs-csi-driver@v1.48.0...v1.62.0

Last rebase

#294

@openshift/storage


Co-authored by Claude

Summary by CodeRabbit

  • New Features

    • Added support for node-local volumes, metadata labeling, volume cloning, snapshot locking, and volume property verification.
    • Expanded Windows and E2E test coverage, including new parameterized test runs and multi-card validation.
  • Bug Fixes

    • Improved volume attach limits, resizing behavior, health checks, and node/pod handling.
    • Updated RBAC and workload defaults for better compatibility across Kubernetes versions.
  • Documentation

    • Added and refreshed docs for installation, parameters, snapshots, Outposts, plugins, node-local volumes, and IAM policies.
  • Chores

    • Updated Helm chart/app versions, images, tooling, and release notes.

ConnorJC3 and others added 30 commits January 23, 2026 22:34
Signed-off-by: Connor catlett <conncatl@amazon.com>
…ot/cherry-pick-2837-to-master

[master] Release 1.55
Signed-off-by: Connor catlett <conncatl@amazon.com>
Bump test-helm-chart instance type to account for increased memory usage
Signed-off-by: Eddie Torres <torredil@amazon.com>
Signed-off-by: Eddie Torres <torredil@amazon.com>
Signed-off-by: Eddie Torres <torredil@amazon.com>
Fix error handling when re-fetching the node during agent-not-ready taint removal attempts
Upgrade to volume-modifier-for-k8s 0.9.2
Signed-off-by: Eddie Torres <torredil@amazon.com>
…ot/cherry-pick-2855-to-master

[master] Release 1.55.1
Signed-off-by: Eddie Torres <torredil@amazon.com>
Remove nodes/proxy from helm tester manifests
Signed-off-by: jukie <isaac.wilson514@gmail.com>
…urable

helm: make healthPort configurable
feat(plugin): configure node and volume topology segments
ConnorJC3 and others added 8 commits June 12, 2026 13:33
Signed-off-by: Connor Catlett <conncatl@amazon.com>
…-health-check

Add plugin method to override health check
Signed-off-by: Connor Catlett <conncatl@amazon.com>
Fixing warn-on-invalid-tag validation bug
This commit carries OpenShift-specific files and modifications:
- OpenShift CI configuration (.ci-operator.yaml, OWNERS, OWNERS_ALIASES)
- OpenShift Dockerfile (Dockerfile.openshift.rhel7) for RHEL 9 with Go 1.25 and OpenShift 4.22
- Snyk configuration (.snyk)
- Vendored dependencies (vendor/)
- License header script update to skip vendor directory
- Remove upstream GitHub workflows and templates
- Fix `make verify` not to require a license header in OCP .ci-operator.yaml in generate-license-header.sh

Updated for OpenShift 4.22 release.
@openshift-ci-robot openshift-ci-robot added the jira/valid-reference Indicates that this PR references a valid Jira ticket of any type. label Jul 3, 2026
@openshift-ci openshift-ci Bot added the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Jul 3, 2026
@openshift-ci-robot

openshift-ci-robot commented Jul 3, 2026

Copy link
Copy Markdown

@dfajmon: This pull request references STOR-2918 which is a valid jira issue.

Warning: The referenced jira issue has an invalid target version for the target branch this PR targets: expected the story to target only the "5.0.0" version, but multiple target versions were set.

Details

In response to this:

STOR-2918: Rebase to upstream v1.62.0 for OCP 5.0

Issue link
https://redhat.atlassian.net/browse/STOR-2918

Diff to upstream v1.62.0
kubernetes-sigs/aws-ebs-csi-driver@v1.62.0...dfajmon:rebase-v1.62.0

Notes for reviewers

Upgrade notice (v1.49.0): The EBS CSI Driver Controller's readiness and liveness probes now periodically check AWS API access via a dry-run DescribeAvailabilityZones call. Clusters with broken networking/DNS or incorrectly configured IAM may fail to install.

Upgrade notice (v1.59.0): The driver now calls DescribeInstanceTypes at runtime for multi-card EBS instance handling. Custom IAM policies must add DescribeInstanceTypes. A fallback table is still present but will be removed in a future release.

Summary of changes

Breaking Changes

  • Remove -a1compat image for a1.* family Amazon EC2 instances; upgrade to a more recent AWS Graviton instance type (#2836)

Major Features

  • Add ability to provision volumes using AZ-ID (#2613)
  • Add dry-run EC2 API call to ebs-csi-driver controller readiness/liveness probes (#2590)
  • Retrieve maximum IOPS limit dynamically via CreateVolume dry-run to support increased IOPS limits for GP3 and other volume types (#2682)
  • Add support for creating instant, point-in-time copies of EBS volumes within the same Availability Zone (#2716)
  • Add ALPHA metadata-labeler sidecar and metadata source (#2591)
  • Add node-local volume support, enabling a single cluster-wide PV/PVC to mount node-specific EBS volumes with ReadWriteMany access (#2726)
  • Add support for custom build-time plugins to extend or modify EBS CSI Driver behavior (#2786)
  • Automatically assign EBS card index for instances with multiple EBS cards (#2835)
  • Detect, abort, and retry stuck attachments (#2833)
  • Adding SnapshotLock Capabilities (#2797)
  • Add encryption support ext4 format option to StorageClass params (#2820)
  • Add support for Windows Server 2025 (#2831)
  • When --k8s-tag-cluster-id is set, tag volumes and snapshots with ebs.csi.aws.com/cluster-name to support cluster-scoped IAM policies (#2899)
  • Call DescribeInstanceTypes at runtime for EBS multi-card instances (#2916)
  • Add plugin method to override health check (#2952)

Notable Bug Fixes

  • Fix cluster tags incorrectly overriding user-specified tags from StorageClass parameters (#2655)
  • Update several incorrect EBS volume limits via generating table from DescribeInstanceTypes API (#2660)
  • Fix throughput handling when creating volumes (#2733)
  • Fix off-by-one error in volume limits for default nitro instances which would get PVCs stuck attempting to attach to a full instance (#2749)
  • Improve error handling of taint removal; Fix last chance taint removal not functioning (#2699)
  • Fix error handling when re-fetching the node object during agent-not-ready taint removal attempts (#2847)
  • Handle duplicate CreateVolume RPCs for in-use volumes (#2908)
  • Prevent deletion of reserved tag keys during volume modification (#2919)
  • Fix init order bug that allowed modifying tags reserved for use by the EBS CSI Driver (#2932)
  • Fix possible nil pointer deref on clones path (#2931)
  • Add mutex to MetricRecorder to prevent crashes caused by concurrent access (#2933)
  • Fixing warn-on-invalid-tag validation bug (#2954)

CVE Fixes

CVE-2026-33814, CVE-2026-33186, CVE-2025-61726

Cherry-picked commits

Commit Subject Author
fdc2f42a2 UPSTREAM: <carry>: Add OpenShift files Daniel Fajmon
7e6a7798f UPSTREAM: <carry>: Updating ose-aws-ebs-csi-driver-container image to be consistent with ART for 5.0 AOS Automation Release Team
0a05883f1 UPSTREAM: <carry>: Updating ose-aws-ebs-csi-driver-container image to be consistent with ART for 5.0 Radomir Vagner

Upstream changelogs

The project uses a shared CHANGELOG.md covering all releases (no per-release changelog pages):

Full changelog

kubernetes-sigs/aws-ebs-csi-driver@v1.48.0...v1.62.0

Last rebase

#294

@openshift/storage


Co-authored by Claude

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository.

@openshift-ci

openshift-ci Bot commented Jul 3, 2026

Copy link
Copy Markdown

Skipping CI for Draft Pull Request.
If you want CI signal for your change, please convert it to an actual PR.
You can still manually trigger a test run with /test all

@openshift-ci

openshift-ci Bot commented Jul 3, 2026

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: dfajmon

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@openshift-ci openshift-ci Bot added the approved Indicates a PR has been approved by an approver from all required OWNERS files. label Jul 3, 2026
@dfajmon
dfajmon marked this pull request as ready for review July 3, 2026 12:23
@openshift-ci openshift-ci Bot removed the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Jul 3, 2026
@dfajmon

dfajmon commented Jul 3, 2026

Copy link
Copy Markdown
Author

/hold
before merge we need to update policies in Hypershift and CSO

@openshift-ci openshift-ci Bot added the do-not-merge/hold Indicates that a PR should not merge because someone has issued a /hold command. label Jul 3, 2026
@openshift-ci
openshift-ci Bot requested review from RomanBednar and jsafrane July 3, 2026 12:28
@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown

Walkthrough

This large release bundles multiple EBS CSI driver features: multi-card volume attachment support, node-local volumes, an alpha metadata-labeler sidecar, EBS snapshot locking, volume cloning, and a build-time plugin system. It updates Helm charts, Kubernetes manifests, dependencies, CI/build tooling, documentation, and adds extensive e2e/unit test coverage while removing legacy A1-compatibility support.

Changes

Core Driver

Layer / File(s) Summary
Driver-name refactor
pkg/util/util.go, pkg/util/ec2_interface.go, pkg/util/sagemaker_interface.go, pkg/driver/driver.go, pkg/driver/identity.go, pkg/driver/validation.go, cmd/main.go
Introduces dynamic driver-name resolution via util.GetDriverName/SetDriverName, moves EC2/SageMaker interfaces to pkg/util, and reorders main.go initialization.
Multi-card attachment
pkg/cloud/devicemanager/*, pkg/cloud/limits/*, hack/generate-volume-limits-table/*, hack/detect-potentially-invalid-limits/*
Adds card-index-aware device allocation, generated volume/card limit tables, and integrates card counts into attach/detach flows.
Cloud service enhancements
pkg/cloud/cloud.go, pkg/cloud/interface.go, pkg/cloud/mock_cloud.go, pkg/cloud/mock_ec2.go
Reworks IOPS-limit discovery, dry-run health checks, batched describe caching, snapshot locking, and clone creation; extends the Cloud interface and mocks.
Controller RPCs
pkg/driver/controller.go, pkg/driver/controller_modify_volume.go
Adds volume cloning, node-local volume routing, snapshot lock parameters, and IOPS-per-GB modify support with reserved-tag protection.
Node service
pkg/driver/node.go, pkg/driver/node_test.go
Adds node-local volume mounting, ext4 encryption support, refactors attach-limit calculation, and improves the not-ready taint watcher.
Metadata labeler
pkg/cloud/metadata/labels.go, pkg/cloud/metadata/k8s.go, pkg/cloud/metadata/metadata.go
Adds a leader-elected controller that patches node labels with ENI/volume counts for the new metadata-labeler source.
Plugin system
pkg/plugin/*
Introduces a build-time plugin interface allowing customization of AWS clients, driver name, and topology.
Metrics/batcher/mounter fixes
pkg/metrics/*, pkg/batcher/batcher.go, pkg/expiringcache/*, pkg/mounter/*, pkg/testutil/*
Exports MetricRecorder with rate limiting, fixes blocking batcher sends, adds cache removal, and adds Windows mounter rescans.

Estimated code review effort: 5 (Critical) | ~180 minutes

Helm Chart & Deploy Manifests

Layer / File(s) Summary
Values/schema & versioning
charts/aws-ebs-csi-driver/values*.{yaml,json}, Chart.yaml
Adds debugLogs, node-local volumes, metadata labeler, PDB and dnsConfig options; bumps chart to 2.61.1.
Templates
charts/aws-ebs-csi-driver/templates/*
Updates health probes, metrics ports, RBAC, and adds the metadata-labeler sidecar; removes A1-compatibility daemonset.
Base K8s manifests
deploy/kubernetes/base/*
Mirrors chart image/probe/RBAC changes in static manifests.

Documentation & CI

Layer / File(s) Summary
Docs
docs/*, README.md, examples/*
Adds IAM policy V2, node-local-volumes/plugins/outposts docs, and volume-clone examples.
CI/Build tooling
.github/*, Makefile, Dockerfile, hack/*, go.mod
Removes Trivy/dependabot workflows, adds e2e parameter-set runner, bumps dependencies/tool versions.

Tests

Layer / File(s) Summary
E2E and helm-template tests
tests/e2e/*, tests/helm-template/*, tests/sanity/*
Adds new feature test suites and a standalone Helm-template rendering test module.

Sequence Diagram(s)

sequenceDiagram
  participant Kubelet
  participant CSIDriver as EBS CSI Driver
  participant DeviceManager
  participant AWSCloud as pkg/cloud
  participant EC2

  Kubelet->>CSIDriver: ControllerPublishVolume(nodeID, volumeID)
  CSIDriver->>AWSCloud: AttachDisk(volumeID, nodeID)
  AWSCloud->>DeviceManager: NewDevice(instance, volumeID, numCards)
  DeviceManager-->>AWSCloud: Device{Path, CardIndex}
  AWSCloud->>EC2: AttachVolume(EbsCardIndex)
  EC2-->>AWSCloud: VolumeAttachment
  AWSCloud->>AWSCloud: WaitForAttachmentState(expectedCardIndex)
  AWSCloud-->>CSIDriver: attach success
  CSIDriver-->>Kubelet: PublishContext
Loading
sequenceDiagram
  participant LeaderElector
  participant LabelController as Metadata Labeler
  participant K8sAPI
  participant EC2

  LeaderElector->>LabelController: elected leader
  LabelController->>K8sAPI: list Nodes and PersistentVolumes
  LabelController->>EC2: GetInstancesPatching(nodeIDs)
  EC2-->>LabelController: Instance details (ENIs, block devices)
  LabelController->>LabelController: compute non-CSI volume/ENI counts
  LabelController->>K8sAPI: patch Node labels
Loading

Estimated code review effort: 5 (Critical) | ~240 minutes

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (6)
pkg/metrics/metrics.go (1)

81-116: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

InitializeNVME/InitializeAsyncEC2Metrics ignore their own receiver m and mutate the global singleton r instead.

Both methods are declared on (m *MetricRecorder) but their bodies exclusively reference the package-level r (registerNVMECollector(r, ...), r.asyncEC2Metrics = ..., r.registry.MustRegister(...)). Every other refactored method in this file (registerHistogramVec, registerCounterVec, initializeMetricWithOperations, IncreaseCount, ObserveHistogram) correctly uses m. Now that MetricRecorder is exported and directly instantiable (as demonstrated by the new TestMetricRecorderConcurrentAccess in metrics_test.go, which builds a MetricRecorder{} without going through InitializeRecorder), calling these two methods on any instance other than the singleton would silently corrupt/mutate global state instead of the receiver's own state.

Based on learnings, "Follow existing option patterns and exported API conventions instead of inventing new abstractions" and "Keep public APIs backward compatible" — the exported type should behave consistently regardless of which instance it's called on.

🐛 Proposed fix to use the receiver consistently
 func (m *MetricRecorder) InitializeNVME(csiMountPointPath, instanceID string) {
-	registerNVMECollector(r, csiMountPointPath, instanceID)
+	registerNVMECollector(m, csiMountPointPath, instanceID)
 }

 func (m *MetricRecorder) InitializeAsyncEC2Metrics(minimumEmissionThreshold time.Duration) {
 	variableLabels := []string{"volume_id", "instance_id", "attachment_state"}
 	cacheCleanupInterval := 15 * time.Minute
 
-	r.asyncEC2Metrics = &AsyncEC2Collector{
+	m.asyncEC2Metrics = &AsyncEC2Collector{
 		...
 	}
-	r.registry.MustRegister(r.asyncEC2Metrics)
+	m.registry.MustRegister(m.asyncEC2Metrics)
 
 	go func() {
 		for {
-			<-r.asyncEC2Metrics.ticker.C
-			r.asyncEC2Metrics.cleanupCache(cacheCleanupInterval)
+			<-m.asyncEC2Metrics.ticker.C
+			m.asyncEC2Metrics.cleanupCache(cacheCleanupInterval)
 		}
 	}()
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/metrics/metrics.go` around lines 81 - 116, InitializeNVME and
InitializeAsyncEC2Metrics are using the package-level singleton r instead of
their receiver m, which breaks behavior for any non-singleton MetricRecorder
instance. Update both methods to consistently operate on m by wiring
registerNVMECollector and the AsyncEC2Collector setup through the receiver’s
fields (including registry, asyncEC2Metrics, and ticker handling) rather than
mutating global state. Keep the existing API surface and patterns used by
registerHistogramVec, registerCounterVec, initializeMetricWithOperations,
IncreaseCount, and ObserveHistogram so exported MetricRecorder instances behave
identically whether created directly or via InitializeRecorder.

Source: Learnings

pkg/cloud/handlers.go (1)

74-95: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Keep non-throttle AWS API errors visible by default Non-throttle API failures are now logged at V(3), so they won’t show up with the shipped --v=2 defaults and become much harder to diagnose.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/cloud/handlers.go` around lines 74 - 95, Non-throttle AWS API failures
are being hidden behind a higher verbosity level in LogServerErrorsMiddleware,
so make them visible at the default logging level. Update the error logging in
the middleware returned by LogServerErrorsMiddleware so the non-throttle branch
logs with the standard error logger instead of klog.V(3), while keeping throttle
errors on the quieter verbose path and preserving the existing smithy.APIError /
retry.DefaultThrottleErrorCodes handling.
pkg/driver/node.go (1)

814-847: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reject --reserved-volume-attachments < -1. Validate() only treats -1 as the sentinel, so any other negative value is subtracted directly in getVolumesLimit() and can inflate MaxVolumesPerNode above the instance’s real attach limit. Add a lower-bound check in the flag validation path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/driver/node.go` around lines 814 - 847, The negative reserved-volume
setting is not being rejected, and getVolumesLimit can miscompute
MaxVolumesPerNode when ReservedVolumeAttachments is below the sentinel value.
Update the validation path in NodeService.Validate() to enforce a lower bound so
only -1 is allowed as the special auto-detect value, and reject any value less
than -1 before getVolumesLimit() uses d.options.ReservedVolumeAttachments.
pkg/driver/controller_modify_volume.go (1)

92-127: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Merge the new modify options into coalesced requests mergeModifyVolumeRequest still drops IOPSPerGB and AllowIopsIncreaseOnResize, so a later coalesced modify request can execute without the requested IOPS-per-GB behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/driver/controller_modify_volume.go` around lines 92 - 127,
mergeModifyVolumeRequest currently merges only size, IOPS, throughput, volume
type, and tags, but it still drops IOPSPerGB and AllowIopsIncreaseOnResize from
modifyDiskOptions. Update mergeModifyVolumeRequest in
controller_modify_volume.go to carry these fields through the coalescing logic,
including conflict checks consistent with the existing option merges, so later
requests preserve the requested IOPS-per-GB behavior. Use the
modifyVolumeRequest and modifyDiskOptions fields as the place to locate and wire
the missing options.
hack/e2e/eksctl/eksctl.sh (1)

22-52: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Pass LINUX_AMI and WINDOWS_AMI into the initial gomplate render. The template already uses these env vars for the linux and custom-windows nodegroups, so omitting them here makes the initial eksctl cluster ignore custom AMIs; only the outpost re-render gets them.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hack/e2e/eksctl/eksctl.sh` around lines 22 - 52, The initial gomplate render
in eksctl_create_cluster is missing the LINUX_AMI and WINDOWS_AMI environment
values, so the main cluster template cannot use custom AMIs for the linux and
custom-windows nodegroups. Update the gomplate invocation in
eksctl_create_cluster to pass through both LINUX_AMI and WINDOWS_AMI alongside
the existing CLUSTER_NAME, REGION, K8S_VERSION, ZONES, INSTANCE_TYPE,
AMI_FAMILY, and WINDOWS variables. Keep the outpost-specific render separate,
but ensure the initial render has the same AMI inputs the template expects.
cmd/main.go (1)

121-131: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Log the actual errors in refactored failure paths.

These paths log stale variables (err/exporterErr) instead of the local errors, which can hide the root cause during tracing or pre-stop failures.

Proposed logging fix
 		exporter, exporterErr := driver.InitOtelTracing()
 		if exporterErr != nil {
-			klog.ErrorS(err, "failed to initialize otel tracing")
+			klog.ErrorS(exporterErr, "failed to initialize otel tracing")
 			klog.FlushAndExit(klog.ExitFlushTimeout, 1)
 		}
@@
 			defer cancel()
 			if shutdownErr := exporter.Shutdown(ctx); shutdownErr != nil {
-				klog.ErrorS(exporterErr, "could not shutdown otel exporter")
+				klog.ErrorS(shutdownErr, "could not shutdown otel exporter")
 			}
 		}()
@@
 		clientset, clientErr := metadata.DefaultKubernetesAPIClient(options.Kubeconfig)()
 		if clientErr != nil {
-			klog.ErrorS(err, "unable to communicate with k8s API")
+			klog.ErrorS(clientErr, "unable to communicate with k8s API")

Also applies to: 213-215

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/main.go` around lines 121 - 131, The OpenTelemetry tracing initialization
and shutdown paths in main use stale error variables instead of the local
failures, so update the logging in InitOtelTracing handling and the
exporter.Shutdown defer to pass the actual local error values. In the
klog.ErrorS calls, replace the captured outer variable with the corresponding
exporterErr or shutdownErr from the current scope so the real root cause is
logged for both failure paths, including the later pre-stop shutdown site.
🟠 Major comments (29)
charts/aws-ebs-csi-driver/templates/poddisruptionbudget-controller.yaml-17-27 (1)

17-27: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Rendering both maxUnavailable and minAvailable unconditionally breaks the common single-value case.

When a user sets only maxUnavailable (or only minAvailable), this block still emits both keys — the unset one renders as an empty value, and PodDisruptionBudget rejects specs with both fields set anyway. This will produce an invalid manifest in the most common override scenario.

🐛 Proposed fix
   {{- if or .Values.controller.podDisruptionBudget.maxUnavailable .Values.controller.podDisruptionBudget.minAvailable }}
-  maxUnavailable: {{ .Values.controller.podDisruptionBudget.maxUnavailable }}
-  minAvailable: {{ .Values.controller.podDisruptionBudget.minAvailable }}
+  {{- if .Values.controller.podDisruptionBudget.maxUnavailable }}
+  maxUnavailable: {{ .Values.controller.podDisruptionBudget.maxUnavailable }}
+  {{- end }}
+  {{- if .Values.controller.podDisruptionBudget.minAvailable }}
+  minAvailable: {{ .Values.controller.podDisruptionBudget.minAvailable }}
+  {{- end }}
   {{- else }}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@charts/aws-ebs-csi-driver/templates/poddisruptionbudget-controller.yaml`
around lines 17 - 27, The PodDisruptionBudget template is rendering both fields
at once in the controller PDB block, which breaks the single-value override
case. Update the conditional logic in the poddisruptionbudget-controller
template so it emits only one of maxUnavailable or minAvailable based on which
value is set in .Values.controller.podDisruptionBudget, and keep the existing
default fallback behavior when neither is provided.
pkg/util/util.go-61-70 (1)

61-70: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

SetDriverName loses its one-time guard on every call

sync.OnceValue is created inside the function, so each invocation gets a fresh instance and always writes driverName. The fatal path can never fire, which means a later call with a different name silently overwrites the package state. Use a package-level once/mutex guard instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/util/util.go` around lines 61 - 70, SetDriverName is recreating
sync.OnceValue on every call, so the one-time guard never persists and
driverName can be overwritten later. Move the guard to package scope in
pkg/util/util.go, using a persistent sync.Once or mutex-protected check around
SetDriverName so only the first call sets driverName and later calls with a
different name hit the fatal klog.ErrorS/FlushAndExit path.
pkg/cloud/cloud.go-903-932 (1)

903-932: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Keep AvailabilityZone and AvailabilityZoneId mutually exclusive. pkg/driver/controller.go can populate both on DiskOptions, so this helper can send both to CreateVolume and hit InvalidParameterCombination. Use else if here or normalize upstream.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/cloud/cloud.go` around lines 903 - 932, The createVolumeHelper method
currently sets both AvailabilityZone and AvailabilityZoneId on the same
CreateVolumeInput when both zone and zoneID are present, which can trigger
InvalidParameterCombination. Update the logic in createVolumeHelper to make
these fields mutually exclusive by preferring one and using else if (or
normalize DiskOptions before calling it), so only one of aws.String(zone) or
aws.String(zoneID) is assigned.
pkg/cloud/cloud.go-2188-2224 (1)

2188-2224: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Decouple plugin health from the dry-run cache pkg/cloud/cloud.go:2194-2224
plugin.HealthCheck() only runs when c.attemptDryRun.Load() is true. After a successful dry-run or skipDriverHealthCheck, later Probe calls return nil until the 3h reset, so a plugin failure can stay hidden for hours. Run the plugin check on every probe or cache it separately from the EC2 dry-run.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/cloud/cloud.go` around lines 2188 - 2224, The plugin health check in
cloud.DryRun is currently gated by c.attemptDryRun, so plugin failures can be
hidden after the dry-run is cached. Update DryRun to always invoke
plugin.GetPlugin().HealthCheck() on every probe, and keep its result separate
from the EC2 dry-run cache behavior. Preserve the existing skipDriverHealthCheck
logic and c.attemptDryRun.Store(false) only for the EC2 dry-run path, not for
the plugin health path.
tests/e2e/testsuites/dynamically_provisioned_copy_volume_tester.go-29-34 (1)

29-34: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Call ValidateFunc after the cloned pod succeeds

ValidateFunc is set by callers in tests/e2e/requires_aws_api.go, but Run() never invokes it. That silently skips the post-clone AWS/API assertions; mirror DynamicallyProvisionedCmdVolumeTest by calling it after tcpod.WaitForSuccess().

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/e2e/testsuites/dynamically_provisioned_copy_volume_tester.go` around
lines 29 - 34, The cloned-volume test currently defines ValidateFunc on
DynamicallyProvisionedCopyVolumeTest but never calls it, so the post-clone
assertions are skipped. Update Run() in DynamicallyProvisionedCopyVolumeTest to
invoke ValidateFunc after tcpod.WaitForSuccess(), mirroring the behavior in
DynamicallyProvisionedCmdVolumeTest and ensuring callers like
requires_aws_api.go can run their AWS/API validation.
tests/e2e/parameter_tests.go-270-271 (1)

270-271: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Avoid pulling a public image in Ginkgo e2e.

public.ecr.aws/amazonlinux/amazonlinux:2023 requires public registry access and can fail in disconnected/restricted CI. Use a repository-managed or mirrored test image that provides cp --reflink. As per coding guidelines, flag Ginkgo e2e tests that pull images from public registries.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/e2e/parameter_tests.go` around lines 270 - 271, The Ginkgo e2e test
setup in the reflinkProbe container is pulling a public Amazon Linux image,
which can break in restricted CI. Update the test’s image reference to use a
repository-managed or mirrored image that still provides cp --reflink, and keep
the change within the reflinkProbe / parameter_tests setup so the e2e test no
longer depends on public registry access.

Source: Coding guidelines

tests/e2e/parameter_tests.go-115-117 (1)

115-117: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Use bounded contexts for direct AWS and Kubernetes calls.

These e2e calls use context.Background(), so a stuck AWS/Kubernetes request can hang the spec until the outer job times out. Use a helper with context.WithTimeout for these direct API calls. As per path instructions, Go code should use context.Context for cancellation and timeouts. Based on learnings, inspect cancellation and timeout boundaries carefully.

Also applies to: 125-130, 155-157, 166-171, 196-198, 208-213, 289-293, 298-300, 313-314, 331-332

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/e2e/parameter_tests.go` around lines 115 - 117, The direct
AWS/Kubernetes calls in the e2e tests are using context.Background(), which can
leave requests hanging indefinitely. Update the affected test setup and helper
calls in parameter_tests.go to use a bounded context created with
context.WithTimeout, and make sure the context is passed through the existing
config.LoadDefaultConfig, EC2/client creation, and any other direct API calls in
the listed blocks. Prefer a small helper for consistent timeout handling and
cancellation, and reference the affected setup patterns around
config.LoadDefaultConfig, ec2.NewFromConfig, and the Kubernetes/AWS client calls
so they can be updated together.

Sources: Path instructions, Learnings

tests/e2e/parameter_tests.go-161-173 (1)

161-173: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Scope EC2 tag assertions to the volume created by this test.

These queries can pass if any pre-existing cluster volume has the expected cluster/extra tag. Filter by the test-created volume handle, or add a unique per-test tag/namespace filter when available, so regressions on the newly provisioned volume are caught.

Also applies to: 202-215

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/e2e/parameter_tests.go` around lines 161 - 173, The EC2 volume tag
checks in the DynamicallyProvisionedCmdVolumeTest are too broad because
DescribeVolumes only filters by the cluster tag, so pre-existing volumes can
make the assertion pass. Update the ValidateFunc in the volume-tag tests
(including the later matching case) to scope the query to the volume created by
this test, using the created volume handle from the test setup or a unique
per-test tag/namespace filter, and keep the assertions on the specific volume
returned by ec2Client.DescribeVolumes.
pkg/driver/node.go-962-1003 (1)

962-1003: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Use a separate context for the last-chance taint removal
ctx is canceled in the watch-error handler on Unauthorized/Forbidden, so the final Get here can fail with context.Canceled before the fallback runs. Use an independent timeout context for the last-chance read/remove path; if attemptTaintRemoval should survive watch cancellation too, it needs the same treatment.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/driver/node.go` around lines 962 - 1003, Use a separate context for the
final taint-removal fallback in the taint-watcher flow: the watch error handler
in node.go cancels ctx on Unauthorized/Forbidden, so the last-chance
clientset.CoreV1().Nodes().Get and any follow-up removal logic can fail early
with context.Canceled. Create an independent timeout context for the “last
chance” read/remove path, and pass that same independent context through
attemptTaintRemoval if it also needs to run after watch cancellation.
tests/e2e/metadata-labeler.go-97-108 (1)

97-108: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Error check happens after using the (possibly nil) result.

nodes.Items is iterated at Line 100 before err from the List call is checked at Line 104. If the API call fails, nodes will be nil, and dereferencing nodes.Items panics instead of failing cleanly via Expect.

🐛 Proposed fix
 			nodes, err := cs.CoreV1().Nodes().List(context.TODO(), metav1.ListOptions{})
+			Expect(err).NotTo(HaveOccurred(), "Failed to list nodes")
 
 			clusterInstances := []string{}
 			for _, clusterNode := range nodes.Items {
 				clusterInstances = append(clusterInstances, parseProviderID(clusterNode.Spec.ProviderID))
 			}
 
-			Expect(err).NotTo(HaveOccurred(), "Failed to list nodes")
 			resp, err := ec2Client.DescribeInstances(context.TODO(), &ec2.DescribeInstancesInput{
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/e2e/metadata-labeler.go` around lines 97 - 108, The `nodes.Items` loop
in `metadata-labeler.go` uses the result of `cs.CoreV1().Nodes().List(...)`
before checking `err`, which can panic if the list call fails. Move the
`Expect(err).NotTo(HaveOccurred(), ...)` check to immediately after the `List`
call and before iterating `nodes.Items`, so the `clusterInstances` build only
runs when `nodes` is valid.
tests/e2e/metadata-labeler.go-427-440 (1)

427-440: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Error-handling logic in createStorageClass silently swallows real Create errors.

The check if err != nil && errors.IsNotFound(err) only asserts when the error is specifically "NotFound" — which a Create call never returns. Any actual failure (AlreadyExists, permission denied, etc.) is silently ignored, so the test proceeds with a broken/incomplete storage class instead of failing with a diagnosable error.

🐛 Proposed fix
 	_, err := cs.StorageV1().StorageClasses().Create(context.Background(), storageClass, metav1.CreateOptions{})
-	if err != nil && errors.IsNotFound(err) {
+	if err != nil && !errors.IsAlreadyExists(err) {
 		Expect(err).NotTo(HaveOccurred(), "Failed to create storage class")
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/e2e/metadata-labeler.go` around lines 427 - 440, The error handling in
createStorageClass is checking the wrong condition, so real Create failures are
ignored and the test can continue with an invalid storage class. Update
createStorageClass to assert or fail whenever
cs.StorageV1().StorageClasses().Create returns any non-nil err, and remove the
errors.IsNotFound branch since Create should not be treated as a NotFound case.
Keep the fix scoped to createStorageClass and its Create call so failures like
AlreadyExists or permission errors surface immediately.
Dockerfile-28-45 (1)

28-45: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Set a non-root runtime user or document the required exception.

These final stages do not declare USER, so they run as root by default. If root is truly required for CSI node operations, add an explicit justification/suppression; otherwise set a non-root user per target image. As per path instructions, Dockerfiles must use “USER non-root; never run as root.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Dockerfile` around lines 28 - 45, The final runtime stages in the Dockerfile
currently omit a USER declaration, so they default to root. Update the
linux-al2023, windows-ltsc2019, windows-ltsc2022, and windows-ltsc2025 stages to
run as a non-root user using the appropriate image-supported account, or add an
explicit documented exception if root is unavoidable. Use the final stage blocks
with the ENTRYPOINT definitions as the place to apply the USER change.

Sources: Path instructions, Linters/SAST tools

pkg/driver/controller.go-668-690 (1)

668-690: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Honor the node-local feature gate during capability validation.

ValidateVolumeCapabilities confirms local-ebs:// volumes even when EnableNodeLocalVolumes is false, while publish rejects them. Return InvalidArgument consistently when the feature is disabled.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/driver/controller.go` around lines 668 - 690, Update
ValidateVolumeCapabilities in controller.go so node-local volumes are gated the
same way as publish paths: when EnableNodeLocalVolumes is false, detect
local-ebs:// volume IDs before the node-local capability check and return
InvalidArgument instead of confirming them. Keep the existing GetDiskByID path
for non-node-local volumes, and ensure the isNodeLocalVolume /
isValidCapabilityForNodeLocal handling only runs when the feature gate is
enabled.
pkg/driver/controller.go-892-911 (1)

892-911: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Validate snapshot lock values before creating the snapshot.

Invalid mode, non-positive duration, or non-positive cool-off currently reaches AWS after the snapshot is created, then relies on cleanup. Reject invalid lock parameters as InvalidArgument before CreateSnapshot.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/driver/controller.go` around lines 892 - 911, Validate snapshot lock
inputs in the snapshot creation path before calling CreateSnapshot, using the
existing LockMode, LockDuration, LockExpirationDate, and LockCoolOffPeriod
handling in pkg/driver/controller.go. Reject invalid lock mode values and any
non-positive LockDuration or LockCoolOffPeriod with
status.Errorf(codes.InvalidArgument, ...) so bad parameters never reach AWS.
Keep the parsing logic near the current snapshot lock setup so the
CreateSnapshot call only runs after all lock fields have been validated.
pkg/driver/controller.go-491-499 (1)

491-499: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Validate publish requests before the node-local fast path.

The node-local branch bypasses validateControllerPublishVolumeRequest, so malformed requests like an empty node_id can reach cloud lookup and return the wrong CSI error.

Move validation before branching
 	volumeID := req.GetVolumeId()
 	nodeID := req.GetNodeId()
 
+	if err := validateControllerPublishVolumeRequest(req); err != nil {
+		return nil, err
+	}
+
 	if isNodeLocalVolume(volumeID) {
 		if !d.options.EnableNodeLocalVolumes {
 			return nil, status.Error(codes.InvalidArgument, "node-local volumes are not enabled")
 		}
 		return d.controllerPublishVolumeNodeLocal(ctx, req)
 	}
-
-	if err := validateControllerPublishVolumeRequest(req); err != nil {
-		return nil, err
-	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/driver/controller.go` around lines 491 - 499, The controller publish path
currently checks isNodeLocalVolume before validateControllerPublishVolumeRequest
in controllerPublishVolume, which lets malformed requests skip validation and
hit the node-local fast path. Move validateControllerPublishVolumeRequest(req)
to run before the node-local branch so all publish requests are rejected
consistently before calling controllerPublishVolumeNodeLocal or any cloud lookup
logic.
pkg/driver/controller.go-420-430 (1)

420-430: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not map unknown create failures to Aborted.

Aborted tells the CO the operation is in-progress/conflicted. Unknown AWS/cloud failures should remain Internal, and capacity/limit failures should map to ResourceExhausted when available.

Safer error mapping
 		case errors.Is(err, cloud.ErrSourceNotFound):
 			errCode = codes.NotFound
+		case errors.Is(err, cloud.ErrLimitExceeded):
+			errCode = codes.ResourceExhausted
 		default:
-			errCode = codes.Aborted
+			errCode = codes.Internal
 		}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/driver/controller.go` around lines 420 - 430, The create-failure gRPC
mapping in controller logic is too broad because unknown cloud errors are
currently being converted to Aborted. Update the error-to-code switch in the
controller’s create path so only known conflicts and validation/not-found cases
map to their specific codes, while unknown AWS/cloud failures default to
Internal instead of Aborted. Also add a case for capacity/limit-related cloud
errors to return ResourceExhausted when those errors can be identified.
pkg/batcher/batcher.go-168-168 (1)

168-168: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Avoid blocking forever on abandoned result channels.

A direct send can leak the execute goroutine if a caller times out or stops receiving. Use a size-1 buffered result channel at creation time, or keep this delivery non-blocking.

Possible localized fix
-			ch <- BatchResult[ResultType]{Result: r, Err: err}
+			select {
+			case ch <- BatchResult[ResultType]{Result: r, Err: err}:
+			default:
+				klog.V(7).InfoS("execute: no receiver for batch result", "task", task)
+			}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/batcher/batcher.go` at line 168, The direct send in `execute` can block
forever if the caller abandons the result channel, so update the `BatchResult`
delivery path to avoid leaking the goroutine. Fix this by making the result
channel created for `execute`/`Batch` size-1 buffered, or by changing the send
on `ch` to a non-blocking delivery pattern while preserving the existing
`BatchResult[ResultType]` contract.
pkg/driver/constants.go-66-67 (1)

66-67: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Keep KmsKeyIdKey as a deprecated alias. Renaming this exported constant is a compile-time break for downstream imports; preserve the old spelling unless this PR is intentionally dropping that API.

Compatibility alias
 	KmsKeyIDKey = "kmskeyid"
+	// KmsKeyIdKey is deprecated; use KmsKeyIDKey.
+	KmsKeyIdKey = KmsKeyIDKey
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/driver/constants.go` around lines 66 - 67, The exported constant rename
in constants.go is a breaking API change; keep KmsKeyIDKey as the canonical name
and preserve KmsKeyIdKey as a deprecated alias for downstream compatibility.
Update the constants declaration to expose both symbols in
pkg/driver/constants.go so existing imports continue to compile, and mark the
older spelling as deprecated if needed.

Source: Learnings

hack/ebs-scale-test/helpers/scale-test/volume-lifecycle-churn-test/volume-lifecycle-churn.sh-137-149 (1)

137-149: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Handle failed Jobs in wait_for_wave_jobs_complete
Exit non-zero as soon as any Job in the wave reports failure; with backoffLimit: 0, a terminal Job failure will never reach succeeded, so this loop can wait forever and mask the churn bug.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@hack/ebs-scale-test/helpers/scale-test/volume-lifecycle-churn-test/volume-lifecycle-churn.sh`
around lines 137 - 149, `wait_for_wave_jobs_complete` only waits for `succeeded`
Jobs, so a terminal Job failure can loop forever and hide churn errors. Update
the `kubectl get jobs`/`jq` logic in `wait_for_wave_jobs_complete` to also
detect any Job in the current `churn-wave` with a failure state, and exit
non-zero immediately when one is found; keep the existing success path for
counting completed Jobs against `REPLICAS`.
pkg/cloud/metadata/metadata.go-108-125 (1)

108-125: 🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

Shorten the metadata-labeler refresh timeout.
UpdateMetadata() runs KubernetesAPIInstanceInfo(..., true) on every periodic NodeGetInfo refresh, and that path can block for up to 5s with exponential backoff when the EC2/volume labels aren’t ready yet. That can make allocatable-count updates stall repeatedly under normal labeler lag. Split the refresh timeout/backoff from the initial metadata fetch, or make it separately configurable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/cloud/metadata/metadata.go` around lines 108 - 125, The metadata-labeler
refresh path in UpdateMetadata() is using the same long timeout/backoff as the
initial KubernetesAPIInstanceInfo fetch, which can stall periodic NodeGetInfo
updates. Adjust the KubernetesAPIInstanceInfo call path used by
Metadata.UpdateMetadata (especially the metadataLabeler=true branch) to use a
shorter, separately configurable refresh timeout/backoff instead of the 5s
initial-fetch behavior. Keep the change localized to UpdateMetadata and
KubernetesAPIInstanceInfo so the refresh path returns quickly when EC2/volume
labels are not yet ready.
tests/e2e/multi_card.go-45-49 (1)

45-49: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Move AWS config loading out of the Describe body
Fail() here runs during Ginkgo tree construction, so an AWS config error can panic before any spec executes. Initialize the EC2 client in BeforeEach or lazily inside It instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/e2e/multi_card.go` around lines 45 - 49, AWS config loading is
happening during Ginkgo tree construction in the Describe scope, so a failure
can abort before any spec runs. Move the config initialization for
config.LoadDefaultConfig and ec2.NewFromConfig out of the Describe body into a
BeforeEach setup or into the It body so Fail() only runs during test execution.
Keep the change localized around the EC2 client setup in multi_card.go.
tests/e2e/testsuites/testsuites.go-411-411 (1)

411-411: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Fix the topology key concatenation

"topology"+util.GetDriverName()+"/zone" is missing the dot, so it builds topologyebs.csi.aws.com/zone instead of topology.ebs.csi.aws.com/zone. That prevents keyFound from ever being set here and trips the Fail(...) path whenever AllowedTopologies is used.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/e2e/testsuites/testsuites.go` at line 411, The topology key check in
the testsuites logic is concatenating the driver name without the required dot,
so update the condition in the `keyFound`/`Fail(...)` path to match the actual
topology key format produced by `AllowedTopologies`. Fix the string built from
`util.GetDriverName()` so it compares against the correct
`topology.<driver>/zone` key, and keep the check in the same `testsuites.go`
block so `keyFound` can be set properly.
cmd/main.go-206-209 (1)

206-209: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Fail metadata-labeler mode when its Kubernetes client or loop fails.

MetadataLabelerMode passes k8sClient even if client creation failed, then exits with status 0 when leader election/label patching returns an error. Make this path fail fast and non-zero so the container status reflects the failure.

Proposed failure handling
 	k8sClient, err = cfg.K8sAPIClient()
 	if err != nil {
+		if cmd == string(driver.MetadataLabelerMode) {
+			klog.ErrorS(err, "failed to setup k8s client")
+			klog.FlushAndExit(klog.ExitFlushTimeout, 1)
+		}
 		klog.V(2).InfoS("Failed to setup k8s client", "err", err)
 	}
@@
 	case string(driver.MetadataLabelerMode):
 		err := metadata.ContinuousUpdateLabelsLeaderElection(k8sClient, cloud, metadata.ControllerMetadataLabelerInterval)
 		if err != nil {
 			klog.ErrorS(err, "failed to patch volume/ENI count on node labels")
-			klog.FlushAndExit(klog.ExitFlushTimeout, 0)
+			klog.FlushAndExit(klog.ExitFlushTimeout, 1)
 		}
+		klog.FlushAndExit(klog.ExitFlushTimeout, 0)

Also applies to: 226-230

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/main.go` around lines 206 - 209, MetadataLabelerMode currently logs K8s
client setup failures and continues, then can still exit successfully even when
leader election or label patching fails. Update the MetadataLabelerMode flow in
main so that cfg.K8sAPIClient() failures and the subsequent labeler loop/error
path both abort the mode and return a non-zero exit status, using the existing
k8sClient/MetadataLabelerMode path to fail fast instead of proceeding with a nil
or unusable client.
pkg/cloud/metadata/k8s.go-123-145 (1)

123-145: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Reject negative label-derived counts.

strconv.Atoi accepts values like -1, so node/SageMaker labels can produce impossible ENI or block-device counts. Validate these count labels as non-negative before storing them. As per path instructions, validate at trust boundaries with allow-lists.

Proposed helper hardening
 	result, err := strconv.Atoi(val)
-	if err != nil {
+	if err != nil || result < 0 {
 		klog.ErrorS(err, "failed to convert label to int", "label", label)
-		return defaultValue, err
+		if err == nil {
+			err = fmt.Errorf("%s label must be non-negative", label)
+		}
+		return defaultValue, err
 	}

Apply the same non-negative check to the SageMaker label parsing paths.

Also applies to: 246-251

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/cloud/metadata/k8s.go` around lines 123 - 145, The SageMaker label
parsing in the node metadata path accepts negative integers because strconv.Atoi
only checks format, not range. In the label handling inside the k8s metadata
logic, add a non-negative validation for both LabelSageMakerENICount and
LabelSageMakerBlockDeviceMappingsCount before assigning to numAttachedENIs and
numBlockDeviceMappings, and treat negative values the same as parse failures by
falling back to defaults and logging via klog.ErrorS. Apply the same hardening
to the additional SageMaker label parsing path referenced by the comment so all
trust-boundary label inputs are allow-listed to valid count values.

Source: Path instructions

hack/e2e/kops/patch-cluster.yaml-76-77 (1)

76-77: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Keep ec2:LockSnapshot tag-scoped.

Line 77 grants snapshot locking on every snapshot ARN in the e2e account. The policy already has tag-scoped snapshot statements below; move ec2:LockSnapshot there instead of granting it unconditionally. AWS lists ec2:LockSnapshot as supporting resource-tag condition keys, so the narrower tag-conditioned form is viable. (docs.aws.amazon.com)

Proposed least-privilege adjustment
             "ec2:CreateVolume",
-            "ec2:EnableFastSnapshotRestores",
-            "ec2:LockSnapshot"
+            "ec2:EnableFastSnapshotRestores"

Also add ec2:LockSnapshot to the existing tag-conditioned DeleteSnapshot statements for CSI-created snapshots.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hack/e2e/kops/patch-cluster.yaml` around lines 76 - 77, The
`ec2:LockSnapshot` permission is currently granted broadly in the snapshot
policy, so move it out of the unconditional action list and add it to the
existing tag-conditioned snapshot statements alongside `DeleteSnapshot` for
CSI-created snapshots. Update the relevant IAM policy blocks in
`patch-cluster.yaml` so `ec2:LockSnapshot` is only allowed when the same
resource tag conditions apply, using the existing snapshot tag-scoped statements
as the location to modify.
pkg/cloud/metadata/labels.go-310-321 (1)

310-321: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Nil-map write panics patchSingleNode if a node has no labels.

newNode.Labels[VolumesLabel] = ... writes directly into node.Labels without checking for nil. v1.Node.ObjectMeta.Labels is a plain map that can be nil; DeepCopy() preserves a nil map as nil. Writing to a nil map panics ("assignment to entry in nil map"), crashing the metadata-labeler goroutine. The test helper makeNode() masks this by always initializing Labels: make(map[string]string), so this path isn't exercised by tests.

🔧 Suggested fix
 	newNode := node.DeepCopy()
+	if newNode.Labels == nil {
+		newNode.Labels = make(map[string]string)
+	}
 	numAttachedENIs := enisVolumeMap[instanceID].ENIs
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/cloud/metadata/labels.go` around lines 310 - 321, patchSingleNode can
panic when newNode.Labels is nil because DeepCopy preserves a nil label map and
the code writes to it directly. In patchSingleNode, initialize newNode.Labels
before assigning VolumesLabel and ENIsLabel so nodes without existing labels are
handled safely, and keep the fix near the existing node.DeepCopy() and label
assignment logic.
hack/e2e/run.sh-239-246 (1)

239-246: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Missing --kubeconfig flag makes the restart-count check inconsistent with the rest of the script.

The kubectl get pods call used to compute the max container restart count omits --kubeconfig "${KUBECONFIG}", while the identical query two lines later (used to print the restart table) and other kubectl invocations in this script consistently pass it. In environments where the default kubeconfig differs from ${KUBECONFIG} (e.g. CI runners with multiple kubeconfigs), this check will query the wrong cluster/context and silently miss real restart failures.

🐛 Proposed fix
-  if [[ $(kubectl get pods -n kube-system -l "app.kubernetes.io/name=aws-ebs-csi-driver" -o json |
+  if [[ $(kubectl get pods -n kube-system -l "app.kubernetes.io/name=aws-ebs-csi-driver" -o json --kubeconfig "${KUBECONFIG}" |
     jq -r '.items[].status.containerStatuses[]?.restartCount // 0' |
     sort -nr | head -n 1) -gt 3 ]]; then
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hack/e2e/run.sh` around lines 239 - 246, The restart-count gate in run.sh
uses kubectl without the same --kubeconfig "${KUBECONFIG}" setting used
elsewhere, so it may read the wrong cluster/context. Update the kubectl get pods
call inside the restart threshold check to pass the explicit kubeconfig just
like the later table-printing query and the rest of the script, keeping the
logic in the same block that checks aws-ebs-csi-driver pod restarts.
pkg/cloud/metadata/labels.go-118-133 (1)

118-133: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

time.Tick leaks resources and a single transient error permanently halts label refresh for the leadership term.

Two issues in this loop:

  1. time.Tick documents that its underlying ticker "cannot be recovered by the garbage collector; it leaks". Since continuousUpdateLabels is re-invoked on every leader-election acquisition (ContinuousUpdateLabelsLeaderElection), repeated leadership flapping combined with errors will accumulate leaked tickers.
  2. On any single updateLabels error (e.g. a transient EC2 API throttle), the function returns and the periodic refresh stops entirely until the next leader election — there's no retry/backoff, so a transient failure silently disables node-label metadata for an indefinite period.
🔧 Suggested fix
-	err = updateLabels(ctx, k8sClient, cloud, pvInformer)
-	if err != nil {
-		klog.ErrorS(err, "Could not patch node labels with updated volume/ENI count")
-		return err
-	}
-	for range time.Tick(updateTime) {
-		err = updateLabels(ctx, k8sClient, cloud, pvInformer)
-		if err != nil {
-			klog.ErrorS(err, "Could not patch node labels with updated volume/ENI count")
-			return err
-		}
-	}
-	return nil
+	if err := updateLabels(ctx, k8sClient, cloud, pvInformer); err != nil {
+		klog.ErrorS(err, "Could not patch node labels with updated volume/ENI count")
+	}
+	ticker := time.NewTicker(updateTime)
+	defer ticker.Stop()
+	for {
+		select {
+		case <-ctx.Done():
+			return ctx.Err()
+		case <-ticker.C:
+			if err := updateLabels(ctx, k8sClient, cloud, pvInformer); err != nil {
+				klog.ErrorS(err, "Could not patch node labels with updated volume/ENI count")
+			}
+		}
+	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/cloud/metadata/labels.go` around lines 118 - 133, The periodic refresh in
continuousUpdateLabels should not use time.Tick and should not exit the whole
leader term on a single updateLabels failure. Replace the time.Tick loop with an
explicit ticker created and stopped inside continuousUpdateLabels, and keep
retrying updateLabels on errors instead of returning immediately; use
klog.ErrorS in the continuousUpdateLabels path to log failures while preserving
the loop so labels continue refreshing until context cancellation.
pkg/cloud/metadata/labels.go-240-249 (1)

240-249: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

A single node with an unparseable provider ID aborts label updates for every node.

getMetadata returns immediately on the first parseProviderID error, discarding results for all other (valid) nodes in the batch. Contrast this with patchNodes, which tolerates up to patchFails per-node failures. One node in an unexpected state (e.g. mid-registration, non-AWS) will block the entire periodic ENI/volume label refresh for the whole cluster.

🔧 Suggested fix
 	for _, node := range nodes.Items {
 		id, err := parseProviderID(&node)
 		if err != nil {
-			return nil, err
+			klog.ErrorS(err, "Skipping node with unparseable provider ID", "node", node.Name)
+			continue
 		}
 		if strings.HasPrefix(id, "i-") {
 			nodeIds = append(nodeIds, id)
 		}
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/cloud/metadata/labels.go` around lines 240 - 249, In getMetadata, a
single parseProviderID failure currently aborts the entire node scan and
prevents label refresh for all valid nodes. Update the loop over nodes.Items so
parseProviderID errors are tolerated per node, similar to patchNodes: skip the
bad node, optionally log/debug it, and continue building nodeIds for the rest.
Keep the existing id prefix filter in place and only fail the whole operation
for truly fatal errors outside per-node parsing.

@jsafrane

Copy link
Copy Markdown

/test help

@jsafrane

Copy link
Copy Markdown

/retest
go 1.26 is available now

@jsafrane

Copy link
Copy Markdown

/test help

@jsafrane

Copy link
Copy Markdown

/test e2e-aws-csi-extended

@gnufied

gnufied commented Jul 30, 2026

Copy link
Copy Markdown
Member

/retest

@openshift-ci

openshift-ci Bot commented Jul 30, 2026

Copy link
Copy Markdown

@dfajmon: The following test failed, say /retest to rerun all failed tests or /retest-required to rerun all mandatory failed tests:

Test name Commit Details Required Rerun command
ci/prow/e2e-aws-csi-extended 6efa66d link false /test e2e-aws-csi-extended

Full PR test history. Your PR dashboard.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. I understand the commands that are listed here.

@gnufied

gnufied commented Jul 31, 2026

Copy link
Copy Markdown
Member

/test

@openshift-ci

openshift-ci Bot commented Jul 31, 2026

Copy link
Copy Markdown

@gnufied: The /test command needs one or more targets.
The following commands are available to trigger required jobs:

/test e2e-aws-csi
/test e2e-aws-ovn-upgrade
/test e2e-hypershift
/test images
/test okd-scos-images
/test unit
/test verify
/test verify-commits
/test verify-deps

The following commands are available to trigger optional jobs:

/test e2e-aws-csi-extended
/test okd-scos-e2e-aws-ovn
/test security

Use /test all to run the following jobs that were automatically triggered:

pull-ci-openshift-aws-ebs-csi-driver-master-e2e-aws-csi
pull-ci-openshift-aws-ebs-csi-driver-master-e2e-aws-ovn-upgrade
pull-ci-openshift-aws-ebs-csi-driver-master-e2e-hypershift
pull-ci-openshift-aws-ebs-csi-driver-master-images
pull-ci-openshift-aws-ebs-csi-driver-master-okd-scos-images
pull-ci-openshift-aws-ebs-csi-driver-master-security
pull-ci-openshift-aws-ebs-csi-driver-master-unit
pull-ci-openshift-aws-ebs-csi-driver-master-verify
pull-ci-openshift-aws-ebs-csi-driver-master-verify-commits
pull-ci-openshift-aws-ebs-csi-driver-master-verify-deps
Details

In response to this:

/test

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

approved Indicates a PR has been approved by an approver from all required OWNERS files. do-not-merge/hold Indicates that a PR should not merge because someone has issued a /hold command. jira/valid-reference Indicates that this PR references a valid Jira ticket of any type.

Projects

None yet

Development

Successfully merging this pull request may close these issues.