Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add integration test with external network disabled and aproxy setup #175

Merged
merged 35 commits into from
Jan 5, 2024
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
827077c
Move repo_policy_compliance_service.py to scripts
yhaliaw Dec 14, 2023
da143c2
Merge branch 'main' into test/proxy-env
yhaliaw Dec 18, 2023
cd1720c
Fix typo
yhaliaw Dec 18, 2023
cee9052
Test application with proxy setup and external network disabled
yhaliaw Dec 19, 2023
ccc3143
Use iptables to block juju machine traffic
yhaliaw Dec 19, 2023
2dad1bb
Merge branch 'main' into test/proxy-env
yhaliaw Dec 19, 2023
b266454
Fix lint issues
yhaliaw Dec 19, 2023
7d39d52
Fix typo
yhaliaw Dec 20, 2023
d2e819b
Add more HTTP proxy juju config for integration test with proxy
yhaliaw Dec 20, 2023
a499a45
Fix proxy config for proxy test
yhaliaw Dec 20, 2023
a4a3190
Add proxy to build-image script
yhaliaw Dec 21, 2023
a4d4397
Fix command string for scheduled runner
yhaliaw Dec 21, 2023
10a40b2
Add more IP to whitelist for proxy test
yhaliaw Dec 21, 2023
1f18cc3
Add git proxy config for pip installion
yhaliaw Dec 21, 2023
4ff8ae6
Update ping test
yhaliaw Dec 21, 2023
ba63685
Remove git proxy config
yhaliaw Dec 21, 2023
b81bbce
Fix shell lint
yhaliaw Dec 21, 2023
91dcc6b
Add every possible private IP address for proxy test whitelist
yhaliaw Dec 22, 2023
b0ba8c3
Increase timeout
yhaliaw Dec 22, 2023
ac58062
fix lint
yhaliaw Dec 22, 2023
1df5b7e
Fix reconcile runner action not changing charm back to active
yhaliaw Dec 22, 2023
740aa68
Test moving upgrade kernel to on_start hook
yhaliaw Dec 22, 2023
133d4da
Fix proxy test
yhaliaw Dec 22, 2023
6392b1f
Fix test
yhaliaw Jan 2, 2024
08f4a6b
Test if aproxy is installed correctly
yhaliaw Jan 2, 2024
c105f7c
Check aproxy configuration
yhaliaw Jan 2, 2024
d4da717
Use tinyproxy instead of proxy.py
yhaliaw Jan 2, 2024
3fadc61
Fix retry of snap install
yhaliaw Jan 2, 2024
48f68b5
Merge branch 'main' into test/proxy-env
yhaliaw Jan 3, 2024
ca43d01
Refactor to reduce indentation
yhaliaw Jan 3, 2024
e07b25e
Merge branch 'main' into test/proxy-env
yhaliaw Jan 3, 2024
f5883a1
Fix assertion
yhaliaw Jan 3, 2024
8e77285
Increase retry of installation of aproxy for integration test
yhaliaw Jan 3, 2024
1fcad59
Update tests/integration/test_charm_with_proxy.py
yhaliaw Jan 4, 2024
5e9c51e
Merge branch 'main' into test/proxy-env
yhaliaw Jan 4, 2024
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
1 change: 1 addition & 0 deletions charmcraft.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ parts:
- rust-all # for cryptography
prime:
- scripts/build-image.sh
- scripts/repo_policy_compliance_service.py
bases:
- build-on:
- name: "ubuntu"
Expand Down
24 changes: 23 additions & 1 deletion scripts/build-image.sh
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,15 @@ cleanup() {

HTTP_PROXY="$1"
HTTPS_PROXY="$2"
MODE="$3"
NO_PROXY="$3"
MODE="$4"

if [[ -n "$HTTP_PROXY" ]]; then
/snap/bin/lxc config set core.proxy_http "$HTTP_PROXY"
fi
if [[ -n "$HTTPS_PROXY" ]]; then
/snap/bin/lxc config set core.proxy_https "$HTTPS_PROXY"
fi

cleanup '/snap/bin/lxc info builder &> /dev/null' '/snap/bin/lxc delete builder --force' 'Cleanup LXD VM of previous run' 10

Expand All @@ -60,6 +68,20 @@ else
retry '/snap/bin/lxc launch ubuntu-daily:jammy builder --vm --device root,size=8GiB' 'Starting LXD VM'
fi
retry '/snap/bin/lxc exec builder -- /usr/bin/who' 'Wait for lxd agent to be ready' 30
if [[ -n "$HTTP_PROXY" ]]; then
/snap/bin/lxc exec builder -- echo "HTTP_PROXY=$HTTP_PROXY" >> /etc/environment
/snap/bin/lxc exec builder -- echo "http_proxy=$HTTP_PROXY" >> /etc/environment
/snap/bin/lxc exec builder -- echo "Acquire::http::Proxy \"$HTTP_PROXY\";" >> /etc/apt/apt.conf
fi
if [[ -n "$HTTPS_PROXY" ]]; then
/snap/bin/lxc exec builder -- echo "HTTPS_PROXY=$HTTPS_PROXY" >> /etc/environment
/snap/bin/lxc exec builder -- echo "https_proxy=$HTTPS_PROXY" >> /etc/environment
/snap/bin/lxc exec builder -- echo "Acquire::https::Proxy \"$HTTPS_PROXY\";" >> /etc/apt/apt.conf
fi
if [[ -n "$NO_PROXY" ]]; then
/snap/bin/lxc exec builder -- echo "NO_PROXY=$NO_PROXY" >> /etc/environment
/snap/bin/lxc exec builder -- echo "no_proxy=$NO_PROXY" >> /etc/environment
fi
retry '/snap/bin/lxc exec builder -- /usr/bin/nslookup github.com' 'Wait for network to be ready' 30

/snap/bin/lxc exec builder -- /usr/bin/apt-get update
Expand Down
4 changes: 2 additions & 2 deletions src-docs/runner_manager.py.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ Construct RunnerManager object for creating and managing runners.

---

<a href="../src/runner_manager.py#L641"><img align="right" style="float:right;" src="https://img.shields.io/badge/-source-cccccc?style=flat-square"></a>
<a href="../src/runner_manager.py#L643"><img align="right" style="float:right;" src="https://img.shields.io/badge/-source-cccccc?style=flat-square"></a>

### <kbd>function</kbd> `build_runner_image`

Expand Down Expand Up @@ -172,7 +172,7 @@ Bring runners in line with target.

---

<a href="../src/runner_manager.py#L651"><img align="right" style="float:right;" src="https://img.shields.io/badge/-source-cccccc?style=flat-square"></a>
<a href="../src/runner_manager.py#L653"><img align="right" style="float:right;" src="https://img.shields.io/badge/-source-cccccc?style=flat-square"></a>

### <kbd>function</kbd> `schedule_build_runner_image`

Expand Down
9 changes: 6 additions & 3 deletions src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ class GithubRunnerCharm(CharmBase):

service_token_path = Path("service_token")
repo_check_web_service_path = Path("/home/ubuntu/repo_policy_compliance_service")
repo_check_web_service_script = Path("templates/repo_policy_compliance_service.py")
repo_check_web_service_script = Path("scripts/repo_policy_compliance_service.py")
repo_check_systemd_service = Path("/etc/systemd/system/repo-policy-compliance.service")
ram_pool_path = Path("/storage/ram")

Expand Down Expand Up @@ -324,8 +324,6 @@ def _on_install(self, _event: InstallEvent) -> None:
"""
self.unit.status = MaintenanceStatus("Installing packages")

self._update_kernel()
yhaliaw marked this conversation as resolved.
Show resolved Hide resolved

try:
# The `_start_services`, `_install_deps` includes retry.
self._install_deps()
Expand Down Expand Up @@ -370,6 +368,8 @@ def _on_start(self, _event: StartEvent) -> None:
Args:
event: Event of starting the charm.
"""
self._check_and_update_dependencies()

runner_manager = self._get_runner_manager()

self.unit.status = MaintenanceStatus("Starting runners")
Expand Down Expand Up @@ -672,6 +672,9 @@ def _reconcile_runners(self, runner_manager: RunnerManager) -> Dict[str, Any]:
delta_virtual_machines = runner_manager.reconcile(
virtual_machines, virtual_machines_resources
)

self.unit.status = ActiveStatus()

yhaliaw marked this conversation as resolved.
Show resolved Hide resolved
return {"delta": {"virtual-machines": delta_virtual_machines}}

def _install_repo_policy_compliance(self) -> bool:
Expand Down
7 changes: 6 additions & 1 deletion src/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -796,4 +796,9 @@ def _snap_install(self, snaps: Iterable[Snap]) -> None:
cmd = ["snap", "install", snap.name, f"--channel={snap.channel}"]
if snap.revision is not None:
cmd.append(f"--revision={snap.revision}")
self.instance.execute(cmd)
exit_code, stdout, stderr = self.instance.execute(cmd)

if exit_code != 0:
err_msg = stderr.read().decode("utf-8")
logger.error("Unable to install %s due to %s", snap.name, err_msg)
raise RunnerCreateError(f"Unable to install {snap.name}")
11 changes: 8 additions & 3 deletions src/runner_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -627,12 +627,14 @@ def _build_image_command(self) -> list[str]:
"""
http_proxy = self.proxies.get("http", "")
https_proxy = self.proxies.get("https", "")
no_proxy = self.proxies.get("no_proxy", "")

cmd = [
"/usr/bin/bash",
BUILD_IMAGE_SCRIPT_FILENAME,
http_proxy,
https_proxy,
no_proxy,
]
if LXD_PROFILE_YAML.exists():
cmd += ["test"]
Expand All @@ -650,13 +652,16 @@ def build_runner_image(self) -> None:

def schedule_build_runner_image(self) -> None:
"""Install cron job for building runner image."""
# Replace empty string in the build image command list and form a string.
build_image_command = " ".join(
[part if part else "''" for part in self._build_image_command()]
)

cron_file = self.cron_path / "build-runner-image"
# Randomized the time executing the building of image to prevent all instances of the charm
# building images at the same time, using up the disk, and network IO of the server.
# The random number are not used for security purposes.
minute = random.randint(0, 59) # nosec B311
base_hour = random.randint(0, 5) # nosec B311
hours = ",".join([str(base_hour + offset) for offset in (0, 6, 12, 18)])
cron_file.write_text(
f"{minute} {hours} * * * ubuntu {' '.join(self._build_image_command())}"
)
cron_file.write_text(f"{minute} {hours} * * * ubuntu {build_image_command}")
11 changes: 10 additions & 1 deletion tests/integration/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,21 +197,30 @@ async def run_in_unit(unit: Unit, command: str, timeout=None) -> tuple[int, str


async def run_in_lxd_instance(
unit: Unit, name: str, command: str, cwd=None, timeout=None
unit: Unit,
name: str,
command: str,
env: dict[str, str] | None = None,
cwd: str | None = None,
timeout: int | None = None,
) -> tuple[int, str | None]:
"""Run command in LXD instance of a juju unit.

Args:
unit: Juju unit to execute the command in.
name: Name of LXD instance.
command: Command to execute.
env: Mapping of environment variable name to value.
cwd: Work directory of the command.
timeout: Amount of time to wait for the execution.

Returns:
Tuple of return code and stdout.
"""
lxc_cmd = f"/snap/bin/lxc exec {name}"
if env:
for key, value in env.items():
lxc_cmd += f"--env {key}={value}"
if cwd:
lxc_cmd += f" --cwd {cwd}"
lxc_cmd += f" -- {command}"
Expand Down
81 changes: 74 additions & 7 deletions tests/integration/test_charm_with_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@

"""Test the usage of a proxy server."""
import subprocess
from asyncio import sleep
from typing import AsyncIterator
from urllib.parse import urlparse

import pytest
import pytest_asyncio
Expand All @@ -15,6 +17,7 @@
get_runner_names,
run_in_lxd_instance,
)
from tests.status_name import ACTIVE_STATUS_NAME
from utilities import execute_command

PROXY_PORT = 8899
Expand Down Expand Up @@ -44,22 +47,81 @@ async def proxy_fixture() -> AsyncIterator[str]:

@pytest_asyncio.fixture(scope="module", name="app_with_aproxy")
async def app_with_aproxy_fixture(
model: Model, app_no_runner: Application, proxy: str
model: Model,
charm_file: str,
app_name: str,
path: str,
token: str,
proxy: str,
) -> Application:
"""Application configured to use aproxy"""

"""Application with aproxy setup and firewall to block all other network access."""
await model.set_config(
{
"apt-http-proxy": proxy,
"apt-https-proxy": proxy,
"apt-no-proxy": "127.0.0.1,localhost,::1",
"juju-http-proxy": proxy,
"juju-https-proxy": proxy,
"juju-no-proxy": "",
"juju-no-proxy": "127.0.0.1,localhost,::1",
"snap-http-proxy": proxy,
"snap-https-proxy": proxy,
"snap-no-proxy": "127.0.0.1,localhost,::1",
"logging-config": "<root>=INFO;unit=DEBUG",
}
)

await app_no_runner.set_config({"experimental-use-aproxy": "true"})
machine = await model.add_machine(constraints={"root-disk": 15}, series="jammy")
# Wait until juju agent has the hostname of the machine.
for _ in range(120):
if machine.hostname is not None:
break
await sleep(10)
else:
assert False, "Timeout waiting for machine to start"

# Disable external network access for the juju machine.
proxy_url = urlparse(proxy)
await machine.ssh(f"sudo iptables -I OUTPUT -d {proxy_url.hostname} -j ACCEPT")
await machine.ssh("sudo iptables -I OUTPUT -d 0.0.0.0/8 -j ACCEPT")
await machine.ssh("sudo iptables -I OUTPUT -d 10.0.0.0/8 -j ACCEPT")
await machine.ssh("sudo iptables -I OUTPUT -d 100.64.0.0/10 -j ACCEPT")
await machine.ssh("sudo iptables -I OUTPUT -d 127.0.0.0/8 -j ACCEPT")
await machine.ssh("sudo iptables -I OUTPUT -d 169.254.0.0/16 -j ACCEPT")
await machine.ssh("sudo iptables -I OUTPUT -d 172.16.0.0/12 -j ACCEPT")
await machine.ssh("sudo iptables -I OUTPUT -d 192.0.0.0/24 -j ACCEPT")
await machine.ssh("sudo iptables -I OUTPUT -d 192.0.2.0/24 -j ACCEPT")
await machine.ssh("sudo iptables -I OUTPUT -d 192.88.99.0/24 -j ACCEPT")
await machine.ssh("sudo iptables -I OUTPUT -d 192.168.0.0/16 -j ACCEPT")
await machine.ssh("sudo iptables -I OUTPUT -d 198.18.0.0/15 -j ACCEPT")
await machine.ssh("sudo iptables -I OUTPUT -d 198.51.100.0/24 -j ACCEPT")
await machine.ssh("sudo iptables -I OUTPUT -d 203.0.113.0/24 -j ACCEPT")
await machine.ssh("sudo iptables -I OUTPUT -d 224.0.0.0/4 -j ACCEPT")
await machine.ssh("sudo iptables -I OUTPUT -d 233.252.0.0/24 -j ACCEPT")
await machine.ssh("sudo iptables -I OUTPUT -d 240.0.0.0/4 -j ACCEPT")
await machine.ssh("sudo iptables -P OUTPUT DROP")
# Test the external network access is disabled.
await machine.ssh("ping -c1 canonical.com 2>&1 | grep '100% packet loss'")
yhaliaw marked this conversation as resolved.
Show resolved Hide resolved

# Deploy the charm in the juju machine with external network access disabled.
application = await model.deploy(
charm_file,
application_name=app_name,
series="jammy",
config={
"path": path,
"token": token,
"virtual-machines": 1,
"denylist": "10.10.0.0/16",
"test-mode": "insecure",
"reconcile-interval": 60,
"experimental-use-aproxy": "true",
},
constraints={"root-disk": 15},
to=machine.id,
)
await model.wait_for_idle(status=ACTIVE_STATUS_NAME, timeout=60 * 60)

return app_no_runner
return application


@pytest.mark.asyncio
Expand All @@ -76,7 +138,12 @@ async def test_usage_of_aproxy(model: Model, app_with_aproxy: Application) -> No
assert names
runner_name = names[0]

return_code, stdout = await run_in_lxd_instance(unit, runner_name, "curl http://canonical.com")
return_code, stdout = await run_in_lxd_instance(
unit,
runner_name,
"curl http://canonical.com",
)

assert return_code == 0

return_code, stdout = await run_in_lxd_instance(
Expand Down
Loading