postjevsql.git / nix / sidecar-image-test.nix
1# The sidecar image booted in docker inside a VM (flake check
2# `sidecar-image`), against the loopback target of nix/test-target.nix,
3# on the host network. It checks what the image promises:
4#
5# - `CREATE EXTENSION postjevsql` has run on the sidecar;
6# - the target's tables are read over verify-full TLS, with the
7#   mapping's password from a mounted file;
8# - a jev call over a foreign table plans as the scan over a ForeignScan;
9# - starting the container again applies a target migration;
10# - a secret given both as a file and as an env var stops the container
11#   with the CLI's refusal;
12# - the image holds no secret and no identity.
13{ pkgs, image }:
14let
15  target = import ./test-target.nix { inherit pkgs; };
16  config = pkgs.writeText "config.toml" ''
17    [target]
18    server = "target"
19    host = "localhost"
20    port = 5433
21    dbname = "appdb"
22    sslrootcert = "/run/secrets/ca.crt"
23    schemas = ["public"]
24
25    [[mapping]]
26    local_user = "postgres"
27    remote_user = "app"
28    password_file = "/run/secrets/target-password"
29
30    [jev]
31    api_key_file = "/run/secrets/typesafe-key"
32  '';
33in
34pkgs.testers.runNixOSTest {
35  name = "postjevsql-sidecar-image";
36
37  nodes.machine = {
38    imports = [ target.module ];
39    virtualisation.memorySize = 2048;
40    virtualisation.diskSize = 4096;
41    virtualisation.docker.enable = true;
42  };
43
44  testScript = ''
45    from shlex import quote
46
47    run = (
48        "docker run -d --name sidecar --network host"
49        " -v /secrets:/run/secrets:ro -v ${config}:/etc/postjevsql-sidecar/config.toml:ro"
50        " -v sidecar-data:/var/lib/postgresql"
51        " postjevsql-sidecar:${image.imageTag} -- -c jev.model=jev-1.13.0 -c port=5432"
52    )
53
54    def sidecar(sql):
55        return machine.succeed(
56            f"docker exec sidecar psql --no-psqlrc -v ON_ERROR_STOP=1 -At -h /run/postgresql -U postgres -d postgres -c {quote(sql)}"
57        ).strip()
58
59    def target(sql):
60        return machine.succeed(
61            f"sudo -u postgres ${target.psql} -At -d appdb -c {quote(sql)}"
62        ).strip()
63
64    # serve starts postgres once to converge and sync, stops it, then
65    # execs it: ready is the second start, after the sync's output.
66    def ready():
67        machine.wait_until_succeeds(
68            "docker logs sidecar 2>&1 | grep -Eq 'applied|in sync' "
69            "&& [ $(docker logs sidecar 2>&1 | grep -c 'database system is ready to accept connections') -ge 2 ] "
70            "&& docker exec sidecar psql -h /run/postgresql -U postgres -d postgres -c 'SELECT 1'",
71            timeout=180,
72        )
73
74    machine.wait_for_unit("pg-target.service")
75    machine.wait_for_unit("docker.service")
76    machine.succeed(
77        "mkdir -p /secrets",
78        "cp ${target.certs}/ca.crt /secrets/ca.crt",
79        "printf %s ${target.password} > /secrets/target-password",
80        "printf %s test-key-never-sent > /secrets/typesafe-key",
81        "chmod 0644 /secrets/*",
82    )
83
84    with subtest("the tarball loads with docker load"):
85        machine.succeed("docker load < ${image}")
86
87    with subtest("the image holds no secret and no identity"):
88        # The layers are uncompressed tars inside, so grep reads their contents.
89        machine.succeed("mkdir /img && tar -xf ${image} -C /img")
90        machine.fail("grep -rq ${target.password} /img")
91        machine.fail("grep -rq test-key-never-sent /img")
92        env = machine.succeed("docker inspect -f '{{json .Config.Env}} {{.Author}}' postjevsql-sidecar:${image.imageTag}")
93        assert "TYPESAFE" not in env and "PASSWORD" not in env, env
94
95    with subtest("a secret set as a file and an env var stops the container with the refusal"):
96        out = machine.fail(run.replace("run -d --name sidecar", "run --rm -e TYPESAFE_API_KEY=x") + " 2>&1")
97        assert "is set twice" in out, out
98        assert "TYPESAFE_API_KEY" in out, out
99
100    machine.succeed(run)
101    ready()
102
103    with subtest("the extension is installed on the sidecar"):
104        installed = sidecar("SELECT string_agg(extname, ',' ORDER BY extname) FROM pg_extension")
105        assert "postjevsql" in installed.split(","), installed
106        assert "postgres_fdw" in installed.split(","), installed
107
108    with subtest("the target's tables are read over verify-full TLS"):
109        options = sidecar("SELECT array_to_string(srvoptions, ' ') FROM pg_foreign_server WHERE srvname = 'target'")
110        assert "sslmode=verify-full" in options, options
111        rows = sidecar("SELECT string_agg(id || ':' || body, ';' ORDER BY id) FROM public.tickets")
112        assert rows == "1:server down;2:typo on page", rows
113
114    with subtest("a jev call over a foreign table plans as the scan over a ForeignScan"):
115        plan = sidecar("EXPLAIN (COSTS OFF) SELECT id FROM public.tickets t WHERE jev(t, 'Urgent?')")
116        assert "Custom Scan" in plan, plan
117        assert "Foreign Scan" in plan, plan
118
119    with subtest("starting the container again applies a target migration"):
120        target("ALTER TABLE public.tickets ADD COLUMN IF NOT EXISTS priority int")
121        machine.succeed("docker stop sidecar && docker start sidecar")
122        machine.wait_until_succeeds(
123            "docker exec sidecar psql -At -h /run/postgresql -U postgres -d postgres -c "
124            + quote("SELECT string_agg(attname, ',' ORDER BY attnum) FROM pg_attribute WHERE attrelid = 'public.tickets'::regclass AND attnum > 0")
125            + " | grep -qx id,body,priority",
126            timeout=180,
127        )
128  '';
129}