|
3 | 3 | # See LICENSE file for licensing details
|
4 | 4 | #
|
5 | 5 |
|
| 6 | +import base64 |
6 | 7 | import logging
|
| 8 | +import os |
| 9 | +import pathlib |
| 10 | +import subprocess |
7 | 11 |
|
| 12 | +import pytest |
| 13 | +import yaml |
8 | 14 | from k8s_test_harness import harness
|
9 |
| -from k8s_test_harness.util import env_util |
| 15 | +from k8s_test_harness.util import constants, env_util, k8s_util |
10 | 16 |
|
11 | 17 | LOG = logging.getLogger(__name__)
|
12 | 18 |
|
| 19 | +DIR = pathlib.Path(__file__).absolute().parent |
| 20 | +TEMPLATES_DIR = DIR / ".." / "templates" |
| 21 | +BASE_DIR = DIR / ".." / ".." |
| 22 | +MANIFESTS_DIR = BASE_DIR / "manifests" |
13 | 23 |
|
14 |
| -def test_integration_mutating_pebble_webhook(function_instance: harness.Instance): |
| 24 | +PEBBLE_ENV = "PEBBLE" |
| 25 | +PEBBLE_ENV_COPY_ONCE = "PEBBLE_COPY_ONCE" |
| 26 | + |
| 27 | +PEBBLE_VOLUME_NAME = "pebble-dir" |
| 28 | +PEBBLE_DEFAULT_DIR = "/var/lib/pebble/default" |
| 29 | +PEBBLE_WRITABLE_SUBPATH = "writable" |
| 30 | +PEBBLE_DEFAULT_WRITABLE_DIR = os.path.join(PEBBLE_DEFAULT_DIR, PEBBLE_WRITABLE_SUBPATH) |
| 31 | + |
| 32 | + |
| 33 | +@pytest.fixture(scope="module") |
| 34 | +def webhook_instance(module_instance: harness.Instance): |
15 | 35 | rock = env_util.get_build_meta_info_for_rock_version(
|
16 | 36 | "mutating-pebble-webhook", "0.0.1", "amd64"
|
17 | 37 | )
|
18 | 38 |
|
19 |
| - LOG.info(f"Using rock: {rock.image}") |
20 |
| - LOG.warn("Integration tests are not yet implemented yet") |
| 39 | + # Generate certificates needed by webhook. |
| 40 | + for target in ["generate-selfsigned-cert", "set-webhook-cabundle"]: |
| 41 | + make_command = [ |
| 42 | + "make", |
| 43 | + "-C", |
| 44 | + BASE_DIR, |
| 45 | + target, |
| 46 | + ] |
| 47 | + subprocess.run(make_command, check=True) |
| 48 | + |
| 49 | + # Some specs need to be updated before applying. |
| 50 | + server_cert = pathlib.Path(BASE_DIR / "tls" / "server.crt").read_bytes() |
| 51 | + server_key = pathlib.Path(BASE_DIR / "tls" / "server.key").read_bytes() |
| 52 | + file_updates = { |
| 53 | + "webhook-deployment.yaml": { |
| 54 | + # what_to_update: new_value. |
| 55 | + "ghcr.io/canonical/mutating-pebble-webhook": f"{rock.image} #", |
| 56 | + }, |
| 57 | + "webhook-secret.yaml": { |
| 58 | + "tls.crt": f"tls.crt: {base64.b64encode(server_cert).decode()} #", |
| 59 | + "tls.key": f"tls.key: {base64.b64encode(server_key).decode()} #", |
| 60 | + }, |
| 61 | + } |
| 62 | + |
| 63 | + # Apply updates if needed, and deploy webhook. |
| 64 | + for filename in [ |
| 65 | + "webhook-ns.yaml", |
| 66 | + "webhook-secret.yaml", |
| 67 | + "webhook-deployment.yaml", |
| 68 | + "webhook-svc.yaml", |
| 69 | + "webhook.yaml", |
| 70 | + ]: |
| 71 | + spec = pathlib.Path(MANIFESTS_DIR / filename).read_text() |
| 72 | + if filename in file_updates: |
| 73 | + for old, new in file_updates[filename].items(): |
| 74 | + spec = spec.replace(old, new, 1) |
| 75 | + |
| 76 | + module_instance.exec( |
| 77 | + ["k8s", "kubectl", "apply", "-f", "-"], |
| 78 | + input=spec.encode(), |
| 79 | + ) |
| 80 | + |
| 81 | + k8s_util.wait_for_deployment( |
| 82 | + module_instance, "mutating-pebble-webhook", "pebble-webhook" |
| 83 | + ) |
| 84 | + |
| 85 | + yield module_instance |
| 86 | + |
| 87 | + |
| 88 | +def _apply_pod(instance: harness.Instance, spec_filename: str): |
| 89 | + """Applies the given Pod spec and waits for it to become Ready/ |
| 90 | +
|
| 91 | + Returns the new Pod spec. |
| 92 | + """ |
| 93 | + # NOTE(claudiub): pod names should be unique, as we're not cleaning them up anyways. |
| 94 | + |
| 95 | + spec = pathlib.Path(TEMPLATES_DIR / spec_filename).read_text() |
| 96 | + spec_yaml = yaml.safe_load(spec) |
| 97 | + pod_name = spec_yaml["metadata"]["name"] |
| 98 | + |
| 99 | + instance.exec( |
| 100 | + ["k8s", "kubectl", "apply", "-f", "-"], |
| 101 | + input=spec.encode(), |
| 102 | + ) |
| 103 | + |
| 104 | + k8s_util.wait_for_resource( |
| 105 | + instance, |
| 106 | + "pod", |
| 107 | + pod_name, |
| 108 | + condition=constants.K8S_CONDITION_READY, |
| 109 | + retry_delay_s=10, |
| 110 | + ) |
| 111 | + |
| 112 | + # Get the Pod yaml spec and return it. |
| 113 | + process = instance.exec( |
| 114 | + ["k8s", "kubectl", "get", "-o", "yaml", "pod", pod_name], |
| 115 | + capture_output=True, |
| 116 | + ) |
| 117 | + |
| 118 | + return yaml.safe_load(process.stdout) |
| 119 | + |
| 120 | + |
| 121 | +def test_webhook_noop(webhook_instance: harness.Instance): |
| 122 | + pod = _apply_pod(webhook_instance, "pod-noop.yaml") |
| 123 | + |
| 124 | + # The pod should not have any environment variables, and no "pebble-dir" volume. |
| 125 | + assert "env" not in pod["spec"]["containers"][0] |
| 126 | + |
| 127 | + volume_names = [vol["name"] for vol in pod["spec"]["volumes"]] |
| 128 | + assert PEBBLE_VOLUME_NAME not in volume_names |
| 129 | + |
| 130 | + |
| 131 | +def test_webhook_already_has_mount(webhook_instance: harness.Instance): |
| 132 | + pod = _apply_pod(webhook_instance, "pod-has-mount.yaml") |
| 133 | + container = pod["spec"]["containers"][0] |
| 134 | + |
| 135 | + # The pod already has a mount in the PEBBLE default path, and the webhook should have |
| 136 | + # skipped processing it. It should not have any environment variables, and no |
| 137 | + # "pebble-dir" volume. |
| 138 | + assert "env" not in container |
| 139 | + |
| 140 | + # Sanity check, make sure we do have a mount. |
| 141 | + volume_mounts = [mount["mountPath"] for mount in container["volumeMounts"]] |
| 142 | + assert PEBBLE_DEFAULT_DIR in volume_mounts |
| 143 | + |
| 144 | + volume_names = [vol["name"] for vol in pod["spec"]["volumes"]] |
| 145 | + assert PEBBLE_VOLUME_NAME not in volume_names |
| 146 | + |
| 147 | + |
| 148 | +def test_webhook_mixed_containers(webhook_instance: harness.Instance): |
| 149 | + pod = _apply_pod(webhook_instance, "pod-mixed.yaml") |
| 150 | + |
| 151 | + # The normal container should not have any env vars set, and no volume mounts for Pebble. |
| 152 | + container = pod["spec"]["containers"][0] |
| 153 | + assert "env" not in container |
| 154 | + |
| 155 | + volume_mounts = [mount["name"] for mount in container["volumeMounts"]] |
| 156 | + assert PEBBLE_VOLUME_NAME not in volume_mounts |
| 157 | + |
| 158 | + # The read-only container should have the PEBBLE and PEBBLE_COPY_ONCE env vars set. |
| 159 | + container = pod["spec"]["containers"][1] |
| 160 | + env = [e["value"] for e in container["env"] if e["name"] == PEBBLE_ENV] |
| 161 | + assert len(env) == 1 and env[0] == PEBBLE_DEFAULT_WRITABLE_DIR |
| 162 | + |
| 163 | + env = [e["value"] for e in container["env"] if e["name"] == PEBBLE_ENV_COPY_ONCE] |
| 164 | + assert len(env) == 1 and env[0] == PEBBLE_DEFAULT_DIR |
| 165 | + |
| 166 | + # The read-only container should have the volume mount. |
| 167 | + volume_mounts = [mount["name"] for mount in container["volumeMounts"]] |
| 168 | + assert PEBBLE_VOLUME_NAME in volume_mounts |
| 169 | + |
| 170 | + # Redundant check, the Pod should have the volume. |
| 171 | + volume_names = [vol["name"] for vol in pod["spec"]["volumes"]] |
| 172 | + assert PEBBLE_VOLUME_NAME in volume_names |
| 173 | + |
| 174 | + |
| 175 | +def test_webhook_update_envs(webhook_instance: harness.Instance): |
| 176 | + pod = _apply_pod(webhook_instance, "pod-update-envs.yaml") |
| 177 | + container = pod["spec"]["containers"][0] |
| 178 | + overwritten_pebble_path = "/var/lib/foo/lish" |
| 179 | + |
| 180 | + # The container should have updated PEBBLE and PEBBLE_COPY_ONCE env vars. |
| 181 | + env = [e["value"] for e in container["env"] if e["name"] == PEBBLE_ENV] |
| 182 | + assert len(env) == 1 and env[0] == os.path.join( |
| 183 | + overwritten_pebble_path, PEBBLE_WRITABLE_SUBPATH |
| 184 | + ) |
| 185 | + |
| 186 | + env = [e["value"] for e in container["env"] if e["name"] == PEBBLE_ENV_COPY_ONCE] |
| 187 | + assert len(env) == 1 and env[0] == overwritten_pebble_path |
| 188 | + |
| 189 | + # The read-only container should have the volume mount. |
| 190 | + volume_mounts = [mount["name"] for mount in container["volumeMounts"]] |
| 191 | + assert PEBBLE_VOLUME_NAME in volume_mounts |
| 192 | + |
| 193 | + # Redundant check, the Pod should have the volume. |
| 194 | + volume_names = [vol["name"] for vol in pod["spec"]["volumes"]] |
| 195 | + assert PEBBLE_VOLUME_NAME in volume_names |
| 196 | + |
| 197 | + |
| 198 | +def test_webhook_rock(webhook_instance: harness.Instance): |
| 199 | + pod = _apply_pod(webhook_instance, "pod-rock.yaml") |
| 200 | + |
| 201 | + # The container has "readOnlyRootFilesystem=true". Without the webhook, Pebble wouldn't |
| 202 | + # be able to start because it cannot create the files it needs. |
| 203 | + # Get the Pod logs. Pebble should now be able to start properly and start the service. |
| 204 | + process = webhook_instance.exec( |
| 205 | + ["k8s", "kubectl", "logs", pod["metadata"]["name"]], |
| 206 | + capture_output=True, |
| 207 | + text=True, |
| 208 | + ) |
| 209 | + |
| 210 | + assert 'Service "mutating-pebble-webhook" starting:' in process.stdout |
0 commit comments