From bed491838c1b1ba7a97d3f22e52492d6f18d7c11 Mon Sep 17 00:00:00 2001 From: Sridhar Gaddam Date: Fri, 13 Mar 2026 17:13:12 +0530 Subject: [PATCH 1/2] Fix infinite reconcile loop when Istio version is EOL When an EOL version is specified in the Istio CR, the Resolve() method returned a plain error. controller-runtime treated this as a retriable failure and kept reconciling with exponential backoff, resulting in an endless loop with "Reconciler error" logs. This PR fixes makes the following changes to the code. 1. Added IsEOLVersion() report a proper message in status conditions. 2. Wraps EOL errors as ValidationError and updates the Istio, IstioCNI and ZTunnel controllers. StandardReconciler already treats ValidationError as non-retriable, so it logs once and stops reconciling until the resource is updated. Signed-off-by: Sridhar Gaddam Fixes: https://github.com/istio-ecosystem/sail-operator/issues/1689 --- controllers/istio/istio_controller.go | 3 +++ pkg/istioversion/version.go | 10 ++++++++++ pkg/istioversion/version_test.go | 5 +++++ pkg/reconcile/cni.go | 3 +++ pkg/reconcile/ztunnel.go | 3 +++ 5 files changed, 24 insertions(+) diff --git a/controllers/istio/istio_controller.go b/controllers/istio/istio_controller.go index 1e19d63f75..b1e7785730 100644 --- a/controllers/istio/istio_controller.go +++ b/controllers/istio/istio_controller.go @@ -123,6 +123,9 @@ func validate(istio *v1.Istio) error { func (r *Reconciler) reconcileActiveRevision(ctx context.Context, istio *v1.Istio) error { version, err := istioversion.Resolve(istio.Spec.Version) if err != nil { + if istioversion.IsEOLVersion(istio.Spec.Version) { + return reconciler.NewValidationError(fmt.Sprintf("version %q is end-of-life and cannot be installed; use a supported version", istio.Spec.Version)) + } return fmt.Errorf("failed to resolve Istio version for %q: %w", istio.Name, err) } values, err := revision.ComputeValues( diff --git a/pkg/istioversion/version.go b/pkg/istioversion/version.go index 1eefc0bbdf..e82988b7d3 100644 --- a/pkg/istioversion/version.go +++ b/pkg/istioversion/version.go @@ -84,6 +84,16 @@ func Resolve(version string) (string, error) { return info.Name, nil } +// IsEOLVersion returns true if the version is known but has been marked end-of-life. +func IsEOLVersion(version string) bool { + for _, eolVersion := range EOL { + if eolVersion == version { + return true + } + } + return false +} + func init() { // we can't use ldflags when running tests, use an env variable instead // tmp workaround for https://github.com/golang/go/issues/64246 diff --git a/pkg/istioversion/version_test.go b/pkg/istioversion/version_test.go index b3683cb3a2..71dcbbe429 100644 --- a/pkg/istioversion/version_test.go +++ b/pkg/istioversion/version_test.go @@ -82,6 +82,7 @@ versions: assert.Equal(t, "v0.1", eolVersions[0]) Map = versionMap + EOL = eolVersions resolved, err := Resolve("latest") assert.NoError(t, err) assert.Equal(t, "v2.0.0", resolved) @@ -89,6 +90,10 @@ versions: resolved, err = Resolve("nonexistent-version") assert.Error(t, err) assert.Equal(t, "", resolved) + + assert.True(t, IsEOLVersion("v0.1")) + assert.False(t, IsEOLVersion("nonexistent-version")) + assert.False(t, IsEOLVersion("v1.0.0")) } func TestParseVersionsYaml_SingleVersion(t *testing.T) { diff --git a/pkg/reconcile/cni.go b/pkg/reconcile/cni.go index e86f519220..beaa25b9b0 100644 --- a/pkg/reconcile/cni.go +++ b/pkg/reconcile/cni.go @@ -70,6 +70,9 @@ func (r *CNIReconciler) Validate(ctx context.Context, version, namespace string) func (r *CNIReconciler) ComputeValues(version string, userValues *v1.CNIValues, profile string) (helm.Values, error) { resolvedVersion, err := istioversion.Resolve(version) if err != nil { + if istioversion.IsEOLVersion(version) { + return nil, reconciler.NewValidationError(fmt.Sprintf("version %q is end-of-life and cannot be installed; use a supported version", version)) + } return nil, fmt.Errorf("failed to resolve CNI version: %w", err) } diff --git a/pkg/reconcile/ztunnel.go b/pkg/reconcile/ztunnel.go index 05848f8510..27e2e8772e 100644 --- a/pkg/reconcile/ztunnel.go +++ b/pkg/reconcile/ztunnel.go @@ -71,6 +71,9 @@ func (r *ZTunnelReconciler) Validate(ctx context.Context, version, namespace str func (r *ZTunnelReconciler) ComputeValues(version string, userValues *v1.ZTunnelValues) (helm.Values, error) { resolvedVersion, err := istioversion.Resolve(version) if err != nil { + if istioversion.IsEOLVersion(version) { + return nil, reconciler.NewValidationError(fmt.Sprintf("version %q is end-of-life and cannot be installed; use a supported version", version)) + } return nil, fmt.Errorf("failed to resolve ZTunnel version: %w", err) } From ebba6c08fe47b997c98fbfecbc178b2ed9059400 Mon Sep 17 00:00:00 2001 From: Sridhar Gaddam Date: Fri, 13 Mar 2026 19:40:43 +0530 Subject: [PATCH 2/2] Fix integration test failure Signed-off-by: Sridhar Gaddam --- tests/integration/api/istio_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/api/istio_test.go b/tests/integration/api/istio_test.go index 62b26df8db..61d26468c7 100644 --- a/tests/integration/api/istio_test.go +++ b/tests/integration/api/istio_test.go @@ -534,7 +534,7 @@ var _ = Describe("Istio resource", Ordered, func() { } Expect(k8sClient.Create(ctx, istio, &client.CreateOptions{})).To(Succeed()) Eventually(getObject).WithArguments(ctx, k8sClient, istioKey, istio). - Should(HaveConditionMessage(v1.IstioConditionReconciled, "failed to resolve Istio version")) + Should(HaveConditionMessage(v1.IstioConditionReconciled, "is end-of-life and cannot be installed")) }) }) })