diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml new file mode 100644 index 0000000..3372cdf --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -0,0 +1,75 @@ +name: Bug Report +description: File a bug report +labels: ["bug"] +body: + - type: markdown + attributes: + value: > + Thanks for taking the time to fill out this bug report! Before submitting your issue, please make + sure you are using the latest version of the charm. If not, please switch to this image prior to + posting your report to make sure it's not already solved. + - type: textarea + id: bug-description + attributes: + label: Bug Description + description: > + If applicable, add screenshots to help explain the problem you are facing. + validations: + required: true + - type: dropdown + id: impact + attributes: + label: Impact + description: How severe is the business impact of this bug? + options: + - Low (minor issue or cosmetic problem) + - Medium (functionality degraded, workaround exists) + - High (major functionality broken, no workaround) + - Critical (system down, data loss, affecting deployment in production) + validations: + required: true + - type: textarea + id: impact-rationale + attributes: + label: Impact Rationale + description: > + If impact is high or critical, please provide the rationale behind your assessment, impacted + project reference and any relevant project deadline dates which will be affected by this bug. + - type: textarea + id: reproduction + attributes: + label: To Reproduce + description: > + Please provide a step-by-step instruction of how to reproduce the behavior. + placeholder: | + 1. `juju deploy ...` + 2. `juju relate ...` + 3. `juju status --relations` + validations: + required: true + - type: textarea + id: environment + attributes: + label: Environment + description: > + We need to know a bit more about the context in which you run the charm. + - Are you running Juju locally, on lxd, in multipass or on some other platform? + - What track and channel you deployed the charm from (i.e. `latest/edge` or similar). + - Version of any applicable components, like the juju snap, the model controller, lxd, microk8s, and/or multipass. + validations: + required: true + - type: textarea + id: logs + attributes: + label: Relevant log output + description: > + Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. + Fetch the logs using `juju debug-log --replay` and `kubectl logs ...`. Additional details available in the juju docs + at https://documentation.ubuntu.com/juju/3.6/howto/manage-logs/#manage-logs + render: shell + validations: + required: true + - type: textarea + id: additional-context + attributes: + label: Additional context diff --git a/.github/ISSUE_TEMPLATE/enhancement_proposal.yaml b/.github/ISSUE_TEMPLATE/enhancement_proposal.yaml new file mode 100644 index 0000000..df54155 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/enhancement_proposal.yaml @@ -0,0 +1,36 @@ +name: Enhancement Proposal +description: File an enhancement proposal +labels: ["enhancement"] +body: + - type: markdown + attributes: + value: > + Thanks for taking the time to fill out this enhancement proposal! Before submitting your issue, please make + sure there isn't already a prior issue concerning this. If there is, please join that discussion instead. + - type: textarea + id: enhancement-proposal + attributes: + label: Enhancement Proposal + description: > + Describe the enhancement you would like to see in as much detail as needed. + validations: + required: true + - type: dropdown + id: impact + attributes: + label: Impact + description: What is the impact of this feature? + options: + - Low (The feature is nice to have) + - Medium (The feature may be helpful in the future) + - High (The feature has short-term technical value) + - Critical (The feature has big short-term business value) + validations: + required: true + - type: textarea + id: impact-rationale + attributes: + label: Impact Rationale + description: > + If impact is high or critical, please provide the rationale behind your assessment with as + much context as possible. diff --git a/.trivyignore b/.trivyignore index 7a7030b..99bee50 100644 --- a/.trivyignore +++ b/.trivyignore @@ -9,3 +9,4 @@ CVE-2025-58189 CVE-2025-61723 CVE-2025-61724 CVE-2025-61725 +CVE-2025-68121 diff --git a/src/actions.py b/src/actions.py index f3c0ab4..c87f4e8 100644 --- a/src/actions.py +++ b/src/actions.py @@ -2,6 +2,7 @@ # See LICENSE file for licensing details. """tmate-ssh-server charm actions.""" + import logging import ops diff --git a/src/ssh_debug.py b/src/ssh_debug.py index 24ed6f8..3f110d5 100644 --- a/src/ssh_debug.py +++ b/src/ssh_debug.py @@ -2,6 +2,7 @@ # See LICENSE file for licensing details. """Observer module for ssh-debug integration.""" + import logging import typing diff --git a/src/state.py b/src/state.py index 5068555..6755a2c 100644 --- a/src/state.py +++ b/src/state.py @@ -2,6 +2,7 @@ # See LICENSE file for licensing details. """tmate-ssh-server states.""" + import dataclasses import ipaddress import logging diff --git a/src/tmate.py b/src/tmate.py index d656b3b..9b2a34e 100644 --- a/src/tmate.py +++ b/src/tmate.py @@ -331,14 +331,12 @@ def generate_tmate_conf(host: str) -> str: except (IncompleteInitError, KeyInstallError) as exc: raise FingerprintError("Error generating fingerprints.") from exc - return textwrap.dedent( - f""" + return textwrap.dedent(f""" set -g tmate-server-host {host} set -g tmate-server-port {PORT} set -g tmate-server-rsa-fingerprint {fingerprints.rsa} set -g tmate-server-ed25519-fingerprint {fingerprints.ed25519} - """ - ) + """) def remove_stopped_containers() -> None: diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index a16d18f..16f83ec 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -2,6 +2,7 @@ # See LICENSE file for licensing details. """Fixtures for tmate-ssh-server charm integration tests.""" + import logging import secrets @@ -126,7 +127,7 @@ async def wait_machine(): await wait_for(wait_machine, timeout=60 * 5) logger.info("Running update.") - (retcode, _, stderr) = await ops_test.juju("ssh", str(machine.entity_id), "sudo apt update -y") + retcode, _, stderr = await ops_test.juju("ssh", str(machine.entity_id), "sudo apt update -y") assert retcode == 0, f"Failed to run apt update, {stderr}" return machine @@ -135,7 +136,7 @@ async def wait_machine(): async def ssh_machine_fixture(ops_test: OpsTest, machine: Machine): """A machine to test tmate ssh connection.""" logger.info("Installing tmate.") - (retcode, stdout, stderr) = await ops_test.juju( + retcode, stdout, stderr = await ops_test.juju( "ssh", str(machine.entity_id), "DEBIAN_FRONTEND=noninteractive sudo apt-get install -y tmate", @@ -148,7 +149,7 @@ async def ssh_machine_fixture(ops_test: OpsTest, machine: Machine): async def proxy_machine_fixture(ops_test: OpsTest, machine: Machine): """A machine to host squid proxy.""" logger.info("Installing squid.") - (retcode, stdout, stderr) = await ops_test.juju( + retcode, stdout, stderr = await ops_test.juju( "ssh", str(machine.entity_id), "DEBIAN_FRONTEND=noninteractive sudo apt-get install -y squid", @@ -171,17 +172,17 @@ async def proxy_machine_fixture(ops_test: OpsTest, machine: Machine): http_access deny all""" temp_config_file_path = Path(f"./{secrets.token_hex(8)}") temp_config_file_path.write_text(squid_config, encoding="utf-8") - (retcode, stdout, stderr) = await ops_test.juju( + retcode, stdout, stderr = await ops_test.juju( "scp", temp_config_file_path.name, f"{machine.entity_id}:~/squid.conf" ) assert retcode == 0, f"Failed to scp squid conf file {stdout} {stderr}" temp_config_file_path.unlink() # cannot scp directly to /etc due to permission error - (retcode, stdout, stderr) = await ops_test.juju( + retcode, stdout, stderr = await ops_test.juju( "ssh", str(machine.entity_id), "sudo mv ~/squid.conf /etc/squid/squid.conf" ) assert retcode == 0, f"Failed to move squid conf file {stdout} {stderr}" - (retcode, stdout, stderr) = await ops_test.juju( + retcode, stdout, stderr = await ops_test.juju( "ssh", str(machine.entity_id), "sudo systemctl restart squid.service" ) assert retcode == 0, f"Failed to restart squid service, {stdout} {stderr}" diff --git a/tests/integration/test_charm.py b/tests/integration/test_charm.py index 48eed22..a6d61f7 100644 --- a/tests/integration/test_charm.py +++ b/tests/integration/test_charm.py @@ -2,6 +2,7 @@ # See LICENSE file for licensing details. """Integration tests for tmate-ssh-server charm.""" + import logging import secrets from pathlib import Path @@ -30,7 +31,7 @@ async def test_ssh_connection( """ temp_config_file_path = Path(f"./{secrets.token_hex(8)}") temp_config_file_path.write_text(tmate_config, encoding="utf-8") - (retcode, stdout, stderr) = await ops_test.juju( + retcode, stdout, stderr = await ops_test.juju( "scp", temp_config_file_path.name, f"{tmate_machine.entity_id}:~/.tmate.conf" ) assert retcode == 0, f"Failed to scp tmate conf file {stdout} {stderr}" @@ -41,7 +42,7 @@ async def test_ssh_connection( ) logger.info("Starting tmate session") - (retcode, stdout, stderr) = await ops_test.juju( + retcode, stdout, stderr = await ops_test.juju( "ssh", tmate_machine.entity_id, "--", @@ -49,12 +50,12 @@ async def test_ssh_connection( ) assert retcode == 0, f"Error running ssh display command, {stdout}, {stderr}" logger.info("New session created %s %s %s", retcode, stdout, stderr) - (retcode, stdout, stderr) = await ops_test.juju( + retcode, stdout, stderr = await ops_test.juju( "ssh", tmate_machine.entity_id, "--", "tmate -S /tmp/tmate.sock wait tmate-ready" ) assert retcode == 0, f"Error running ssh display command, {stdout}, {stderr}" logger.info("Tmate ready %s %s %s", retcode, stdout, stderr) - (retcode, stdout, stderr) = await ops_test.juju( + retcode, stdout, stderr = await ops_test.juju( "ssh", tmate_machine.entity_id, "--", "tmate -S /tmp/tmate.sock display -p '#{tmate_ssh}'" ) assert retcode == 0, f"Error running ssh display command, {stdout}, {stderr}" @@ -88,9 +89,7 @@ async def test_ssh_connection( session.send("echo test > ~/test.txt && cat ~/test.txt\n") # type: ignore stdout = session.recv(10000) logger.info(SHELL_STDOUT_LOG_STR, str(stdout)) - (retcode, stdout, stderr) = await ops_test.juju( - "ssh", tmate_machine.entity_id, "cat ~/test.txt" - ) + retcode, stdout, stderr = await ops_test.juju("ssh", tmate_machine.entity_id, "cat ~/test.txt") assert retcode == 0, f"Error running ssh command, {stdout}, {stderr}" assert "test" in stdout, f"Failed to write with ssh command, {stdout}" @@ -106,10 +105,10 @@ async def test_restart_of_inactive_service( assert: the service has been restarted successfully. """ await ops_test.juju("ssh", unit.entity_id, "--", "docker stop $(docker ps -q)") - (retcode, stdout, stderr) = await ops_test.juju("ssh", unit.entity_id, "--", "docker ps") + retcode, stdout, stderr = await ops_test.juju("ssh", unit.entity_id, "--", "docker ps") assert retcode == 0, f"Error running docker ps command, {stdout}, {stderr}" assert "tmate-ssh-server" not in stdout, "tmate-ssh-server service is still running" - (retcode, _, _) = await ops_test.juju( + retcode, _, _ = await ops_test.juju( "ssh", unit.entity_id, "--", "systemctl --quiet is-active tmate-ssh-server" ) assert retcode != 0, "tmate-ssh-server service is still running" @@ -117,11 +116,11 @@ async def test_restart_of_inactive_service( async with ops_test.fast_forward(): await unit.model.wait_for_idle(apps=[tmate_ssh_server.name], status=ActiveStatus.name) - (retcode, stdout, stderr) = await ops_test.juju( + retcode, stdout, stderr = await ops_test.juju( "ssh", unit.entity_id, "--", "systemctl --quiet is-active tmate-ssh-server" ) assert retcode == 0, f"tmate-ssh-server service is not running, {stdout}, {stderr}" - (retcode, stdout, stderr) = await ops_test.juju("ssh", unit.entity_id, "--", "docker ps") + retcode, stdout, stderr = await ops_test.juju("ssh", unit.entity_id, "--", "docker ps") assert retcode == 0, f"Error running docker ps command, {stdout}, {stderr}" assert "tmate-ssh-server" in stdout, "tmate-ssh-server service is not running" diff --git a/tests/integration/test_proxy.py b/tests/integration/test_proxy.py index 3a64c58..b3e7008 100644 --- a/tests/integration/test_proxy.py +++ b/tests/integration/test_proxy.py @@ -2,6 +2,7 @@ # See LICENSE file for licensing details. """Integration tests for tmate-ssh-server charm.""" + import logging from juju.machine import Machine @@ -47,7 +48,7 @@ async def wait_for_access_log() -> bool: Returns: Whether ghcr.io access was found in proxy log. """ - (retcode, stdout, stderr) = await ops_test.juju( + retcode, stdout, stderr = await ops_test.juju( "ssh", proxy_machine.entity_id, "sudo cat /var/log/squid/access.log" ) assert retcode == 0, f"Failed read squid access log, {stdout} {stderr}" diff --git a/tests/unit/test_tmate.py b/tests/unit/test_tmate.py index 0ddfc72..d5cfcd6 100644 --- a/tests/unit/test_tmate.py +++ b/tests/unit/test_tmate.py @@ -50,9 +50,7 @@ def test_install_dependencies_proxy_config(monkeypatch: pytest.MonkeyPatch): "https-proxy": "{proxy_config.https_proxy}", "no-proxy": "{proxy_config.no_proxy}" }} -}}""" == tmp_file_path.read_text( - encoding="utf-8" - ) +}}""" == tmp_file_path.read_text(encoding="utf-8") @pytest.mark.parametrize( @@ -497,17 +495,12 @@ def test_generate_tmate_conf(fingerprints: tmate.Fingerprints): """ host = "test_host_value" - assert ( - textwrap.dedent( - f""" + assert textwrap.dedent(f""" set -g tmate-server-host {host} set -g tmate-server-port {tmate.PORT} set -g tmate-server-rsa-fingerprint {fingerprints.rsa} set -g tmate-server-ed25519-fingerprint {fingerprints.ed25519} - """ - ) - == tmate.generate_tmate_conf(host) - ) + """) == tmate.generate_tmate_conf(host) def test_remove_stopped_containers_error(monkeypatch: pytest.MonkeyPatch):