Skip to content

Commit 7112b7b

Browse files
authored
Merge pull request #18 from canonical/integrations
Allow multi-unit setups
2 parents 7267ba3 + 356c24e commit 7112b7b

File tree

4 files changed

+199
-68
lines changed

4 files changed

+199
-68
lines changed

charmcraft.yaml

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,12 @@ charm-libs:
4141
- lib: operator_libs_linux.systemd
4242
version: "1"
4343

44+
peers:
45+
replicas:
46+
interface: git_ubuntu_primary_info
47+
4448
config:
4549
options:
46-
controller_ip:
47-
description: |
48-
The IP or network location of the primary node. This option is ignored
49-
for the primary node.
50-
default: "127.0.0.1"
51-
type: string
5250
controller_port:
5351
description: |
5452
The network port on the primary node used for import assignments, must
@@ -71,18 +69,6 @@ config:
7169
An ssh private key that matches with a public key associated with the
7270
lpuser account on Launchpad.
7371
type: secret
74-
node_id:
75-
description: |
76-
The ID of this git-ubuntu operator node, must be unique in the network.
77-
default: 0
78-
type: int
79-
primary:
80-
description: |
81-
If this is the primary git-ubuntu importer node, containing the Broker
82-
and Poller instances. Non-primary nodes will only contain Worker
83-
instances.
84-
default: True
85-
type: boolean
8672
publish:
8773
description: |
8874
If updates should be pushed to Launchpad. Set to False for local

src/charm.py

Lines changed: 109 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import logging
1616
from pathlib import Path
17+
from socket import getfqdn
1718

1819
import ops
1920

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

54+
self.framework.observe(self.on.leader_elected, self._on_leader_elected)
55+
self.framework.observe(
56+
self.on.replicas_relation_changed, self._on_replicas_relation_changed
57+
)
58+
5359
@property
54-
def _controller_ip(self) -> str:
55-
return str(self.config.get("controller_ip"))
60+
def _peer_relation(self) -> ops.Relation | None:
61+
"""Get replica peer relation if available."""
62+
return self.model.get_relation("replicas")
5663

5764
@property
5865
def _controller_port(self) -> int:
@@ -71,16 +78,11 @@ def _lp_username(self) -> str:
7178

7279
@property
7380
def _node_id(self) -> int:
74-
node_id = self.config.get("node_id")
75-
if isinstance(node_id, int):
76-
return node_id
77-
return 0
81+
return int(self.unit.name.split("/")[-1])
7882

7983
@property
8084
def _is_primary(self) -> bool:
81-
if self.config.get("primary"):
82-
return True
83-
return False
85+
return self.unit.is_leader()
8486

8587
@property
8688
def _is_publishing_active(self) -> bool:
@@ -110,6 +112,77 @@ def _lpuser_ssh_key(self) -> str | None:
110112

111113
return None
112114

115+
@property
116+
def _git_ubuntu_primary_relation(self) -> ops.Relation | None:
117+
"""Get the peer relation that contains the primary node IP.
118+
119+
Returns:
120+
The peer relation or None if it does not exist.
121+
"""
122+
return self.model.get_relation("replicas")
123+
124+
def _open_controller_port(self) -> bool:
125+
"""Open the configured controller network port.
126+
127+
Returns:
128+
True if the port was opened, False otherwise.
129+
"""
130+
self.unit.status = ops.MaintenanceStatus("Opening controller port.")
131+
132+
try:
133+
port = self._controller_port
134+
135+
if port > 0:
136+
self.unit.set_ports(port)
137+
logger.info("Opened controller port %d", port)
138+
else:
139+
self.unit.status = ops.BlockedStatus("Invalid controller port configuration.")
140+
return False
141+
except ops.ModelError:
142+
self.unit.status = ops.BlockedStatus("Failed to open controller port.")
143+
return False
144+
145+
return True
146+
147+
def _set_peer_primary_node_address(self) -> bool:
148+
"""Set the primary node's IP to this unit's in the peer relation databag.
149+
150+
Returns:
151+
True if the data was updated, False otherwise.
152+
"""
153+
self.unit.status = ops.MaintenanceStatus("Setting primary node address in peer relation.")
154+
155+
relation = self._git_ubuntu_primary_relation
156+
157+
if relation:
158+
new_primary_address = getfqdn()
159+
relation.data[self.app]["primary_address"] = new_primary_address
160+
logger.info("Updated primary node address to %s", new_primary_address)
161+
return True
162+
163+
return False
164+
165+
def _get_primary_node_address(self) -> str | None:
166+
"""Get the primary node's network address - local if primary or juju binding if secondary.
167+
168+
Returns:
169+
The primary IP as a string if available, None otherwise.
170+
"""
171+
if self._is_primary:
172+
return "127.0.0.1"
173+
174+
relation = self._git_ubuntu_primary_relation
175+
176+
if relation:
177+
primary_address = relation.data[self.app]["primary_address"]
178+
179+
if primary_address is not None and len(str(primary_address)) > 0:
180+
logger.info("Found primary node address %s", primary_address)
181+
return str(primary_address)
182+
183+
logger.warning("No primary node address found.")
184+
return None
185+
113186
def _refresh_importer_node(self) -> None:
114187
"""Remove old and install new git-ubuntu services."""
115188
self.unit.status = ops.MaintenanceStatus("Refreshing git-ubuntu services.")
@@ -145,23 +218,29 @@ def _refresh_importer_node(self) -> None:
145218
return
146219
logger.info("Initialized importer node as primary.")
147220
else:
221+
primary_ip = self._get_primary_node_address()
222+
223+
if primary_ip is None:
224+
self.unit.status = ops.BlockedStatus("Secondary node requires a peer relation.")
225+
return
226+
148227
if not node.setup_secondary_node(
149228
GIT_UBUNTU_USER_HOME_DIR,
150229
self._node_id,
151230
self._num_workers,
152231
GIT_UBUNTU_SYSTEM_USER_USERNAME,
153232
will_publish,
154233
self._controller_port,
155-
self._controller_ip,
234+
primary_ip,
156235
):
157236
self.unit.status = ops.BlockedStatus("Failed to install git-ubuntu services.")
158237
return
159238
logger.info("Initialized importer node as secondary.")
160239

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

163-
def _on_start(self, _: ops.StartEvent) -> None:
164-
"""Handle start event."""
242+
def _start_services(self) -> None:
243+
"""Start the services and note the result through status."""
165244
if node.start(GIT_UBUNTU_USER_HOME_DIR):
166245
node_type_str = "primary" if self._is_primary else "secondary"
167246
self.unit.status = ops.ActiveStatus(
@@ -170,6 +249,10 @@ def _on_start(self, _: ops.StartEvent) -> None:
170249
else:
171250
self.unit.status = ops.BlockedStatus("Failed to start services.")
172251

252+
def _on_start(self, _: ops.StartEvent) -> None:
253+
"""Handle start event."""
254+
self._start_services()
255+
173256
def _update_git_user_config(self) -> bool:
174257
"""Attempt to update git config with the default git-ubuntu user name and email."""
175258
self.unit.status = ops.MaintenanceStatus("Updating git config for git-ubuntu user.")
@@ -260,12 +343,26 @@ def _on_config_changed(self, _: ops.ConfigChangedEvent) -> None:
260343
not self._update_git_user_config()
261344
or not self._update_lpuser_config()
262345
or not self._update_git_ubuntu_snap()
346+
or not self._open_controller_port()
263347
):
264348
return
265349

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

353+
def _on_leader_elected(self, _: ops.LeaderElectedEvent) -> None:
354+
"""Refresh services and update peer data when the unit is elected as leader."""
355+
if not self._set_peer_primary_node_address():
356+
self.unit.status = ops.BlockedStatus(
357+
"Failed to update primary node IP in peer relation."
358+
)
359+
360+
def _on_replicas_relation_changed(self, _: ops.RelationChangedEvent) -> None:
361+
"""Refresh services for secondary nodes when peer relations change."""
362+
if not self._is_primary:
363+
self._refresh_importer_node()
364+
self._start_services()
365+
269366

270367
if __name__ == "__main__": # pragma: nocover
271368
ops.main(GitUbuntuCharm)

tests/integration/conftest.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@ def charm():
3838

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

4545
yield APP_NAME

0 commit comments

Comments
 (0)