Skip to content

Commit 7267ba3

Browse files
authored
Merge pull request #17 from canonical/ssh-key-secret
Add ssh key secret capabilities
2 parents d3c1044 + a13e597 commit 7267ba3

File tree

8 files changed

+202
-11
lines changed

8 files changed

+202
-11
lines changed

.licenserc.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,5 @@ header:
2929
- 'LICENSE'
3030
- 'pyproject.toml'
3131
- 'lib/**'
32+
- 'tests/integration/test-ssh-key'
3233
comment: on-failure

charmcraft.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ config:
6666
This user must be a member of ~git-ubuntu-import.
6767
default: "git-ubuntu-bot"
6868
type: string
69+
lpuser_ssh_key:
70+
description: |
71+
An ssh private key that matches with a public key associated with the
72+
lpuser account on Launchpad.
73+
type: secret
6974
node_id:
7075
description: |
7176
The ID of this git-ubuntu operator node, must be unique in the network.

src/charm.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,21 @@ def _num_workers(self) -> int:
9595
return num_workers
9696
return 0
9797

98+
@property
99+
def _lpuser_ssh_key(self) -> str | None:
100+
try:
101+
secret_id = str(self.config["lpuser_ssh_key"])
102+
ssh_key_secret = self.model.get_secret(id=secret_id)
103+
ssh_key_data = ssh_key_secret.get_content().get("sshkey")
104+
105+
if ssh_key_data is not None:
106+
return str(ssh_key_data)
107+
108+
except (KeyError, ops.SecretNotFoundError, ops.model.ModelError):
109+
pass
110+
111+
return None
112+
98113
def _refresh_importer_node(self) -> None:
99114
"""Remove old and install new git-ubuntu services."""
100115
self.unit.status = ops.MaintenanceStatus("Refreshing git-ubuntu services.")
@@ -103,13 +118,27 @@ def _refresh_importer_node(self) -> None:
103118
self.unit.status = ops.BlockedStatus("Failed to remove old git-ubuntu services.")
104119
return
105120

121+
will_publish = self._is_publishing_active
122+
ssh_key_data = self._lpuser_ssh_key
123+
124+
if will_publish:
125+
if ssh_key_data is None:
126+
logger.warning(
127+
"ssh private key unavailable, blocking publishing to Launchpad for now."
128+
)
129+
will_publish = False
130+
else:
131+
usr.update_ssh_private_key(
132+
GIT_UBUNTU_SYSTEM_USER_USERNAME, GIT_UBUNTU_USER_HOME_DIR, ssh_key_data
133+
)
134+
106135
if self._is_primary:
107136
if not node.setup_primary_node(
108137
GIT_UBUNTU_USER_HOME_DIR,
109138
self._node_id,
110139
self._num_workers,
111140
GIT_UBUNTU_SYSTEM_USER_USERNAME,
112-
self._is_publishing_active,
141+
will_publish,
113142
self._controller_port,
114143
):
115144
self.unit.status = ops.BlockedStatus("Failed to install git-ubuntu services.")
@@ -121,7 +150,7 @@ def _refresh_importer_node(self) -> None:
121150
self._node_id,
122151
self._num_workers,
123152
GIT_UBUNTU_SYSTEM_USER_USERNAME,
124-
self._is_publishing_active,
153+
will_publish,
125154
self._controller_port,
126155
self._controller_ip,
127156
):

src/git_ubuntu.py

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -338,16 +338,22 @@ def destroy_services(service_folder: str) -> bool:
338338
Returns:
339339
True if all services were deleted successfully, False otherwise.
340340
"""
341+
service_list = _get_services_list(service_folder)
342+
343+
if service_list is None:
344+
return False
345+
341346
service_folder_path = pathops.LocalPath(service_folder)
342-
services_removed = False
347+
systemd_service_path = pathops.LocalPath("/etc/systemd/system/")
343348

349+
services_folder_services_removed = False
350+
systemd_folder_services_removed = False
351+
352+
# Remove from service folder
344353
try:
345-
for service_file in service_folder_path.iterdir():
346-
if service_file.suffix == ".service":
347-
service_file.unlink(missing_ok=True)
348-
else:
349-
logger.debug("Skipping removal of non-service file %s", service_file.name)
350-
services_removed = True
354+
for service in service_list:
355+
pathops.LocalPath(service_folder_path, service).unlink(missing_ok=True)
356+
services_folder_services_removed = True
351357
except NotADirectoryError:
352358
logger.error("The provided location %s is not a directory.", service_folder)
353359
except PermissionError as e:
@@ -357,4 +363,18 @@ def destroy_services(service_folder: str) -> bool:
357363
except (IOError, OSError) as e:
358364
logger.error("Failed to remove a service file due to error: %s", str(e))
359365

360-
return services_removed
366+
# Remove from /etc/systemd/system
367+
try:
368+
for service in service_list:
369+
pathops.LocalPath(systemd_service_path, service).unlink(missing_ok=True)
370+
systemd_folder_services_removed = True
371+
except NotADirectoryError:
372+
logger.error("/etc/systemd/system is not a directory.")
373+
except PermissionError as e:
374+
logger.error("Failed to start services due to permission issues: %s", str(e))
375+
except FileNotFoundError:
376+
logger.error("/etc/systemd/system folder not found.")
377+
except (IOError, OSError) as e:
378+
logger.error("Failed to remove a service file due to error: %s", str(e))
379+
380+
return services_folder_services_removed and systemd_folder_services_removed

src/user_management.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,67 @@ def setup_git_ubuntu_user_files(user: str, home_dir: str, git_ubuntu_source_url:
177177
return _write_python_keyring_config_file(user, home_dir)
178178

179179

180+
def update_ssh_private_key(user: str, home_dir: str, ssh_key_data: str) -> bool:
181+
"""Create or refresh the .ssh/id private key file for launchpad access.
182+
183+
Args:
184+
user: The git-ubuntu user.
185+
home_dir: The home directory for the user.
186+
ssh_key_data: The private key data.
187+
188+
Returns:
189+
True if directory and file creation succeeded, False otherwise.
190+
"""
191+
ssh_key_file = pathops.LocalPath(home_dir, ".ssh/id")
192+
193+
parent_dir = ssh_key_file.parent
194+
ssh_dir_success = False
195+
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:
219+
return False
220+
221+
key_success = False
222+
223+
try:
224+
ssh_key_file.write_text(
225+
ssh_key_data,
226+
mode=0o600,
227+
user=user,
228+
group=user,
229+
)
230+
key_success = True
231+
except (FileNotFoundError, NotADirectoryError) as e:
232+
logger.error("Failed to create ssh private key due to directory issues: %s", str(e))
233+
except LookupError as e:
234+
logger.error("Failed to create ssh private key due to issues with root user: %s", str(e))
235+
except PermissionError as e:
236+
logger.error("Failed to create ssh private key due to permission issues: %s", str(e))
237+
238+
return key_success
239+
240+
180241
def set_snap_homedirs(home_dir: str) -> bool:
181242
"""Allow snaps to run for a user with a given home directory.
182243

tests/integration/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ def charm():
3939
@pytest.fixture(scope="module")
4040
def app(juju: jubilant.Juju, charm: Path):
4141
"""Deploy git-ubuntu charm with publishing off."""
42-
juju.deploy(f"./{charm}", config={"publish": False})
42+
juju.deploy(f"./{charm}")
4343
juju.wait(lambda status: jubilant.all_active(status, APP_NAME))
4444

4545
yield APP_NAME

tests/integration/test-ssh-key

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
-----BEGIN OPENSSH PRIVATE KEY-----
2+
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
49+
-----END OPENSSH PRIVATE KEY-----

tests/integration/test_charm.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,33 @@ def test_installed_dump_files(app: str, juju: jubilant.Juju):
115115
app: The app in charge of this unit.
116116
juju: The juju model in charge of the app.
117117
"""
118+
juju.wait(jubilant.all_active)
119+
118120
debian_keyring_status = juju.ssh(
119121
f"{app}/0", "test -f /etc/git-ubuntu/debian-archive-keyring.gpg | echo $?", ""
120122
).strip()
121123
assert debian_keyring_status == "0"
124+
125+
126+
def test_update_config_with_ssh_key(app: str, juju: jubilant.Juju):
127+
"""Wait on other tests, then update config with an ssh key and test that it exists.
128+
129+
Args:
130+
app: The app in charge of this unit.
131+
juju: The juju model in charge of the app.
132+
"""
133+
juju.wait(jubilant.all_active)
134+
sleep(60)
135+
136+
with open("tests/integration/test-ssh-key", "r") as file:
137+
file_content = file.read()
138+
139+
secret_uri = juju.add_secret("lpuser-ssh-key", {"sshkey": file_content})
140+
juju.grant_secret("lpuser-ssh-key", app)
141+
142+
juju.config(app, {"lpuser_ssh_key": secret_uri})
143+
juju.wait(jubilant.all_active)
144+
145+
ssh_key = juju.ssh(f"{app}/0", "sudo -u git-ubuntu cat /var/local/git-ubuntu/.ssh/id", "")
146+
147+
assert file_content == ssh_key

0 commit comments

Comments
 (0)