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
22 changes: 4 additions & 18 deletions charmcraft.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,12 @@ charm-libs:
- lib: operator_libs_linux.systemd
version: "1"

peers:
replicas:
interface: git_ubuntu_primary_info

config:
options:
controller_ip:
description: |
The IP or network location of the primary node. This option is ignored
for the primary node.
default: "127.0.0.1"
type: string
controller_port:
description: |
The network port on the primary node used for import assignments, must
Expand All @@ -71,18 +69,6 @@ config:
An ssh private key that matches with a public key associated with the
lpuser account on Launchpad.
type: secret
node_id:
description: |
The ID of this git-ubuntu operator node, must be unique in the network.
default: 0
type: int
primary:
description: |
If this is the primary git-ubuntu importer node, containing the Broker
and Poller instances. Non-primary nodes will only contain Worker
instances.
default: True
type: boolean
publish:
description: |
If updates should be pushed to Launchpad. Set to False for local
Expand Down
121 changes: 109 additions & 12 deletions src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import logging
from pathlib import Path
from socket import getfqdn

import ops

Expand Down Expand Up @@ -50,9 +51,15 @@ def __init__(self, framework: ops.Framework):
self.framework.observe(self.on.install, self._on_install)
self.framework.observe(self.on.config_changed, self._on_config_changed)

self.framework.observe(self.on.leader_elected, self._on_leader_elected)
self.framework.observe(
self.on.replicas_relation_changed, self._on_replicas_relation_changed
)

@property
def _controller_ip(self) -> str:
return str(self.config.get("controller_ip"))
def _peer_relation(self) -> ops.Relation | None:
"""Get replica peer relation if available."""
return self.model.get_relation("replicas")

@property
def _controller_port(self) -> int:
Expand All @@ -71,16 +78,11 @@ def _lp_username(self) -> str:

@property
def _node_id(self) -> int:
node_id = self.config.get("node_id")
if isinstance(node_id, int):
return node_id
return 0
return int(self.unit.name.split("/")[-1])

@property
def _is_primary(self) -> bool:
if self.config.get("primary"):
return True
return False
return self.unit.is_leader()

@property
def _is_publishing_active(self) -> bool:
Expand Down Expand Up @@ -110,6 +112,77 @@ def _lpuser_ssh_key(self) -> str | None:

return None

@property
def _git_ubuntu_primary_relation(self) -> ops.Relation | None:
"""Get the peer relation that contains the primary node IP.

Returns:
The peer relation or None if it does not exist.
"""
return self.model.get_relation("replicas")

def _open_controller_port(self) -> bool:
"""Open the configured controller network port.

Returns:
True if the port was opened, False otherwise.
"""
self.unit.status = ops.MaintenanceStatus("Opening controller port.")

try:
port = self._controller_port

if port > 0:
self.unit.set_ports(port)
logger.info("Opened controller port %d", port)
else:
self.unit.status = ops.BlockedStatus("Invalid controller port configuration.")
return False
except ops.ModelError:
self.unit.status = ops.BlockedStatus("Failed to open controller port.")
return False

return True

def _set_peer_primary_node_address(self) -> bool:
"""Set the primary node's IP to this unit's in the peer relation databag.

Returns:
True if the data was updated, False otherwise.
"""
self.unit.status = ops.MaintenanceStatus("Setting primary node address in peer relation.")

relation = self._git_ubuntu_primary_relation

if relation:
new_primary_address = getfqdn()
relation.data[self.app]["primary_address"] = new_primary_address
logger.info("Updated primary node address to %s", new_primary_address)
return True

return False

def _get_primary_node_address(self) -> str | None:
"""Get the primary node's network address - local if primary or juju binding if secondary.

Returns:
The primary IP as a string if available, None otherwise.
"""
if self._is_primary:
return "127.0.0.1"

relation = self._git_ubuntu_primary_relation

if relation:
primary_address = relation.data[self.app]["primary_address"]

if primary_address is not None and len(str(primary_address)) > 0:
logger.info("Found primary node address %s", primary_address)
return str(primary_address)

logger.warning("No primary node address found.")
return None

def _refresh_importer_node(self) -> None:
"""Remove old and install new git-ubuntu services."""
self.unit.status = ops.MaintenanceStatus("Refreshing git-ubuntu services.")
Expand Down Expand Up @@ -145,23 +218,29 @@ def _refresh_importer_node(self) -> None:
return
logger.info("Initialized importer node as primary.")
else:
primary_ip = self._get_primary_node_address()

if primary_ip is None:
self.unit.status = ops.BlockedStatus("Secondary node requires a peer relation.")
return

if not node.setup_secondary_node(
GIT_UBUNTU_USER_HOME_DIR,
self._node_id,
self._num_workers,
GIT_UBUNTU_SYSTEM_USER_USERNAME,
will_publish,
self._controller_port,
self._controller_ip,
primary_ip,
):
self.unit.status = ops.BlockedStatus("Failed to install git-ubuntu services.")
return
logger.info("Initialized importer node as secondary.")

self.unit.status = ops.ActiveStatus("Importer node install complete.")

def _on_start(self, _: ops.StartEvent) -> None:
"""Handle start event."""
def _start_services(self) -> None:
"""Start the services and note the result through status."""
if node.start(GIT_UBUNTU_USER_HOME_DIR):
node_type_str = "primary" if self._is_primary else "secondary"
self.unit.status = ops.ActiveStatus(
Expand All @@ -170,6 +249,10 @@ def _on_start(self, _: ops.StartEvent) -> None:
else:
self.unit.status = ops.BlockedStatus("Failed to start services.")

def _on_start(self, _: ops.StartEvent) -> None:
"""Handle start event."""
self._start_services()

def _update_git_user_config(self) -> bool:
"""Attempt to update git config with the default git-ubuntu user name and email."""
self.unit.status = ops.MaintenanceStatus("Updating git config for git-ubuntu user.")
Expand Down Expand Up @@ -260,12 +343,26 @@ def _on_config_changed(self, _: ops.ConfigChangedEvent) -> None:
not self._update_git_user_config()
or not self._update_lpuser_config()
or not self._update_git_ubuntu_snap()
or not self._open_controller_port()
):
return

# Initialize or re-install git-ubuntu services as needed.
self._refresh_importer_node()

def _on_leader_elected(self, _: ops.LeaderElectedEvent) -> None:
"""Refresh services and update peer data when the unit is elected as leader."""
if not self._set_peer_primary_node_address():
self.unit.status = ops.BlockedStatus(
"Failed to update primary node IP in peer relation."
)

def _on_replicas_relation_changed(self, _: ops.RelationChangedEvent) -> None:
"""Refresh services for secondary nodes when peer relations change."""
if not self._is_primary:
self._refresh_importer_node()
self._start_services()


if __name__ == "__main__": # pragma: nocover
ops.main(GitUbuntuCharm)
4 changes: 2 additions & 2 deletions tests/integration/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ def charm():

@pytest.fixture(scope="module")
def app(juju: jubilant.Juju, charm: Path):
"""Deploy git-ubuntu charm with publishing off."""
juju.deploy(f"./{charm}")
"""Deploy git-ubuntu charm with a primary and worker unit."""
juju.deploy(f"./{charm}", num_units=2)
juju.wait(lambda status: jubilant.all_active(status, APP_NAME))

yield APP_NAME
Loading
Loading