Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2ce364ea15 | |||
| 3057685588 | |||
| d45799a314 | |||
| 9788fe146b | |||
| f69263c480 | |||
|
ae8c114109
|
|||
|
ea0d90d8f0
|
|||
|
7d1b9f99a5
|
@@ -0,0 +1,26 @@
|
|||||||
|
# Build validation for pull requests (and pushes to main).
|
||||||
|
#
|
||||||
|
# Builds the full multi-arch image but does NOT push it anywhere — it only
|
||||||
|
# proves the Dockerfile still builds for every supported architecture. This is
|
||||||
|
# the gate Renovate automerge waits on: a dependency bump that breaks the build
|
||||||
|
# fails this check and will NOT be automerged (and therefore never reaches
|
||||||
|
# :stable or the routers).
|
||||||
|
#
|
||||||
|
# Reports pass/fail status back to Gitea, so it shows up as a required check on
|
||||||
|
# the PR.
|
||||||
|
|
||||||
|
when:
|
||||||
|
- event: pull_request
|
||||||
|
- event: push
|
||||||
|
branch: main
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Build all arches (no push)
|
||||||
|
image: woodpeckerci/plugin-docker-buildx:6.1.0
|
||||||
|
privileged: true
|
||||||
|
settings:
|
||||||
|
repo: mikrotik-tailscale
|
||||||
|
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||||
|
dry-run: true
|
||||||
|
build_args:
|
||||||
|
- OCI_VERSION=ci-${CI_COMMIT_SHA}
|
||||||
@@ -43,7 +43,7 @@ steps:
|
|||||||
- 'printf "PLUGIN_PASSWORD=%s\n" "$(bao kv get -mount secret -field REGISTRY_PASSWORD container-registry)" >> /woodpecker/registry.env'
|
- 'printf "PLUGIN_PASSWORD=%s\n" "$(bao kv get -mount secret -field REGISTRY_PASSWORD container-registry)" >> /woodpecker/registry.env'
|
||||||
|
|
||||||
- name: Build and push multi-arch image
|
- name: Build and push multi-arch image
|
||||||
image: woodpeckerci/plugin-docker-buildx:5.2.2
|
image: woodpeckerci/plugin-docker-buildx:6.1.0
|
||||||
privileged: true
|
privileged: true
|
||||||
settings:
|
settings:
|
||||||
registry: gitea.lumpiasty.xyz
|
registry: gitea.lumpiasty.xyz
|
||||||
|
|||||||
+1
-1
@@ -171,7 +171,7 @@ RUN mkdir -p /out/usrlocalbin && \
|
|||||||
# This stage runs on the TARGET platform (no --platform override): gcc then
|
# This stage runs on the TARGET platform (no --platform override): gcc then
|
||||||
# produces native target-arch binaries directly. Under buildx this is
|
# produces native target-arch binaries directly. Under buildx this is
|
||||||
# transparently emulated via binfmt/QEMU for non-native targets.
|
# transparently emulated via binfmt/QEMU for non-native targets.
|
||||||
FROM alpine:3.21.7@sha256:48b0309ca019d89d40f670aa1bc06e426dc0931948452e8491e3d65087abc07d AS busybox
|
FROM alpine:3.23.4@sha256:5b10f432ef3da1b8d4c7eb6c487f2f5a8f096bc91145e68878dd4a5019afde11 AS busybox
|
||||||
|
|
||||||
# renovate: datasource=docker depName=busybox versioning=docker
|
# renovate: datasource=docker depName=busybox versioning=docker
|
||||||
ARG BUSYBOX_VERSION=1.37.0
|
ARG BUSYBOX_VERSION=1.37.0
|
||||||
|
|||||||
+62
-26
@@ -28,10 +28,12 @@ Measured flattened rootfs for the arm64 image:
|
|||||||
| arm64 | ~3.5 MB |
|
| arm64 | ~3.5 MB |
|
||||||
| arm/v7 | ~3.5 MB |
|
| arm/v7 | ~3.5 MB |
|
||||||
|
|
||||||
> The extracted rootfs must contain the binary only **once**. If you measure
|
On a deployed RouterOS device the container consumes **~3.7 MiB of flash**
|
||||||
> ~7 MB on the device with `du -sx /`, the Dockerfile has reintroduced an
|
(measured by `free-hdd-space` delta). Note that `du` *inside* the container
|
||||||
> overlayfs copy-up — see
|
reports roughly double that (~7 MB) — that is RouterOS block-allocation
|
||||||
> [Avoiding overlayfs layer duplication](#avoiding-overlayfs-layer-duplication).
|
rounding, **not** real usage or duplication; see
|
||||||
|
[Avoiding overlayfs layer duplication](#avoiding-overlayfs-layer-duplication)
|
||||||
|
for how to measure correctly.
|
||||||
|
|
||||||
The binary is built with Tailscale's `--extra-small` feature tag set as the
|
The binary is built with Tailscale's `--extra-small` feature tag set as the
|
||||||
baseline. Features are opted in explicitly — any new feature Tailscale adds
|
baseline. Features are opted in explicitly — any new feature Tailscale adds
|
||||||
@@ -73,33 +75,46 @@ the Tailscale binary.
|
|||||||
|
|
||||||
### Avoiding overlayfs layer duplication
|
### Avoiding overlayfs layer duplication
|
||||||
|
|
||||||
A subtle but important detail: **the final image must not run a `RUN` that
|
Best practice for the final image: **don't run a `RUN` that mutates a directory
|
||||||
mutates a directory already populated by an earlier layer**, or the extracted
|
already populated by an earlier layer.** Each Dockerfile instruction is its own
|
||||||
on-disk size roughly doubles for that directory's contents.
|
layer; if `/usr/local/bin/` is created by a `COPY` (containing the ~3 MB
|
||||||
|
`tailscale.combined`) and a later `RUN ln -s …` adds a symlink *inside that same
|
||||||
RouterOS Container uses overlayfs and stores the **extracted** layers on disk.
|
directory*, overlayfs performs a **copy-up** of the entire directory — including
|
||||||
Each Dockerfile instruction is its own layer. If `/usr/local/bin/` is created by
|
the 3 MB binary — into the new layer. The binary then physically exists in two
|
||||||
a `COPY` (containing the ~3 MB `tailscale.combined`) and a later `RUN ln -s …`
|
image layers.
|
||||||
adds a symlink *inside that same directory*, overlayfs performs a **copy-up**:
|
|
||||||
it copies the entire `/usr/local/bin/` directory — including the 3 MB binary —
|
|
||||||
into the new layer's upper dir. RouterOS then extracts both copies to flash, so
|
|
||||||
`du -sx /` reports ~7 MB instead of ~3.4 MB for a directory whose only real file
|
|
||||||
is 3 MB. (The compressed image hides this — compression dedupes identical blocks
|
|
||||||
— which is why it only shows up when you measure the *extracted* rootfs on the
|
|
||||||
device.)
|
|
||||||
|
|
||||||
The fix: assemble `/usr/local/bin/` completely in the **builder** stage (binary
|
The fix: assemble `/usr/local/bin/` completely in the **builder** stage (binary
|
||||||
+ both `argv[0]` symlinks) and bring it into the final image with a **single
|
+ both `argv[0]` symlinks) and bring it into the final image with a **single
|
||||||
`COPY` layer**, never mutating it afterwards. The Dockerfile does this; don't
|
`COPY` layer**, never mutating it afterwards. The Dockerfile does this; don't
|
||||||
reintroduce a post-`COPY` `RUN` against that path.
|
reintroduce a post-`COPY` `RUN` against that path. You can confirm the published
|
||||||
|
image carries the binary in exactly one layer:
|
||||||
To verify the extracted footprint on a deployed router:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
/container/shell [find where name=tailscale]
|
docker save <image> -o img.tar && tar xf img.tar -C img/
|
||||||
du -sx / # expect ~3500 KiB (1 KiB blocks), not ~7000
|
# then grep each blob layer for usr/local/bin/tailscale.combined — it must
|
||||||
|
# appear in exactly ONE layer.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Note: this is about keeping the *image* clean. It does **not** change what `du`
|
||||||
|
reports on the device — see the measurement note below.
|
||||||
|
|
||||||
|
To verify the on-flash footprint on a deployed router, use the **free-space
|
||||||
|
delta**, not `du`:
|
||||||
|
|
||||||
|
```
|
||||||
|
/system/resource/print # note free-hdd-space before and after adding the container
|
||||||
|
```
|
||||||
|
|
||||||
|
The container should consume **~3.7 MiB** of flash (e.g. 94.6 → 90.9 MiB free).
|
||||||
|
|
||||||
|
Do **not** trust `du` inside the container for this. Busybox `du` reports
|
||||||
|
*allocated blocks*, and RouterOS's container store rounds a ~3 MB file up to
|
||||||
|
~6 MB of blocks — so `du -sx /` reports ~7 MB even though real flash use is
|
||||||
|
~3.7 MB. `ls -la /usr/local/bin` confirms the binary's true content size
|
||||||
|
(~3.1 MB) and that it is a single file with two symlinks (no duplication).
|
||||||
|
The image itself carries the binary in exactly one layer (verified at the blob
|
||||||
|
level); the inflation is purely the filesystem's block accounting.
|
||||||
|
|
||||||
## Architecture support
|
## Architecture support
|
||||||
|
|
||||||
A single Dockerfile builds all three supported RouterOS architectures. The Go
|
A single Dockerfile builds all three supported RouterOS architectures. The Go
|
||||||
@@ -309,14 +324,35 @@ run **self-hosted** from a Woodpecker cron pipeline (Woodpecker has no native
|
|||||||
Renovate support):
|
Renovate support):
|
||||||
|
|
||||||
- `renovate.json` — repository rules. All dependencies follow the latest
|
- `renovate.json` — repository rules. All dependencies follow the latest
|
||||||
upstream releases (including major versions); each bump arrives as its own PR
|
upstream releases; each bump arrives as its own PR. Base image tags also get
|
||||||
that the multi-arch build validates before you merge. Base image tags also
|
their `@sha256` digests refreshed via `pinDigests`. Notable rules:
|
||||||
get their `@sha256` digests refreshed via `pinDigests`. The one special rule:
|
|
||||||
- `tailscale` only follows **stable** releases — Tailscale uses even minor
|
- `tailscale` only follows **stable** releases — Tailscale uses even minor
|
||||||
versions for stable (`v1.98.x`) and odd for unstable (`v1.99.x`), so the
|
versions for stable (`v1.98.x`) and odd for unstable (`v1.99.x`), so the
|
||||||
rule filters to even minors.
|
rule filters to even minors.
|
||||||
- `.woodpecker/renovate.yaml` — the scheduled job that runs `renovate/renovate`
|
- `.woodpecker/renovate.yaml` — the scheduled job that runs `renovate/renovate`
|
||||||
against this repo.
|
against this repo.
|
||||||
|
- `.woodpecker/pr-build.yaml` — builds all three arches (no push) on every PR
|
||||||
|
and reports status to Gitea. This is the gate for automerge.
|
||||||
|
|
||||||
|
### Automerge policy
|
||||||
|
|
||||||
|
These updates **automerge** once the PR build passes — they reach `:stable`
|
||||||
|
(and the routers) without manual review:
|
||||||
|
|
||||||
|
| Update | Automerge? | Why |
|
||||||
|
|---|---|---|
|
||||||
|
| Tailscale stable (patch **and** minor) | ✅ | the point of the project; the PR build catches breakage |
|
||||||
|
| Go / Alpine / busybox **patch** | ✅ | bugfix-only, build-internal |
|
||||||
|
| Base-image **digest** refresh (same tag) | ✅ | content refresh, no version change |
|
||||||
|
| Go / Alpine / busybox **minor/major** | ❌ manual | larger toolchain/base changes warrant review |
|
||||||
|
| Renovate runner, syntax frontend | ❌ manual | tooling — review deliberately |
|
||||||
|
|
||||||
|
**Important:** automerge depends on the PR build being a **required status
|
||||||
|
check** in Gitea branch protection. The PR build only proves the image *builds*
|
||||||
|
for all arches — it does not run the daemon, so a runtime regression in a new
|
||||||
|
Tailscale release could still be automerged. That is an accepted trade-off for
|
||||||
|
the convenience of unattended Tailscale updates; if a release misbehaves, roll
|
||||||
|
back by re-tagging the previous `v…-mt.N` (the immutable tags are kept).
|
||||||
|
|
||||||
Validate the configs locally:
|
Validate the configs locally:
|
||||||
|
|
||||||
|
|||||||
+28
-4
@@ -7,10 +7,12 @@
|
|||||||
],
|
],
|
||||||
"labels": ["dependencies"],
|
"labels": ["dependencies"],
|
||||||
"rebaseWhen": "behind-base-branch",
|
"rebaseWhen": "behind-base-branch",
|
||||||
"dockerfile": {
|
|
||||||
"pinDigests": true
|
|
||||||
},
|
|
||||||
"packageRules": [
|
"packageRules": [
|
||||||
|
{
|
||||||
|
"matchManagers": ["dockerfile"],
|
||||||
|
"description": "Keep base-image tags pinned to a digest.",
|
||||||
|
"pinDigests": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"matchDatasources": ["github-releases"],
|
"matchDatasources": ["github-releases"],
|
||||||
"matchPackageNames": ["tailscale/tailscale"],
|
"matchPackageNames": ["tailscale/tailscale"],
|
||||||
@@ -18,6 +20,28 @@
|
|||||||
"extractVersion": "^v(?<version>\\d+\\.\\d+\\.\\d+)$",
|
"extractVersion": "^v(?<version>\\d+\\.\\d+\\.\\d+)$",
|
||||||
"allowedVersions": "/^\\d+\\.\\d*[02468]\\.\\d+$/",
|
"allowedVersions": "/^\\d+\\.\\d*[02468]\\.\\d+$/",
|
||||||
"ignoreUnstable": true
|
"ignoreUnstable": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matchDatasources": ["github-releases"],
|
||||||
|
"matchPackageNames": ["tailscale/tailscale"],
|
||||||
|
"description": "Automerge all stable Tailscale releases (patch AND minor) once the PR build passes.",
|
||||||
|
"matchUpdateTypes": ["minor", "patch"],
|
||||||
|
"automerge": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matchManagers": ["dockerfile"],
|
||||||
|
"matchPackageNames": ["golang", "alpine", "busybox"],
|
||||||
|
"description": "Automerge PATCH-only bumps of build components (Go/Alpine/busybox) once the PR build passes; review minor/major manually.",
|
||||||
|
"matchUpdateTypes": ["patch"],
|
||||||
|
"automerge": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matchManagers": ["dockerfile"],
|
||||||
|
"matchUpdateTypes": ["digest", "pinDigest"],
|
||||||
|
"description": "Automerge base-image digest refreshes (same tag, new sha256) once the PR build passes.",
|
||||||
|
"automerge": true
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"automergeType": "pr",
|
||||||
|
"platformAutomerge": true
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user