Skip to content

Commit cf72ac8

Browse files
authored
Merge pull request #22 from canonical/deployment-additions
Add a launchpad secret option Refactor directory creation Add additional charmcraft.yaml links Only provide worker nodes to secondary units
2 parents d8e06d2 + 11e98cd commit cf72ac8

File tree

6 files changed

+134
-118
lines changed

6 files changed

+134
-118
lines changed

charmcraft.yaml

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,14 @@ peers:
4848
interface: git_ubuntu_primary_info
4949

5050
links:
51-
documentation: https://discourse.charmhub.io/t/git-ubuntu-operator-documentation/19098
51+
documentation: |
52+
https://discourse.charmhub.io/t/git-ubuntu-operator-documentation/19098
53+
website: |
54+
https://charmhub.io/git-ubuntu
55+
issues: |
56+
https://github.com/canonical/git-ubuntu-operator/issues
57+
source: |
58+
https://github.com/canonical/git-ubuntu-operator
5259
5360
config:
5461
options:
@@ -74,6 +81,11 @@ config:
7481
An ssh private key that matches with a public key associated with the
7582
lpuser account on Launchpad.
7683
type: secret
84+
lpuser_lp_key:
85+
description: |
86+
A Launchpad keyring entry that allows launchpadlib access, associated
87+
with the lpuser account on Launchpad.
88+
type: secret
7789
publish:
7890
description: |
7991
If updates should be pushed to Launchpad. Set to False for local
@@ -82,6 +94,6 @@ config:
8294
type: boolean
8395
workers:
8496
description: |
85-
The number of git-ubuntu worker processes to maintain.
97+
The number of git-ubuntu worker processes to maintain per secondary node.
8698
default: 2
8799
type: int

src/charm.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,21 @@ def _lpuser_ssh_key(self) -> str | None:
112112

113113
return None
114114

115+
@property
116+
def _lpuser_lp_key(self) -> str | None:
117+
try:
118+
secret_id = str(self.config["lpuser_lp_key"])
119+
lp_key_secret = self.model.get_secret(id=secret_id)
120+
lp_key_data = lp_key_secret.get_content().get("lpkey")
121+
122+
if lp_key_data is not None:
123+
return str(lp_key_data)
124+
125+
except (KeyError, ops.SecretNotFoundError, ops.model.ModelError):
126+
pass
127+
128+
return None
129+
115130
@property
116131
def _git_ubuntu_primary_relation(self) -> ops.Relation | None:
117132
"""Get the peer relation that contains the primary node IP.
@@ -193,6 +208,7 @@ def _refresh_importer_node(self) -> None:
193208

194209
will_publish = self._is_publishing_active
195210
ssh_key_data = self._lpuser_ssh_key
211+
lp_key_data = self._lpuser_lp_key
196212

197213
if will_publish:
198214
if ssh_key_data is None:
@@ -205,13 +221,19 @@ def _refresh_importer_node(self) -> None:
205221
GIT_UBUNTU_SYSTEM_USER_USERNAME, GIT_UBUNTU_USER_HOME_DIR, ssh_key_data
206222
)
207223

224+
if lp_key_data is None:
225+
logger.warning(
226+
"Launchpad keyring entry unavailable, unable to gather package updates."
227+
)
228+
else:
229+
usr.update_launchpad_keyring_secret(
230+
GIT_UBUNTU_SYSTEM_USER_USERNAME, GIT_UBUNTU_USER_HOME_DIR, lp_key_data
231+
)
232+
208233
if self._is_primary:
209234
if not node.setup_primary_node(
210235
GIT_UBUNTU_USER_HOME_DIR,
211-
self._node_id,
212-
self._num_workers,
213236
GIT_UBUNTU_SYSTEM_USER_USERNAME,
214-
will_publish,
215237
self._controller_port,
216238
):
217239
self.unit.status = ops.BlockedStatus("Failed to install git-ubuntu services.")

src/importer_node.py

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -57,38 +57,21 @@ def setup_secondary_node(
5757

5858
def setup_primary_node(
5959
git_ubuntu_user_home: str,
60-
node_id: int,
61-
num_workers: int,
6260
system_user: str,
63-
push_to_lp: bool,
6461
primary_port: int,
6562
) -> bool:
66-
"""Set up necessary services for a primary git-ubuntu importer node.
63+
"""Set up poller and broker services to create a primary git-ubuntu importer node.
6764
6865
Args:
6966
git_ubuntu_user_home: The home directory of the git-ubuntu user.
70-
node_id: The unique ID of this node.
71-
num_workers: The number of worker instances to set up.
7267
system_user: The user + group to run the services as.
73-
push_to_lp: True if publishing repositories to Launchpad.
7468
primary_port: The network port used for worker assignments.
7569
7670
Returns:
7771
True if installation succeeded, False otherwise.
7872
"""
7973
services_folder = pathops.LocalPath(git_ubuntu_user_home, "services")
8074

81-
if not setup_secondary_node(
82-
git_ubuntu_user_home,
83-
node_id,
84-
num_workers,
85-
system_user,
86-
push_to_lp,
87-
primary_port,
88-
"127.0.0.1",
89-
):
90-
return False
91-
9275
# Setup broker service.
9376
if not git_ubuntu.setup_broker_service(
9477
services_folder.as_posix(),

src/user_management.py

Lines changed: 77 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,42 @@ def _run_command_as_user(user: str, command: str) -> bool:
3030
return True
3131

3232

33+
def _mkdir_for_user_with_error_checking(
34+
directory: pathops.LocalPath, user: str, mode: int = 0o755
35+
) -> bool:
36+
"""Create a directory and handle possible mkdir errors.
37+
38+
Args:
39+
directory: The directory to create, skipping if it exists.
40+
user: The user who should own this directory.
41+
mode: The permissions mode for the folder, defaults to standard rwxr-xr-x.
42+
43+
Returns:
44+
True if the folder was created, False otherwise.
45+
"""
46+
try:
47+
directory.mkdir(parents=True, user=user, group=user, mode=mode)
48+
return True
49+
except FileExistsError:
50+
logger.info("Directory %s already exists.", directory.as_posix())
51+
return True
52+
except NotADirectoryError:
53+
logger.error("Directory location %s already exists as a file.", directory.as_posix())
54+
except PermissionError:
55+
logger.error(
56+
"Unable to create new directory %s: permission denied.",
57+
directory.as_posix(),
58+
)
59+
except LookupError:
60+
logger.error(
61+
"Unable to create directory %s: unknown user/group %s",
62+
directory.as_posix(),
63+
user,
64+
)
65+
66+
return False
67+
68+
3369
def _clone_git_ubuntu_source(cloning_user: str, parent_directory: str, source_url: str) -> bool:
3470
"""Clone the git-ubuntu git repo to a given directory.
3571
@@ -71,31 +107,8 @@ def _write_python_keyring_config_file(user: str, home_dir: str) -> bool:
71107
python_keyring_config = pathops.LocalPath(home_dir, ".config/python_keyring/keyringrc.cfg")
72108

73109
parent_dir = python_keyring_config.parent
74-
config_dir_success = False
75-
76-
try:
77-
parent_dir.mkdir(parents=True, user=user, group=user)
78-
config_dir_success = True
79-
except FileExistsError:
80-
logger.info("User config directory %s already exists.", parent_dir.as_posix())
81-
config_dir_success = True
82-
except NotADirectoryError:
83-
logger.error(
84-
"User config directory location %s already exists as a file.", parent_dir.as_posix()
85-
)
86-
except PermissionError:
87-
logger.error(
88-
"Unable to create new user config directory %s: permission denied.",
89-
parent_dir.as_posix(),
90-
)
91-
except LookupError:
92-
logger.error(
93-
"Unable to create config directory %s: unknown user/group %s",
94-
parent_dir.as_posix(),
95-
user,
96-
)
97110

98-
if not config_dir_success:
111+
if not _mkdir_for_user_with_error_checking(parent_dir, user):
99112
return False
100113

101114
keyring_config_success = False
@@ -151,27 +164,8 @@ def setup_git_ubuntu_user_files(user: str, home_dir: str, git_ubuntu_source_url:
151164

152165
# Create the services folder if it does not yet exist
153166
services_dir = pathops.LocalPath(home_dir, "services")
154-
services_dir_success = False
155-
156-
try:
157-
services_dir.mkdir(parents=True, user=user, group=user)
158-
logger.info("Created services directory %s.", services_dir)
159-
services_dir_success = True
160-
except FileExistsError:
161-
logger.info("Services directory %s already exists.", services_dir)
162-
services_dir_success = True
163-
except NotADirectoryError:
164-
logger.error("Service directory location %s already exists as a file.", services_dir)
165-
except PermissionError:
166-
logger.error("Unable to create new service directory %s: permission denied.", services_dir)
167-
except LookupError:
168-
logger.error(
169-
"Unable to create service directory %s: unknown user/group %s",
170-
services_dir,
171-
user,
172-
)
173167

174-
if not services_dir_success:
168+
if not _mkdir_for_user_with_error_checking(services_dir, user):
175169
return False
176170

177171
return _write_python_keyring_config_file(user, home_dir)
@@ -191,31 +185,8 @@ def update_ssh_private_key(user: str, home_dir: str, ssh_key_data: str) -> bool:
191185
ssh_key_file = pathops.LocalPath(home_dir, ".ssh/id")
192186

193187
parent_dir = ssh_key_file.parent
194-
ssh_dir_success = False
195188

196-
try:
197-
parent_dir.mkdir(mode=0o700, parents=True, user=user, group=user)
198-
ssh_dir_success = True
199-
except FileExistsError:
200-
logger.info("ssh directory %s already exists.", parent_dir.as_posix())
201-
ssh_dir_success = True
202-
except NotADirectoryError:
203-
logger.error(
204-
"User ssh directory location %s already exists as a file.", parent_dir.as_posix()
205-
)
206-
except PermissionError:
207-
logger.error(
208-
"Unable to create user ssh directory %s: permission denied.",
209-
parent_dir.as_posix(),
210-
)
211-
except LookupError:
212-
logger.error(
213-
"Unable to create user ssh directory %s: unknown user/group %s",
214-
parent_dir.as_posix(),
215-
user,
216-
)
217-
218-
if not ssh_dir_success:
189+
if not _mkdir_for_user_with_error_checking(parent_dir, user, 0o700):
219190
return False
220191

221192
key_success = False
@@ -238,6 +209,44 @@ def update_ssh_private_key(user: str, home_dir: str, ssh_key_data: str) -> bool:
238209
return key_success
239210

240211

212+
def update_launchpad_keyring_secret(user: str, home_dir: str, lp_key_data: str) -> bool:
213+
"""Create or refresh the python keyring file for launchpad access.
214+
215+
Args:
216+
user: The git-ubuntu user.
217+
home_dir: The home directory for the user.
218+
lp_key_data: The private keyring data.
219+
220+
Returns:
221+
True if directory and file creation succeeded, False otherwise.
222+
"""
223+
lp_key_file = pathops.LocalPath(home_dir, ".local/share/python_keyring/keyring_pass.cfg")
224+
225+
parent_dir = lp_key_file.parent
226+
227+
if not _mkdir_for_user_with_error_checking(parent_dir, user):
228+
return False
229+
230+
key_success = False
231+
232+
try:
233+
lp_key_file.write_text(
234+
lp_key_data,
235+
mode=0o600,
236+
user=user,
237+
group=user,
238+
)
239+
key_success = True
240+
except (FileNotFoundError, NotADirectoryError) as e:
241+
logger.error("Failed to create lp key entry due to directory issues: %s", str(e))
242+
except LookupError as e:
243+
logger.error("Failed to create lp key entry due to issues with root user: %s", str(e))
244+
except PermissionError as e:
245+
logger.error("Failed to create lp key entry due to permission issues: %s", str(e))
246+
247+
return key_success
248+
249+
241250
def set_snap_homedirs(home_dir: str) -> bool:
242251
"""Allow snaps to run for a user with a given home directory.
243252

tests/integration/test_charm.py

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -67,18 +67,19 @@ def get_services_dict(unit_name: str, juju: jubilant.Juju) -> dict[str, dict[str
6767
== "git-ubuntu importer service poller"
6868
)
6969

70-
node_id = int(unit_name.split("/")[-1])
70+
else:
71+
node_id = int(unit_name.split("/")[-1])
7172

72-
assert services[f"git-ubuntu-importer-service-worker{node_id}_0.service"]["active"]
73-
assert (
74-
services[f"git-ubuntu-importer-service-worker{node_id}_0.service"]["description"]
75-
== "git-ubuntu importer service worker"
76-
)
77-
assert services[f"git-ubuntu-importer-service-worker{node_id}_1.service"]["active"]
78-
assert (
79-
services[f"git-ubuntu-importer-service-worker{node_id}_1.service"]["description"]
80-
== "git-ubuntu importer service worker"
81-
)
73+
assert services[f"git-ubuntu-importer-service-worker{node_id}_0.service"]["active"]
74+
assert (
75+
services[f"git-ubuntu-importer-service-worker{node_id}_0.service"]["description"]
76+
== "git-ubuntu importer service worker"
77+
)
78+
assert services[f"git-ubuntu-importer-service-worker{node_id}_1.service"]["active"]
79+
assert (
80+
services[f"git-ubuntu-importer-service-worker{node_id}_1.service"]["description"]
81+
== "git-ubuntu importer service worker"
82+
)
8283

8384

8485
def test_installed_apps(app: str, juju: jubilant.Juju):

tests/unit/test_importer_node.py

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -37,19 +37,14 @@ def test_setup_secondary_node_failure(mock_setup_worker):
3737

3838
@patch("importer_node.git_ubuntu.setup_poller_service")
3939
@patch("importer_node.git_ubuntu.setup_broker_service")
40-
@patch("importer_node.setup_secondary_node")
41-
def test_setup_primary_node_success(mock_secondary, mock_broker, mock_poller):
40+
def test_setup_primary_node_success(mock_broker, mock_poller):
4241
"""Test successful primary node setup."""
43-
mock_secondary.return_value = True
4442
mock_broker.return_value = True
4543
mock_poller.return_value = True
4644

47-
result = importer_node.setup_primary_node(
48-
"/var/local/git-ubuntu", 1, 2, "git-ubuntu", True, 1692
49-
)
45+
result = importer_node.setup_primary_node("/var/local/git-ubuntu", "git-ubuntu", 1692)
5046

5147
assert result is True
52-
mock_secondary.assert_called_once()
5348
mock_broker.assert_called_once()
5449
mock_poller.assert_called_once()
5550

@@ -59,9 +54,7 @@ def test_setup_primary_node_secondary_failure(mock_secondary):
5954
"""Test primary node setup with secondary failure."""
6055
mock_secondary.return_value = False
6156

62-
result = importer_node.setup_primary_node(
63-
"/var/local/git-ubuntu", 1, 2, "git-ubuntu", True, 1692
64-
)
57+
result = importer_node.setup_primary_node("/var/local/git-ubuntu", "git-ubuntu", 1692)
6558

6659
assert result is False
6760

@@ -73,9 +66,7 @@ def test_setup_primary_node_broker_failure(mock_secondary, mock_broker):
7366
mock_secondary.return_value = True
7467
mock_broker.return_value = False
7568

76-
result = importer_node.setup_primary_node(
77-
"/var/local/git-ubuntu", 1, 2, "git-ubuntu", True, 1692
78-
)
69+
result = importer_node.setup_primary_node("/var/local/git-ubuntu", "git-ubuntu", 1692)
7970

8071
assert result is False
8172

@@ -89,9 +80,7 @@ def test_setup_primary_node_poller_failure(mock_secondary, mock_broker, mock_pol
8980
mock_broker.return_value = True
9081
mock_poller.return_value = False
9182

92-
result = importer_node.setup_primary_node(
93-
"/var/local/git-ubuntu", 1, 2, "git-ubuntu", True, 1692
94-
)
83+
result = importer_node.setup_primary_node("/var/local/git-ubuntu", "git-ubuntu", 1692)
9584

9685
assert result is False
9786

0 commit comments

Comments
 (0)