Skip to content

Commit

Permalink
fix: storage mount (#89)
Browse files Browse the repository at this point in the history
  • Loading branch information
yanksyoon committed Jan 5, 2024
1 parent a5b0152 commit 810010e
Show file tree
Hide file tree
Showing 12 changed files with 246 additions and 175 deletions.
15 changes: 10 additions & 5 deletions jenkins_rock/rockcraft.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,22 @@ name: jenkins
summary: Jenkins rock
description: Jenkins OCI image for the Jenkins charm
version: "1.0"
base: ubuntu:22.04
build-base: ubuntu:22.04
base: ubuntu@22.04
build-base: ubuntu@22.04
license: Apache-2.0
platforms:
amd64:
services:
jenkins:
override: merge
summary: The Jenkins server.
command: java -Djava.awt.headless=true -jar /srv/jenkins/jenkins.war
override: replace
summary: jenkins
startup: enabled
command: java -Djava.awt.headless=true -Djava.util.logging.config.file=/var/lib/jenkins/logging.properties -jar /srv/jenkins/jenkins.war
environment:
JENKINS_HOME: /var/lib/jenkins
user: jenkins
group: jenkins

parts:
add-user:
plugin: nil
Expand Down
83 changes: 72 additions & 11 deletions src-docs/jenkins.py.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,37 @@ Wait until Jenkins service is up.

---

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

## <kbd>function</kbd> `is_storage_ready`

```python
is_storage_ready(container: Container) → bool
```

Return whether the Jenkins home directory is mounted and owned by jenkins.



**Args:**

- <b>`container`</b>: The Jenkins workload container.



**Raises:**

- <b>`StorageMountError`</b>: if there was an error getting storage information.



**Returns:**
True if home directory is mounted and owned by jenkins, False otherwise.


---

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

## <kbd>function</kbd> `get_admin_credentials`

Expand All @@ -77,7 +107,7 @@ Retrieve admin credentials.

---

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

## <kbd>function</kbd> `calculate_env`

Expand All @@ -95,7 +125,7 @@ Return a dictionary for Jenkins Pebble layer.

---

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

## <kbd>function</kbd> `get_version`

Expand All @@ -119,7 +149,7 @@ Get the Jenkins server version.

---

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

## <kbd>function</kbd> `bootstrap`

Expand All @@ -145,7 +175,7 @@ Initialize and install Jenkins.

---

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

## <kbd>function</kbd> `get_node_secret`

Expand Down Expand Up @@ -176,7 +206,7 @@ Get node secret from jenkins.

---

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

## <kbd>function</kbd> `add_agent_node`

Expand Down Expand Up @@ -207,7 +237,7 @@ Add a Jenkins agent node.

---

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

## <kbd>function</kbd> `remove_agent_node`

Expand All @@ -233,7 +263,7 @@ Remove a Jenkins agent node.

---

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

## <kbd>function</kbd> `safe_restart`

Expand All @@ -258,7 +288,7 @@ Safely restart Jenkins server after all jobs are done executing.

---

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

## <kbd>function</kbd> `get_agent_name`

Expand All @@ -282,7 +312,7 @@ Infer agent name from unit name.

---

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

## <kbd>function</kbd> `remove_unlisted_plugins`

Expand Down Expand Up @@ -313,7 +343,7 @@ Remove plugins that are not in the list of desired plugins.

---

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

## <kbd>function</kbd> `rotate_credentials`

Expand Down Expand Up @@ -435,6 +465,37 @@ An error occurred trying to update Jenkins.



---

## <kbd>class</kbd> `StorageMountError`
Represents an error probing for Jenkins storage mount.



**Attributes:**

- <b>`msg`</b>: Explanation of the error.

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

### <kbd>function</kbd> `__init__`

```python
__init__(msg: str)
```

Initialize a new instance of the StorageMountError exception.



**Args:**

- <b>`msg`</b>: Explanation of the error.





---

## <kbd>class</kbd> `ValidationError`
Expand Down
3 changes: 1 addition & 2 deletions src-docs/state.py.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,6 @@ The Jenkins k8s operator charm state.
- <b>`restart_time_range`</b>: Time range to allow Jenkins to update version.
- <b>`agent_relation_meta`</b>: Metadata of all agents from units related through agent relation.
- <b>`deprecated_agent_relation_meta`</b>: Metadata of all agents from units related through deprecated agent relation.
- <b>`is_storage_ready`</b>: Whether the Jenkins home storage is mounted.
- <b>`proxy_config`</b>: Proxy configuration to access Jenkins upstream through.
- <b>`plugins`</b>: The list of allowed plugins to install.

Expand All @@ -260,7 +259,7 @@ The Jenkins k8s operator charm state.

---

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

### <kbd>classmethod</kbd> `from_charm`

Expand Down
4 changes: 2 additions & 2 deletions src/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def on_get_admin_password(self, event: ops.ActionEvent) -> None:
event: The event fired from get-admin-password action.
"""
container = self.charm.unit.get_container(JENKINS_SERVICE_NAME)
if not container.can_connect() or not self.state.is_storage_ready:
if not container.can_connect() or not jenkins.is_storage_ready(container):
event.fail("Service not yet ready.")
return
credentials = jenkins.get_admin_credentials(container)
Expand All @@ -49,7 +49,7 @@ def on_rotate_credentials(self, event: ops.ActionEvent) -> None:
event: The rotate credentials event.
"""
container = self.charm.unit.get_container(JENKINS_SERVICE_NAME)
if not container.can_connect() or not self.state.is_storage_ready:
if not container.can_connect() or not jenkins.is_storage_ready(container):
event.fail("Service not yet ready.")
return
try:
Expand Down
8 changes: 4 additions & 4 deletions src/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ def _on_deprecated_agent_relation_joined(self, event: ops.RelationJoinedEvent) -
# This is to avoid the None type, juju-info binding should not be None.
assert (binding := self.model.get_binding("juju-info")) # nosec
host = binding.network.bind_address
if not container.can_connect() or not self.state.is_storage_ready or not host:
if not container.can_connect() or not jenkins.is_storage_ready(container) or not host:
logger.warning("Service not yet ready. Deferring.")
event.defer() # The event needs to be handled after Jenkins has started(pebble ready).
return
Expand Down Expand Up @@ -108,7 +108,7 @@ def _on_agent_relation_joined(self, event: ops.RelationJoinedEvent) -> None:
# This is to avoid the None type, juju-info binding should not be None.
assert (binding := self.model.get_binding("juju-info")) # nosec
host = binding.network.bind_address
if not container.can_connect() or not self.state.is_storage_ready or not host:
if not container.can_connect() or not jenkins.is_storage_ready(container) or not host:
logger.warning("Service not yet ready. Deferring.")
event.defer() # The event needs to be handled after Jenkins has started(pebble ready).
return
Expand Down Expand Up @@ -148,7 +148,7 @@ def _on_deprecated_agent_relation_departed(self, event: ops.RelationDepartedEven
"""
# the event unit cannot be None.
container = self.charm.unit.get_container(JENKINS_SERVICE_NAME)
if not container.can_connect() or not self.state.is_storage_ready:
if not container.can_connect() or not jenkins.is_storage_ready(container):
logger.warning("Relation departed before service ready.")
return

Expand Down Expand Up @@ -176,7 +176,7 @@ def _on_agent_relation_departed(self, event: ops.RelationDepartedEvent) -> None:
"""
# the event unit cannot be None.
container = self.charm.unit.get_container(JENKINS_SERVICE_NAME)
if not container.can_connect() or not self.state.is_storage_ready:
if not container.can_connect() or not jenkins.is_storage_ready(container):
logger.warning("Relation departed before service ready.")
return

Expand Down
18 changes: 10 additions & 8 deletions src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,14 @@ def _on_jenkins_pebble_ready(self, event: ops.PebbleReadyEvent) -> None:
Args:
event: The event fired when pebble is ready.
Raises:
TimeoutError: if there was an error waiting for Jenkins service to come up.
JenkinsBootstrapError: if there was an error installing Jenkins.
JenkinsError: if there was an error fetching Jenkins version.
"""
container = self.unit.get_container(JENKINS_SERVICE_NAME)
if not container or not container.can_connect() or not self.state.is_storage_ready:
if not container or not container.can_connect() or not jenkins.is_storage_ready(container):
self.unit.status = ops.WaitingStatus("Waiting for container/storage.")
event.defer() # Jenkins installation should be retried until preconditions are met.
return
Expand All @@ -129,19 +134,16 @@ def _on_jenkins_pebble_ready(self, event: ops.PebbleReadyEvent) -> None:
jenkins.wait_ready()
except TimeoutError as exc:
logger.error("Timed out waiting for Jenkins, %s", exc)
self.unit.status = ops.BlockedStatus("Timed out waiting for Jenkins.")
return
raise
except jenkins.JenkinsBootstrapError as exc:
logger.error("Error installing plugins, %s", exc)
self.unit.status = ops.BlockedStatus("Error installling plugins.")
return
raise

try:
version = jenkins.get_version()
except jenkins.JenkinsError as exc:
logger.error("Failed to get Jenkins version, %s", exc)
self.unit.status = ops.BlockedStatus("Failed to get Jenkins version.")
return
raise

self.unit.set_workload_version(version)
self.unit.status = ops.ActiveStatus()
Expand Down Expand Up @@ -174,7 +176,7 @@ def _on_update_status(self, _: ops.UpdateStatusEvent) -> None:
2. Update Jenkins patch version if available and is within restart-time-range config value.
"""
container = self.unit.get_container(JENKINS_SERVICE_NAME)
if not container.can_connect() or not self.state.is_storage_ready:
if not container.can_connect() or not jenkins.is_storage_ready(container):
self.unit.status = ops.WaitingStatus("Waiting for container/storage.")
return

Expand Down
39 changes: 39 additions & 0 deletions src/jenkins.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,45 @@ def wait_ready(timeout: int = 300, check_interval: int = 10) -> None:
raise TimeoutError("Timed out waiting for Jenkins to become ready.") from exc


class StorageMountError(JenkinsBootstrapError):
"""Represents an error probing for Jenkins storage mount.
Attributes:
msg: Explanation of the error.
"""

def __init__(self, msg: str):
"""Initialize a new instance of the StorageMountError exception.
Args:
msg: Explanation of the error.
"""
self.msg = msg


def is_storage_ready(container: ops.Container) -> bool:
"""Return whether the Jenkins home directory is mounted and owned by jenkins.
Args:
container: The Jenkins workload container.
Raises:
StorageMountError: if there was an error getting storage information.
Returns:
True if home directory is mounted and owned by jenkins, False otherwise.
"""
mount_info: str = container.pull("/proc/mounts").read()
if str(JENKINS_HOME_PATH) not in mount_info:
return False
proc: ops.pebble.ExecProcess = container.exec(["stat", "-c", "%U", str(JENKINS_HOME_PATH)])
try:
stdout, _ = proc.wait_output()
except (ops.pebble.ChangeError, ops.pebble.ExecError) as exc:
raise StorageMountError("Error fetching storage ownership info.") from exc
return "jenkins" in stdout


@dataclasses.dataclass(frozen=True)
class Credentials:
"""Information needed to log into Jenkins.
Expand Down
Loading

0 comments on commit 810010e

Please sign in to comment.