Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion sunbeam-python/sunbeam/feature_gates.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,10 @@ 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,
# 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"],
},
}
Expand Down Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions sunbeam-python/sunbeam/features/loadbalancer/feature.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
60 changes: 60 additions & 0 deletions sunbeam-python/tests/unit/sunbeam/features/test_loadbalancer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
# ---------------------------------------------------------------------------
Expand Down
10 changes: 7 additions & 3 deletions sunbeam-python/tests/unit/sunbeam/test_feature_gates.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Loading