87 lines
3.5 KiB
Go
87 lines
3.5 KiB
Go
package main
|
|
|
|
import (
|
|
"os"
|
|
"regexp"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// R-1105: restic is pinned OUTSIDE the OS. Until v0.323.0 it came from the runtime's apt list, so the
|
|
// version was „whatever Debian ships" — a Debian 12 → 13 base move would have swapped restic 0.14.0 for
|
|
// 0.18.0 under the off-site code with nobody choosing it. The consequences pinned here:
|
|
// - the version is one explicit ARG, and each architecture's checksum is pinned beside it;
|
|
// - the fetch checks restic's signature (vendored key, no keyserver) AND both checksums;
|
|
// - the runtime takes restic ONLY from that stage — no apt package can bring a second one;
|
|
// - every base image is pinned by digest, and the Docker repo's codename is read from the image.
|
|
func TestR1105_ResticIsPinnedOutsideTheOS(t *testing.T) {
|
|
raw, err := os.ReadFile("../../Dockerfile")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
src := string(raw)
|
|
// Stages, split on FROM, with `\`-continued lines joined so an apt list reads as one command.
|
|
joined := strings.ReplaceAll(src, "\\\n", " ")
|
|
var stages []string
|
|
for _, part := range regexp.MustCompile(`(?m)^FROM `).Split(joined, -1)[1:] {
|
|
stages = append(stages, "FROM "+part)
|
|
}
|
|
if len(stages) < 3 {
|
|
t.Fatalf("expected a builder, a restic and a runtime stage, found %d stages", len(stages))
|
|
}
|
|
var fetch string
|
|
for _, s := range stages {
|
|
if regexp.MustCompile(`^FROM \S+ AS restic\b`).MatchString(s) {
|
|
fetch = s
|
|
}
|
|
}
|
|
if fetch == "" {
|
|
t.Fatal("no `AS restic` stage: restic is no longer fetched as a pinned release binary")
|
|
}
|
|
runtime := stages[len(stages)-1]
|
|
|
|
if m := regexp.MustCompile(`ARG RESTIC_VERSION=(\d+\.\d+\.\d+)\s`).FindStringSubmatch(fetch); m == nil {
|
|
t.Error("ARG RESTIC_VERSION=X.Y.Z is missing — the version must be an explicit decision")
|
|
}
|
|
for _, arch := range []string{"AMD64", "ARM64"} {
|
|
if !regexp.MustCompile(`ARG RESTIC_SHA256_` + arch + `=[0-9a-f]{64}\s`).MatchString(fetch) {
|
|
t.Errorf("no pinned sha256 for %s", arch)
|
|
}
|
|
}
|
|
for _, want := range []string{
|
|
"COPY third_party/restic/restic-release-key.asc",
|
|
"gpgv --keyring", // SHA256SUMS is restic's, signed
|
|
`SHA256SUMS | sha256sum -c -`, // the download matches the published sum
|
|
`echo "${want} ${f}" | sha256sum -c -`, // ...and the sum pinned in this file
|
|
} {
|
|
if !strings.Contains(fetch, want) {
|
|
t.Errorf("the restic stage no longer does %q", want)
|
|
}
|
|
}
|
|
if _, err := os.Stat("../../third_party/restic/restic-release-key.asc"); err != nil {
|
|
t.Errorf("the vendored release key is missing: %v", err)
|
|
}
|
|
|
|
if !strings.Contains(runtime, "COPY --from=restic /usr/local/bin/restic /usr/local/bin/restic") {
|
|
t.Error("the runtime does not take restic from the pinned stage")
|
|
}
|
|
aptRe := regexp.MustCompile(`apt-get install[^&]*`)
|
|
for _, inst := range aptRe.FindAllString(runtime, -1) {
|
|
if regexp.MustCompile(`\srestic(\s|$)`).MatchString(inst) {
|
|
t.Errorf("the runtime's apt list installs restic — a second, OS-chosen restic: %q", strings.Join(strings.Fields(inst), " "))
|
|
}
|
|
}
|
|
|
|
for _, from := range regexp.MustCompile(`(?m)^FROM (\S+)`).FindAllStringSubmatch(src, -1) {
|
|
if !strings.Contains(from[1], "@sha256:") {
|
|
t.Errorf("base image %s is not pinned by digest", from[1])
|
|
}
|
|
}
|
|
if regexp.MustCompile(`download\.docker\.com/linux/debian (bookworm|trixie|forky)`).MatchString(src) {
|
|
t.Error("the Docker apt repo codename is hard-coded — it must come from /etc/os-release (VERSION_CODENAME)")
|
|
}
|
|
if !strings.Contains(runtime, "${VERSION_CODENAME}") {
|
|
t.Error("the Docker apt repo line does not read VERSION_CODENAME")
|
|
}
|
|
}
|