From 26ae2424c1b8f00c91af23646a1522e26c6fc0a7 Mon Sep 17 00:00:00 2001 From: hemanthnakkina <104809969+hemanthnakkina@users.noreply.github.com> Date: Thu, 13 Aug 2026 10:28:40 +0530 Subject: [PATCH 1/2] chore: Make Loadbalancer Amphora feature generally available Loadbalancer Amphora feature is behind a feature gate. Move it out of feature gate so that the functionality is generally available. Signed-off-by: Hemanth Nakkina (cherry picked from commit 75f77ec6e271a4f99d05a1fedf8515a28bfcd206) (cherry picked from commit d26b638c4d4c186eac2d8313dfb9cc2c0040252c) --- sunbeam-python/sunbeam/feature_gates.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sunbeam-python/sunbeam/feature_gates.py b/sunbeam-python/sunbeam/feature_gates.py index a7df49c09..7ea9ab7e7 100644 --- a/sunbeam-python/sunbeam/feature_gates.py +++ b/sunbeam-python/sunbeam/feature_gates.py @@ -238,7 +238,7 @@ def is_visible(self) -> bool: "requires": ["feature.microovn-sdn"], }, "feature.loadbalancer-amphora": { - "generally_available": False, # TODO: Set to True when Amphora support is GA + "generally_available": True, "requires": ["feature.microovn-sdn"], }, } From a4313fc69fe1ba054a963be6c9ff5e52c357236c Mon Sep 17 00:00:00 2001 From: Hemanth Nakkina Date: Thu, 13 Aug 2026 14:56:13 +0530 Subject: [PATCH 2/2] fix(loadbalancer): enforce microovn-sdn requirement at use-time MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Making amphora GA (prev commit) left requires:["feature.microovn-sdn"] on a GA gate, which validate_feature_gate_config — run at every CLI startup and snap set — would raise on default deployments where microovn-sdn is off (not the default SDN on stable/2025.1). - Skip GA gates' requires in validate_feature_gate_config (startup validator); their deps are use-time concerns, not config-time. - Add use-time check in run_configure_plans enable path: raise ClickException if feature.microovn-sdn is not enabled, before deploying any amphora infrastructure. - Update test_ga_gate_with_unmet_dep to reflect GA gates' requires are not validated at startup; add TestRunConfigurePlansMicroovnCheck. Signed-off-by: Hemanth Nakkina (cherry picked from commit 7909e3dd4698a3c3c1ed6cac6ea17f5f758b22be) --- sunbeam-python/sunbeam/feature_gates.py | 8 +++ .../sunbeam/features/loadbalancer/feature.py | 8 +++ .../sunbeam/features/test_loadbalancer.py | 60 +++++++++++++++++++ .../tests/unit/sunbeam/test_feature_gates.py | 10 +++- 4 files changed, 83 insertions(+), 3 deletions(-) diff --git a/sunbeam-python/sunbeam/feature_gates.py b/sunbeam-python/sunbeam/feature_gates.py index 7ea9ab7e7..d3ddd5f55 100644 --- a/sunbeam-python/sunbeam/feature_gates.py +++ b/sunbeam-python/sunbeam/feature_gates.py @@ -239,6 +239,9 @@ def is_visible(self) -> bool: }, "feature.loadbalancer-amphora": { "generally_available": True, + # Amphora requires MicroOVN as the SDN provider. Enforced at use-time + # (sunbeam/features/loadbalancer/feature.py) so a default deployment + # without microovn-sdn enabled is not blocked at CLI startup. "requires": ["feature.microovn-sdn"], }, } @@ -329,6 +332,11 @@ def validate_feature_gate_config(snap: Optional[Snap] = None) -> None: violations: list[str] = [] for gate_key, gate_config in FEATURE_GATES.items(): + # GA gates' `requires` are enforced at use-time, not at startup/config + # validation. Otherwise a GA gate requiring a still-gated feature would + # block every default CLI run where that feature is off by default. + if gate_config.get("generally_available"): + continue dep_keys = gate_config.get("requires", []) if not isinstance(dep_keys, list) or not dep_keys: continue diff --git a/sunbeam-python/sunbeam/features/loadbalancer/feature.py b/sunbeam-python/sunbeam/features/loadbalancer/feature.py index 1c87ec280..da559bf9a 100644 --- a/sunbeam-python/sunbeam/features/loadbalancer/feature.py +++ b/sunbeam-python/sunbeam/features/loadbalancer/feature.py @@ -1800,6 +1800,14 @@ def run_configure_plans( run_plan(plan, console, show_hints) click.echo("Octavia Amphora provider disabled.") else: + # Enforce amphora's requires at use-time: amphora needs MicroOVN as + # the SDN provider. Declared in FEATURE_GATES but not checked at + # startup (GA gate), so verify here before deploying anything. + if not is_feature_gate_enabled("feature.microovn-sdn"): + raise click.ClickException( + "Octavia Amphora provider requires the MicroOVN SDN feature. " + "Enable it with: snap set openstack feature.microovn-sdn=true" + ) # Enable path: credentials are only needed here # (OpenStack resource creation). jhelper_keystone = deployment.get_juju_helper(keystone=True) diff --git a/sunbeam-python/tests/unit/sunbeam/features/test_loadbalancer.py b/sunbeam-python/tests/unit/sunbeam/features/test_loadbalancer.py index 71dae0f82..6c9f3880f 100644 --- a/sunbeam-python/tests/unit/sunbeam/features/test_loadbalancer.py +++ b/sunbeam-python/tests/unit/sunbeam/features/test_loadbalancer.py @@ -1102,6 +1102,66 @@ def test_teardown_runs_when_previously_enabled(self): assert mock_run_plan.call_count == 2 +# --------------------------------------------------------------------------- +# run_configure_plans — use-time check for amphora's microovn-sdn requirement +# --------------------------------------------------------------------------- + + +class TestRunConfigurePlansMicroovnCheck: + """Amphora's requires:["feature.microovn-sdn"] is enforced at use-time. + + Amphora is GA, so its `requires` is not checked at startup/config + validation (validate_feature_gate_config skips GA gates). Instead it is + enforced here, in the enable path of run_configure_plans, before any + amphora infrastructure is deployed. + """ + + def _run_configure_plans(self, amphora_enabled=True, microovn_enabled=True): + from unittest.mock import PropertyMock + + feature = LoadbalancerFeature() + deployment = Mock() + + answers_sequence = [ + {_AMPHORA_ENABLED_KEY: True}, # previous state + {_AMPHORA_ENABLED_KEY: amphora_enabled}, # after AmphoraConfigStep + ] + load_answers_iter = iter(answers_sequence) + + with ( + patch.object( + type(feature), "manifest", new_callable=PropertyMock, return_value=None + ), + patch( + "sunbeam.features.loadbalancer.feature.questions.load_answers", + side_effect=lambda *_: dict(next(load_answers_iter)), + ), + patch("sunbeam.features.loadbalancer.feature.run_preflight_checks"), + patch("sunbeam.features.loadbalancer.feature.run_plan"), + patch("sunbeam.features.loadbalancer.feature.JujuHelper"), + patch("sunbeam.features.loadbalancer.feature.is_feature_gate_enabled") as m, + patch("sunbeam.features.loadbalancer.feature.retrieve_admin_credentials"), + patch("click.echo"), + ): + m.side_effect = lambda key, snap=None: ( + key == "feature.microovn-sdn" and microovn_enabled + ) + feature.run_configure_plans(deployment, show_hints=False) + + def test_enable_raises_when_microovn_sdn_disabled(self): + """Enable path raises if feature.microovn-sdn is not enabled.""" + with pytest.raises(click.ClickException, match="MicroOVN SDN feature"): + self._run_configure_plans(amphora_enabled=True, microovn_enabled=False) + + def test_enable_proceeds_when_microovn_sdn_enabled(self): + """Enable path does not raise when feature.microovn-sdn is enabled.""" + self._run_configure_plans(amphora_enabled=True, microovn_enabled=True) + + def test_disable_does_not_check_microovn_sdn(self): + """Disable path doesn't require microovn-sdn (only enabling does).""" + self._run_configure_plans(amphora_enabled=False, microovn_enabled=False) + + # --------------------------------------------------------------------------- # RemoveCNIInfraStep # --------------------------------------------------------------------------- diff --git a/sunbeam-python/tests/unit/sunbeam/test_feature_gates.py b/sunbeam-python/tests/unit/sunbeam/test_feature_gates.py index 79ede820a..69aab8903 100644 --- a/sunbeam-python/tests/unit/sunbeam/test_feature_gates.py +++ b/sunbeam-python/tests/unit/sunbeam/test_feature_gates.py @@ -884,7 +884,11 @@ def test_multiple_missing_deps(self): }, ) def test_ga_gate_with_unmet_dep(self): - """GA gate with unmet dependency still raises.""" + """GA gate with unmet dependency does not raise at validation. + + GA gates' `requires` are enforced at use-time, not at startup/config + validation — otherwise a GA gate requiring a still-gated feature + would block every default CLI run where that feature is off. + """ snap = self._mock_snap_with_gates({}) - with pytest.raises(FeatureGateError, match="feature.x.*requires.*feature.y"): - validate_feature_gate_config(snap) + validate_feature_gate_config(snap)