Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Set charm status to waiting if ingress is not ready or if the 2 ingresses have conflicting path #127

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
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
17 changes: 17 additions & 0 deletions src-docs/ingress.py.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,21 @@ Return the path in whick Jenkins is expected to be listening.
**Returns:**
the path for the ingress URL.

---

<a href="../src/ingress.py#L46"><img align="right" style="float:right;" src="https://img.shields.io/badge/-source-cccccc?style=flat-square"></a>

### <kbd>function</kbd> `is_ingress_ready`

```python
is_ingress_ready() → str
```

Indicate if the ingress relation is ready.



**Returns:**
True if ingress is ready


29 changes: 29 additions & 0 deletions src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,18 @@ def calculate_env(self) -> jenkins.Environment:
JENKINS_PREFIX=self.ingress_observer.get_path(),
)

def _is_ingress_correctly_configured(self) -> typing.Tuple[bool, str]:
"""Indicate whether ingress is properly configured.

Returns:
A tuple of a bool indicating whether ingress is configured and the reason if not.
Copy link
Contributor

Choose a reason for hiding this comment

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

What about making this a named tuple so that we don't have to assign meaning to indexes of the tuple implicitly?

"""
if not self.ingress_observer.is_ingress_ready():
return (False, "Missing ingress relation")
Copy link
Contributor

Choose a reason for hiding this comment

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

Is the ingress integration always required?

if self.ingress_observer.get_path() != self.agent_discovery_ingress_observer.get_path():
return (False, "ingress and agent-discovery-ingress must have the same path")
return (True, "")

def _on_jenkins_pebble_ready(self, event: ops.PebbleReadyEvent) -> None:
"""Configure and start Jenkins server.

Expand All @@ -135,6 +147,12 @@ def _on_jenkins_pebble_ready(self, event: ops.PebbleReadyEvent) -> None:
event.defer() # Jenkins installation should be retried until preconditions are met.
return

is_ingress_configured, reason = self._is_ingress_correctly_configured()
if not is_ingress_configured:
self.unit.status = ops.WaitingStatus(reason)
event.defer()
return

self.unit.status = ops.MaintenanceStatus("Installing Jenkins.")
# First Jenkins server start installs Jenkins server.
container.add_layer(
Expand Down Expand Up @@ -200,6 +218,11 @@ def _on_update_status(self, _: ops.UpdateStatusEvent) -> None:
self.unit.status = ops.WaitingStatus("Waiting for container/storage.")
return

is_ingress_configured, reason = self._is_ingress_correctly_configured()
if not is_ingress_configured:
self.unit.status = ops.WaitingStatus(reason)
return

if self.state.restart_time_range and not timerange.check_now_within_bound_hours(
self.state.restart_time_range.start, self.state.restart_time_range.end
):
Expand All @@ -220,6 +243,12 @@ def _on_jenkins_home_storage_attached(self, event: ops.StorageAttachedEvent) ->
event.defer()
return

is_ingress_configured, reason = self._is_ingress_correctly_configured()
if not is_ingress_configured:
self.unit.status = ops.WaitingStatus(reason)
event.defer()
return

command = [
"chown",
"-R",
Expand Down
8 changes: 8 additions & 0 deletions src/ingress.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,11 @@ def get_path(self) -> str:
if path == "/":
return ""
return path

def is_ingress_ready(self) -> str:
"""Indicate if the ingress relation is ready.

Returns:
True if ingress is ready
"""
return self.ingress.is_ready()
132 changes: 132 additions & 0 deletions tests/unit/test_charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import datetime
import functools
import json
import typing
from unittest.mock import MagicMock, PropertyMock, patch

Expand Down Expand Up @@ -106,12 +107,16 @@ def test__on_jenkins_pebble_ready_error(
"""
# speed up waiting by changing default argument values
monkeypatch.setattr(requests, "get", functools.partial(mocked_get_request, status_code=200))

with (
patch.object(jenkins.Jenkins, "wait_ready"),
patch.object(jenkins.Jenkins, "bootstrap") as bootstrap_mock,
):
bootstrap_mock.side_effect = jenkins.JenkinsBootstrapError
harness = harness_container.harness
harness.add_relation(
"ingress", "traefik-k8s", app_data={"ingress": json.dumps({"url": "http://test"})}
)
harness.begin()

jenkins_charm = typing.cast(JenkinsK8sOperatorCharm, harness.charm)
Expand All @@ -131,6 +136,9 @@ def test__on_jenkins_pebble_ready_get_version_error(
"""
monkeypatch.setattr(requests, "get", functools.partial(mocked_get_request, status_code=200))
harness = harness_container.harness
harness.add_relation(
"ingress", "traefik-k8s", app_data={"ingress": json.dumps({"url": "http://test"})}
)
harness.begin()

with (
Expand All @@ -153,6 +161,9 @@ def test__on_jenkins_pebble_jenkins_not_ready(harness_container: HarnessWithCont
assert: the charm raises an error.
"""
harness = harness_container.harness
harness.add_relation(
"ingress", "traefik-k8s", app_data={"ingress": json.dumps({"url": "http://test"})}
)
harness.begin()
with patch.object(jenkins.Jenkins, "wait_ready") as wait_ready_mock:
wait_ready_mock.side_effect = TimeoutError
Expand All @@ -177,6 +188,9 @@ def test__on_jenkins_pebble_ready(harness_container: HarnessWithContainer):
patch.object(jenkins.Jenkins, "version", new_callable=PropertyMock) as version_mock,
):
version_mock.return_value = "1"
harness.add_relation(
"ingress", "traefik-k8s", app_data={"ingress": json.dumps({"url": "http://test"})}
)
harness.begin()

jenkins_charm = typing.cast(JenkinsK8sOperatorCharm, harness.charm)
Expand All @@ -187,6 +201,75 @@ def test__on_jenkins_pebble_ready(harness_container: HarnessWithContainer):
), f"unit should be in {ACTIVE_STATUS_NAME}"


@pytest.mark.usefixtures("patch_os_environ")
def test__on_jenkins_pebble_ready_ingress_not_present(harness_container: HarnessWithContainer):
"""
arrange: given a charm without an ingress relation.
act: when the charm starts.
assert: The charm is set to waiting status with the correct message.
"""
harness = harness_container.harness
with (
patch.object(jenkins.Jenkins, "wait_ready"),
patch.object(jenkins.Jenkins, "bootstrap"),
patch.object(jenkins.Jenkins, "version", new_callable=PropertyMock) as version_mock,
):
version_mock.return_value = "1"
harness.begin()

jenkins_charm = typing.cast(JenkinsK8sOperatorCharm, harness.charm)
jenkins_charm._on_jenkins_pebble_ready(MagicMock(spec=ops.PebbleReadyEvent))

assert (
jenkins_charm.unit.status.name == WAITING_STATUS_NAME
), f"unit should be in {WAITING_STATUS_NAME}"
assert jenkins_charm.unit.status.message == "Missing ingress relation"


@pytest.mark.usefixtures("patch_os_environ")
def test__on_jenkins_pebble_ready_ingress_routing_mode_mismatch(
harness_container: HarnessWithContainer,
):
"""
arrange: given a charm with ingress and agent-discovery-ingress having different routing modes.
act: when the charm starts.
assert: The charm is set to waiting status with the correct message.
"""
harness = harness_container.harness
with (
patch.object(jenkins.Jenkins, "wait_ready"),
patch.object(jenkins.Jenkins, "bootstrap"),
patch.object(jenkins.Jenkins, "version", new_callable=PropertyMock) as version_mock,
):
version_mock.return_value = "1"
harness.add_relation(
"ingress",
"traefik-k8s-ingress-1",
app_data={"ingress": json.dumps({"url": "http://traefik1.ingress.internal"})},
)
harness.add_relation(
"agent-discovery-ingress",
"traefik-k8s-ingress-2",
app_data={"ingress": json.dumps({"url": "http://ingress.internal/traefik2"})},
)
harness.begin()

jenkins_charm = typing.cast(JenkinsK8sOperatorCharm, harness.charm)
jenkins_charm._on_jenkins_pebble_ready(MagicMock(spec=ops.PebbleReadyEvent))

assert (
jenkins_charm.agent_discovery_ingress_observer.get_path()
!= jenkins_charm.ingress_observer.get_path()
)
assert (
jenkins_charm.unit.status.name == WAITING_STATUS_NAME
), f"unit should be in {WAITING_STATUS_NAME}"
assert (
jenkins_charm.unit.status.message
== "ingress and agent-discovery-ingress must have the same path"
)


@pytest.mark.parametrize(
"exception, expected_status",
[
Expand Down Expand Up @@ -255,6 +338,9 @@ def test__on_update_status_not_in_time_range(
mock_datetime.utcnow.return_value = datetime.datetime(2023, 1, 1, 23)
monkeypatch.setattr(timerange, "datetime", mock_datetime)
harness_container.harness.update_config({"restart-time-range": "00-23"})
harness_container.harness.add_relation(
"ingress", "traefik-k8s", app_data={"ingress": json.dumps({"url": "http://test"})}
)
harness_container.harness.begin()
harness = harness_container.harness
original_status = harness.charm.unit.status.name
Expand Down Expand Up @@ -320,6 +406,9 @@ def test__on_update_status(
"_remove_unlisted_plugins",
lambda *_args, **_kwargs: remove_plugin_status,
)
harness_container.harness.add_relation(
"ingress", "traefik-k8s", app_data={"ingress": json.dumps({"url": "http://test"})}
)
harness_container.harness.begin()

jenkins_charm = typing.cast(JenkinsK8sOperatorCharm, harness_container.harness.charm)
Expand All @@ -328,12 +417,38 @@ def test__on_update_status(
assert jenkins_charm.unit.status == expected_status


def test__on_update_status_ingress_missing(
harness_container: HarnessWithContainer,
monkeypatch: pytest.MonkeyPatch,
):
"""
arrange: Given a jenkins charm with ingress missing.
act: when _on_update_status is called.
assert: Unit should be in waiting status with the correct message.
"""
monkeypatch.setattr(
JenkinsK8sOperatorCharm,
"_remove_unlisted_plugins",
MagicMock(),
)
harness_container.harness.begin()

jenkins_charm = typing.cast(JenkinsK8sOperatorCharm, harness_container.harness.charm)
jenkins_charm._on_update_status(MagicMock(spec=ops.UpdateStatusEvent))

assert jenkins_charm.unit.status.name == WAITING_STATUS_NAME
assert jenkins_charm.unit.status.message == "Missing ingress relation"


def test__on_jenkins_home_storage_attached(harness: Harness, monkeypatch: pytest.MonkeyPatch):
"""
arrange: given a base jenkins charm.
act: when _on_jenkins_home_storage_attached is called.
assert: The chown command was ran on the jenkins container with correct parameters.
"""
harness.add_relation(
"ingress", "traefik-k8s", app_data={"ingress": json.dumps({"url": "http://test"})}
)
harness.begin()
jenkins_charm = typing.cast(JenkinsK8sOperatorCharm, harness.charm)
container = jenkins_charm.unit.containers["jenkins"]
Expand All @@ -353,6 +468,23 @@ def test__on_jenkins_home_storage_attached(harness: Harness, monkeypatch: pytest
)


def test__on_jenkins_home_storage_attached_ingress_missing(harness: Harness):
"""
arrange: given a base jenkins charm with missing ingress relation.
act: when _on_jenkins_home_storage_attached is called.
assert: The charm is set to waiting status with the correct message.
"""
harness.begin()
jenkins_charm = typing.cast(JenkinsK8sOperatorCharm, harness.charm)
container = jenkins_charm.unit.containers["jenkins"]
harness.set_can_connect(container, True)

jenkins_charm._on_jenkins_home_storage_attached(MagicMock(spec=ops.StorageAttachedEvent))

assert jenkins_charm.unit.status.name == WAITING_STATUS_NAME
assert jenkins_charm.unit.status.message == "Missing ingress relation"


def test__on_jenkins_home_storage_attached_container_not_ready(
harness: Harness, monkeypatch: pytest.MonkeyPatch
):
Expand Down
Loading