Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion config/example-runner-1.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ control_plane:
manager_version: v0.2.1-nddev.88
scheduling_mode: scale-set
provider: incus
provider_version: v0.1.5-nddev.120
provider_version: v0.1.5-nddev.121
provider_interface: v0.1.0
worker_kind: incus-container
runner: actions/runner
Expand Down
2 changes: 1 addition & 1 deletion config/example-runner-2.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ control_plane:
manager_version: v0.2.1-nddev.88
scheduling_mode: scale-set
provider: incus
provider_version: v0.1.5-nddev.120
provider_version: v0.1.5-nddev.121
provider_interface: v0.1.0
worker_kind: incus-container
runner: actions/runner
Expand Down
2 changes: 1 addition & 1 deletion config/example-runner-3.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ control_plane:
manager_version: v0.2.1-nddev.88
scheduling_mode: scale-set
provider: incus
provider_version: v0.1.5-nddev.120
provider_version: v0.1.5-nddev.121
provider_interface: v0.1.0
worker_kind: incus-container
runner: actions/runner
Expand Down
2 changes: 1 addition & 1 deletion config/example-runner-4.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ control_plane:
manager_version: v0.2.1-nddev.88
scheduling_mode: scale-set
provider: incus
provider_version: v0.1.5-nddev.120
provider_version: v0.1.5-nddev.121
provider_interface: v0.1.0
worker_kind: incus-container
runner: actions/runner
Expand Down
2 changes: 1 addition & 1 deletion config/example-services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ control_plane:
manager_version: v0.2.1-nddev.88
scheduling_mode: scale-set
provider: incus
provider_version: v0.1.5-nddev.120
provider_version: v0.1.5-nddev.121
provider_interface: v0.1.0
worker_kind: incus-container
runner: actions/runner
Expand Down
6 changes: 3 additions & 3 deletions config/provider-derivative.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ artifact: garm-provider-incus
# state all move together, because all three derive from here. A provider change
# that does not bump it ships under the previous version, which is exactly how
# runner-1 and runner-2 diverged.
derivative_version: v0.1.5-nddev.120
derivative_version: v0.1.5-nddev.121

# The external-provider protocol GARM speaks to this binary. It moves on its own
# schedule -- a provider release does not imply an interface release -- so it is
Expand All @@ -37,8 +37,8 @@ runtime:
queue_intent_schema_version: 6

build:
source_commit: 3a77ae2562015ad006857deaf2823a84575e3e3c
binary_sha256: 3c07b99b8d11f1bcded6208147540522aaa37302bf2203da78937bdbd2c70368
source_commit: 53ec735cd97727e5ba6175551b07e7c5621a533c
binary_sha256: b0d1a6980bb7efdd112acaed141d01708eeed9bb211770edc965cc033ff322a7
go_version: go1.26.7
cgo_enabled: false
target_os: linux
Expand Down
44 changes: 35 additions & 9 deletions internal/garmproviderincus/provider/incus.go
Original file line number Diff line number Diff line change
Expand Up @@ -1387,22 +1387,48 @@ func (l *Incus) adoptColdDirectJIT(
}
}

// runnerToolFilename is the exact shape of an official linux-x64 runner
// release asset. Anything else -- another architecture, another project, a
// rewritten host -- is refused.
var runnerToolFilename = regexp.MustCompile(`^actions-runner-linux-x64-(\d+\.\d+\.\d+)\.tar\.gz$`)

// validateRunnerTool checks the runner package GitHub advertises for this
// job. It does NOT require that package to be the version the fleet pins.
//
// The fleet does not download it. Every path the fleet uses -- the warm
// claim and the direct-JIT cold create -- runs the runner baked into the
// worker image, and neither passes the cloud-config this metadata would
// have fed (provider .108). The version that runs is pinned where it is
// installed: the image manifest names the archive and its digest, the build
// verifies both and the smoke asserts the installed runner.
//
// Requiring GitHub's advertised version to equal the pinned one made the
// fleet's create depend on GitHub's release schedule. On 2026-09-02 GitHub
// moved every organisation to 2.337.0 while the image baked 2.336.0, and
// within ten minutes 25 of 77 creates failed with "filename ... does not
// match pinned ...", a share that would have reached every create as the
// warm pools drained. A supply-chain guard must constrain what runs, not
// what an upstream advertises.
//
// What stays: the asset must be an official actions/runner linux-x64
// release tarball whose download URL is the github.com release URL for that
// same filename, so a substituted host or a renamed asset is still refused.
func (l *Incus) validateRunnerTool(tool commonParams.RunnerApplicationDownload) error {
version := strings.TrimPrefix(l.platform.ControlPlane.RunnerVersion, "v")
if version == "" || version == l.platform.ControlPlane.RunnerVersion {
if strings.TrimPrefix(l.platform.ControlPlane.RunnerVersion, "v") == l.platform.ControlPlane.RunnerVersion {
return fmt.Errorf("platform runner version must have a v prefix")
}
expectedFilename := fmt.Sprintf("actions-runner-linux-x64-%s.tar.gz", version)
filename := tool.GetFilename()
match := runnerToolFilename.FindStringSubmatch(filename)
if match == nil {
return fmt.Errorf("filename %q is not an official linux-x64 runner release asset", filename)
}
expectedURL := fmt.Sprintf(
"https://github.com/actions/runner/releases/download/v%s/%s",
version,
expectedFilename,
match[1],
filename,
)
if tool.GetFilename() != expectedFilename {
return fmt.Errorf("filename %q does not match pinned %q", tool.GetFilename(), expectedFilename)
}
if tool.GetDownloadURL() != expectedURL {
return fmt.Errorf("download URL %q does not match pinned official URL", tool.GetDownloadURL())
return fmt.Errorf("download URL %q does not match the official release URL for %q", tool.GetDownloadURL(), filename)
}
return nil
}
Expand Down
37 changes: 30 additions & 7 deletions internal/garmproviderincus/provider/incus_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -943,18 +943,41 @@ func TestGetCreateInstanceArgsInjectsOnlyTrustedRunnerCacheBootstrap(t *testing.
require.Contains(t, string(specs.PreInstallScripts["01-nddev-runner-groups.sh"]), "usermod --groups sudo runner")
}

func TestRunnerToolMetadataMustMatchPinnedImageVersion(t *testing.T) {
// The runner that runs is the one baked into the image, and the image
// manifest pins it. This guard constrains the package GitHub advertises to
// an official actions/runner linux-x64 release whose URL matches its own
// filename -- and deliberately not to the fleet's pinned version, because
// requiring that made every create depend on GitHub's release schedule: on
// 2026-09-02 GitHub moved every organisation to 2.337.0 and 25 of 77 creates
// failed in ten minutes while the image ran 2.336.0 perfectly well.
func TestRunnerToolMetadataMustBeAnOfficialReleaseAsset(t *testing.T) {
provider := newTestProvider(new(MockIncusServer))
valid := testTools()[0]
require.NoError(t, provider.validateRunnerTool(valid))

wrongFilename := valid
wrongFilename.Filename = ptr("actions-runner-linux-x64-2.337.0.tar.gz")
require.ErrorContains(t, provider.validateRunnerTool(wrongFilename), "does not match pinned")
newerThanPinned := valid
newerThanPinned.Filename = ptr("actions-runner-linux-x64-2.337.0.tar.gz")
newerThanPinned.DownloadURL = ptr("https://github.com/actions/runner/releases/download/v2.337.0/actions-runner-linux-x64-2.337.0.tar.gz")
require.NoError(t, provider.validateRunnerTool(newerThanPinned), "a newer advertised runner is not a reason to refuse the job")

wrongURL := valid
wrongURL.DownloadURL = ptr("https://example.invalid/actions-runner-linux-x64-2.336.0.tar.gz")
require.ErrorContains(t, provider.validateRunnerTool(wrongURL), "does not match pinned official URL")
mismatched := valid
mismatched.Filename = ptr("actions-runner-linux-x64-2.337.0.tar.gz")
require.ErrorContains(t, provider.validateRunnerTool(mismatched), "does not match the official release URL")

substitutedHost := valid
substitutedHost.DownloadURL = ptr("https://example.invalid/actions-runner-linux-x64-2.336.0.tar.gz")
require.ErrorContains(t, provider.validateRunnerTool(substitutedHost), "does not match the official release URL")

for _, name := range []string{
"actions-runner-linux-arm64-2.336.0.tar.gz",
"actions-runner-osx-x64-2.336.0.tar.gz",
"totally-not-a-runner-2.336.0.tar.gz",
"actions-runner-linux-x64-2.336.0.tar.gz.sig",
} {
wrong := valid
wrong.Filename = ptr(name)
require.ErrorContains(t, provider.validateRunnerTool(wrong), "not an official linux-x64 runner release asset", name)
}
}

func TestCanonicalRepositoryIdentity(t *testing.T) {
Expand Down