diff --git a/ops/charms/node_base.py b/ops/charms/node_base.py index 1f68fda..f8c263c 100644 --- a/ops/charms/node_base.py +++ b/ops/charms/node_base.py @@ -62,6 +62,7 @@ def __init__( kubectl: Optional[PathLike] = "/snap/bin/kubectl", user_label_key: str = "labels", timeout: Optional[PositiveInt] = None, + raise_invalid_label: bool = False, ) -> None: """Initialize the LabelMaker. @@ -71,6 +72,7 @@ def __init__( kubectl (Optional[PathLike], optional): Path to the kubectl binary. Defaults to "/snap/bin/kubectl". user_label_key (str, optional): The key in the charm config where the user labels are stored. Defaults to "labels". timeout (Optional[PositiveInt], optional): Number of seconds to retry a command. Defaults to None. + raise_invalid_label (bool, optional): Whether to raise an exception when an invalid label is found. Defaults to False. """ super().__init__(parent=charm, key="NodeBase") self.charm = charm @@ -79,6 +81,7 @@ def __init__( self.user_labels_key = user_label_key self.timeout = DEFAULT_TIMEOUT if timeout is None else timeout self._stored.set_default(current_labels=dict()) + self._raise_invalid_label = raise_invalid_label def _retried_call( self, cmd: List[str], retry_msg: str, timeout: Optional[int] = None @@ -185,7 +188,9 @@ def user_labels(self) -> Mapping[str, str]: try: key, val = item.split("=") except ValueError: - log.info(f"Skipping malformed option: {item}.") + if self._raise_invalid_label: + raise self.NodeLabelError(f"Malformed label: {item}.") + log.error(f"Skipping Malformed label: {item}.") else: user_labels[key] = val return user_labels @@ -210,8 +215,12 @@ def apply_node_labels(self) -> None: # Add any new labels. for key, val in user_labels.items(): - self.set_label(key, val) - self._stored.current_labels[key] = val + if val.endswith("-"): + # Remove the label if the value ends with a dash. + self.remove_label(key) + else: + self.set_label(key, val) + self._stored.current_labels[key] = val # Set the juju-application and juju-charm labels. self.set_label("juju-application", self.charm.model.app.name) diff --git a/ops/tests/unit/test_ops.py b/ops/tests/unit/test_ops.py index 5de00eb..f7988b1 100644 --- a/ops/tests/unit/test_ops.py +++ b/ops/tests/unit/test_ops.py @@ -127,14 +127,19 @@ def test_active_labels_apply_layers_from_config( subprocess_run, harness, label_maker, caplog ): harness.update_config( - {"my-labels": "node-role.kubernetes.io/control-plane= invalid"} + { + "my-labels": "node-role.kubernetes.io/control-plane= invalid extra-label.removable-" + } ) subprocess_run.return_value = RunResponse(0) - label_maker._stored.current_labels = {"node-role.kubernetes.io/worker": ""} + label_maker._stored.current_labels = { + "node-role.kubernetes.io/worker": "", + "extra-label.removable": "", + } label_maker.apply_node_labels() - assert "Skipping malformed option: invalid." in caplog.messages + assert "Skipping Malformed label: invalid." in caplog.messages assert label_maker._stored.current_labels == { - "node-role.kubernetes.io/control-plane": "" + "node-role.kubernetes.io/control-plane": "", } subprocess_run.assert_has_calls( [ @@ -151,6 +156,7 @@ def test_active_labels_apply_layers_from_config( ) for label_args in [ ("node-role.kubernetes.io/worker-",), + ("extra-label.removable-",), ("node-role.kubernetes.io/control-plane=", "--overwrite"), ("juju-application=test-charm", "--overwrite"), ("juju-charm=test-charm", "--overwrite"), @@ -158,3 +164,11 @@ def test_active_labels_apply_layers_from_config( ] ], ) + + +def test_raise_invalid_label(subprocess_run, harness, label_maker): + harness.update_config({"my-labels": "this=isn't=valid"}) + subprocess_run.return_value = RunResponse(0) + label_maker._raise_invalid_label = True + with pytest.raises(node_base.LabelMaker.NodeLabelError): + label_maker.apply_node_labels()