Skip to content

Commit 79d281e

Browse files
committed
[IMP] Build Multiarch images in CI
The ultimate goal of this commit is to support building arm64, but I wound up changing a few different things along the way as well. First of all, we build the image for both architectures in one step, but then we have to push it to a local registry in order to pull each architecture and test. Then, once the tests have passed, we re-tag the image or manifest from the local registry and push it to ghcr and Dockerhub (if Dockerhub credentials exist). Not essential to this task, but very beneficial for myself and any future contributors, I have made the whole thing repo agnostic, so anyone can just fork and test, no CI customization required.
1 parent fbffb62 commit 79d281e

4 files changed

Lines changed: 115 additions & 57 deletions

File tree

.github/workflows/ci.yaml

Lines changed: 41 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,17 @@ jobs:
2525
- uses: pre-commit/action@v1.0.1
2626

2727
build-test-push:
28+
if: github.ref == format('refs/heads/{0}', github.event.repository.default_branch) || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository)
2829
runs-on: ubuntu-latest
2930
needs: pre-commit
31+
permissions:
32+
contents: read
33+
packages: write
34+
services:
35+
registry:
36+
image: registry:3
37+
ports:
38+
- 5000:5000
3039
strategy:
3140
fail-fast: false
3241
matrix:
@@ -42,43 +51,55 @@ jobs:
4251
- "10"
4352
- "9.6"
4453
env:
54+
DOCKER_PLATFORM: linux/amd64,linux/arm64
55+
LOCAL_REGISTRY: localhost:5000
4556
# Indicates what's the equivalent to tecnativa/postgres-autoconf:latest image
4657
LATEST_RELEASE: "18-alpine"
47-
DOCKER_REPO: tecnativa/postgres-autoconf
48-
DOCKER_TAG: ${{ matrix.pg_version }}-alpine
58+
DOCKER_REPO: ${{ github.repository == 'Tecnativa/docker-postgres-autoconf' && 'tecnativa/postgres-autoconf' || github.repository }}
59+
BASE_TAG: ${{ matrix.pg_version }}-alpine
60+
DOCKER_TAG: ${{ github.event_name == 'pull_request' && format('{0}-test-pr{1}', matrix.pg_version, github.event.number) || format('{0}-alpine', matrix.pg_version) }}
4961
GIT_SHA1: ${{ github.sha }}
50-
IS_PR: ${{ github.event_name == 'pull_request' }}
62+
# Github does not allow evaluating a secret in an if condition, so we need to set them as environment variables
63+
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
64+
DOCKERHUB_LOGIN: ${{ secrets.DOCKERHUB_LOGIN }}
5165
steps:
66+
# Image repo names have to be lowercase.
67+
- name: Lowercase image repository name
68+
run: |
69+
DOCKER_REPO=${DOCKER_REPO,,}
70+
echo "DOCKER_REPO=$DOCKER_REPO" >> "$GITHUB_ENV"
5271
- uses: actions/checkout@v4
5372
- uses: actions/setup-python@v6
5473
- run: pip install -r tests/ci-requirements.txt
5574

75+
- name: Set up QEMU
76+
uses: docker/setup-qemu-action@v4
77+
- name: Set up Docker Buildx
78+
uses: docker/setup-buildx-action@v4
79+
with:
80+
driver-opts: network=host
5681
# Build images
57-
- run: ./hooks/build
82+
- name: Build images
83+
run: ./hooks/build
5884
# Test
59-
- run: python -m unittest tests.test -v
60-
- name: Set Docker Tag
85+
- name: Test each platform
6186
run: |
62-
if [ "${{ env.IS_PR }}" = "true" ]; then
63-
echo "DOCKER_TAG=${{ matrix.pg_version }}-test-pr${{ github.event.number }}" >> $GITHUB_ENV
64-
else
65-
echo "DOCKER_TAG=${{ matrix.pg_version }}-alpine" >> $GITHUB_ENV
66-
fi
67-
- name: Tag Docker Image for PR
68-
if: env.IS_PR
69-
run: docker tag ${{ env.DOCKER_REPO }}:${{ matrix.pg_version }}-alpine ${{ env.DOCKER_REPO }}:${{ env.DOCKER_TAG }}
87+
IFS=',' read -ra PLATFORMS <<< "$DOCKER_PLATFORM"
88+
for platform in "${PLATFORMS[@]}"; do
89+
echo "Testing platform: $platform"
90+
TEST_PLATFORM="$platform" python -m unittest tests.test -v
91+
done
7092
# Push
7193
- name: Push Docker Image to Docker Hub
72-
if: github.repository == 'Tecnativa/docker-postgres-autoconf' && (github.ref == 'refs/heads/master' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository))
94+
if: env.DOCKERHUB_TOKEN && env.DOCKERHUB_LOGIN
7395
env:
7496
REGISTRY_HOST: docker.io
75-
REGISTRY_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
76-
REGISTRY_USERNAME: ${{ secrets.DOCKERHUB_LOGIN }}
97+
REGISTRY_TOKEN: ${{ env.DOCKERHUB_TOKEN }}
98+
REGISTRY_USERNAME: ${{ env.DOCKERHUB_LOGIN }}
7799
run: ./hooks/push
78100
- name: Push Docker Image to GitHub Registry
79-
if: github.repository == 'Tecnativa/docker-postgres-autoconf' && (github.ref == 'refs/heads/master' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository))
80101
env:
81102
REGISTRY_HOST: ghcr.io
82-
REGISTRY_TOKEN: ${{ secrets.BOT_TOKEN }}
83-
REGISTRY_USERNAME: ${{ secrets.BOT_LOGIN }}
103+
REGISTRY_TOKEN: ${{ secrets.BOT_TOKEN || secrets.GITHUB_TOKEN }}
104+
REGISTRY_USERNAME: ${{ secrets.BOT_LOGIN || github.repository_owner }}
84105
run: ./hooks/push

hooks/build

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,42 @@ from plumbum import FG, local
33
from plumbum.cmd import date, docker
44

55
# Check environment variables are present
6+
DOCKER_PLATFORM = local.env.get("DOCKER_PLATFORM", "linux/amd64")
67
DOCKER_TAG = local.env["DOCKER_TAG"]
8+
BASE_TAG = local.env.get("BASE_TAG", DOCKER_TAG)
9+
REPO = local.env["DOCKER_REPO"]
710
COMMIT = local.env.get("GIT_SHA1")
811
DATE = date("--rfc-3339", "ns")
9-
10-
# Build image
11-
(
12-
docker[
13-
"image",
14-
"build",
15-
"--build-arg",
16-
"VCS_REF={}".format(COMMIT),
17-
"--build-arg",
18-
"BUILD_DATE={}".format(DATE),
19-
"--build-arg",
20-
"BASE_TAG={}".format(DOCKER_TAG),
21-
"--tag",
22-
"tecnativa/postgres-autoconf:{}".format(DOCKER_TAG),
23-
".",
24-
]
25-
& FG
12+
LOCAL_REGISTRY = local.env.get("LOCAL_REGISTRY")
13+
IMAGE = (
14+
"%s/%s:%s" % (LOCAL_REGISTRY, REPO, DOCKER_TAG)
15+
if LOCAL_REGISTRY
16+
else "%s:%s" % (REPO, DOCKER_TAG)
2617
)
18+
PLATFORMS = [p.strip() for p in DOCKER_PLATFORM.split(",") if p.strip()]
19+
20+
build = docker[
21+
"buildx",
22+
"build",
23+
"--platform",
24+
DOCKER_PLATFORM,
25+
"--build-arg",
26+
"VCS_REF={}".format(COMMIT),
27+
"--build-arg",
28+
"BUILD_DATE={}".format(DATE),
29+
"--build-arg",
30+
"BASE_TAG={}".format(BASE_TAG),
31+
"--tag",
32+
IMAGE,
33+
".",
34+
]
35+
if LOCAL_REGISTRY:
36+
build = build["--push"]
37+
elif len(PLATFORMS) == 1:
38+
build = build["--load"]
39+
else:
40+
raise SystemExit(
41+
"Multi-platform builds require LOCAL_REGISTRY; "
42+
"set DOCKER_PLATFORM to one value for local --load builds."
43+
)
44+
(build & FG)

hooks/push

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,20 @@ REPO = local.env["DOCKER_REPO"]
77
SUFFIX = local.env.get("DOCKER_REPO_SUFFIX", "")
88
VERSION = local.env["DOCKER_TAG"]
99

10-
# Log all locally available images; will help to pin images
11-
docker["image", "ls", "--digests", REPO] & FG
1210

13-
# Login in Docker Hub
11+
def image_ref(registry, tag):
12+
return "%s/%s%s:%s" % (registry, REPO, SUFFIX, tag)
13+
14+
15+
source = image_ref(local.env["LOCAL_REGISTRY"], VERSION)
16+
dest_tags = [image_ref(REGISTRY, VERSION)]
17+
if VERSION == local.env.get("LATEST_RELEASE"):
18+
latest = "alpine" if VERSION.endswith("-alpine") else "latest"
19+
dest_tags.append(image_ref(REGISTRY, latest))
20+
21+
docker["buildx", "imagetools", "inspect", source] & FG
22+
23+
# Login in Docker Hub or ghcr
1424
docker(
1525
"login",
1626
"--username",
@@ -20,13 +30,7 @@ docker(
2030
REGISTRY,
2131
)
2232

23-
# Push built images
24-
local_image = "%s:%s" % (REPO, VERSION)
25-
public_image = "%s/%s%s:%s" % (REGISTRY, REPO, SUFFIX, VERSION)
26-
docker["image", "tag", local_image, public_image] & FG
27-
docker["image", "push", public_image] & FG
28-
if VERSION == local.env.get("LATEST_RELEASE"):
29-
latest_version = "alpine" if VERSION.endswith("-alpine") else "latest"
30-
public_image = "%s/%s%s:%s" % (REGISTRY, REPO, SUFFIX, latest_version)
31-
docker["image", "tag", local_image, public_image] & FG
32-
docker["image", "push", public_image] & FG
33+
promote = docker["buildx", "imagetools", "create"]
34+
for dest in dest_tags:
35+
promote = promote["-t", dest]
36+
(promote[source] & FG)

tests/test.py

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,19 @@
2222
class PostgresAutoconfCase(unittest.TestCase):
2323
"""Test behavior for this docker image"""
2424

25+
@classmethod
26+
def _platform_args(cls):
27+
platform = os.environ.get("TEST_PLATFORM")
28+
if platform:
29+
return ("--platform", platform)
30+
return ()
31+
2532
@classmethod
2633
def setUpClass(cls):
27-
with local.cwd(local.cwd / ".."):
28-
print("Building image")
29-
local["./hooks/build"] & FG
30-
cls.image = f"tecnativa/postgres-autoconf:{local.env['DOCKER_TAG']}"
34+
registry = local.env.get("LOCAL_REGISTRY")
35+
repo = local.env.get("DOCKER_REPO", "tecnativa/postgres-autoconf")
36+
tag = local.env["DOCKER_TAG"]
37+
cls.image = f"{registry}/{repo}:{tag}" if registry else f"{repo}:{tag}"
3138
cls.cert_files = ("client.ca.cert.pem", "server.cert.pem", "server.key.pem")
3239
return super().setUpClass()
3340

@@ -56,7 +63,7 @@ def _check_local_connection(self):
5663
# The 1st test could fail while postgres boots
5764
for attempt in range(10):
5865
try:
59-
time.sleep(5)
66+
time.sleep(15)
6067
# Test local connections via unix socket work
6168
self.assertEqual(
6269
"1\n",
@@ -75,13 +82,13 @@ def _check_local_connection(self):
7582
"test_user",
7683
),
7784
)
78-
except AssertionError:
85+
except (AssertionError, ProcessExecutionError):
7986
if attempt < 9:
8087
print("Failure number {}. Retrying...".format(attempt))
8188
else:
8289
raise
8390
else:
84-
continue
91+
return
8592

8693
def _check_password_auth(self, host=None):
8794
"""Test connection with password auth work fine."""
@@ -93,6 +100,7 @@ def _check_password_auth(self, host=None):
93100
docker(
94101
"container",
95102
"run",
103+
*self._platform_args(),
96104
"--network",
97105
"lan",
98106
"-e",
@@ -126,6 +134,7 @@ def _check_cert_auth(self):
126134
docker(
127135
"container",
128136
"run",
137+
*self._platform_args(),
129138
"--network",
130139
"wan",
131140
"-e",
@@ -163,6 +172,7 @@ def test_server_certs_var(self):
163172
self.postgres_container = docker(
164173
"container",
165174
"run",
175+
*self._platform_args(),
166176
"-d",
167177
"--network",
168178
"lan",
@@ -198,6 +208,7 @@ def test_server_certs_mount(self):
198208
self.postgres_container = docker(
199209
"container",
200210
"run",
211+
*self._platform_args(),
201212
"-d",
202213
"--network",
203214
"lan",
@@ -221,6 +232,7 @@ def test_no_certs_lan(self):
221232
self.postgres_container = docker(
222233
"container",
223234
"run",
235+
*self._platform_args(),
224236
"-d",
225237
"--network",
226238
"lan",
@@ -244,6 +256,7 @@ def test_no_certs_wan(self):
244256
self.postgres_container = docker(
245257
"container",
246258
"run",
259+
*self._platform_args(),
247260
"-d",
248261
"--network",
249262
"lan",
@@ -271,6 +284,7 @@ def test_certs_falsy_lan(self):
271284
self.postgres_container = docker(
272285
"container",
273286
"run",
287+
*self._platform_args(),
274288
"-d",
275289
"--network",
276290
"lan",
@@ -312,6 +326,7 @@ def test_hba_extra_rules_added(self):
312326
# Start the Postgres container with HBA_EXTRA_RULES
313327
self.postgres_container = docker(
314328
"run",
329+
*self._platform_args(),
315330
"-d",
316331
"--name",
317332
"postgres_test_hba_extra_rules",

0 commit comments

Comments
 (0)