Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/vole.git

Write over SSH with a registered key:

git clone ssh://git@rodent.software:2222/vole.git

A free and open-source forge that lives in your Git repo.

Let a job declare the network it needs

d79580247b8f33f7ffdf2f05847733cad12c187a · Keenan Nemetz · 7d ago · parent 4ac42680

Let a job declare the network it needs

A runner's --container-network decided the network for every job it took, so a
machine that could reach the network gave it to builds that never asked, and a
job that did need one could be handed to a machine that had none. The docs said
"add bridge only when a job needs it", which no workflow could express.

A job now says `network: none` or `network: bridge`. The request is pinned with
the run, and the lease only hands a bridge job to a runner that advertises the
capability, so a job waits for a capable machine instead of failing on one that
never could. The runner applies the job's request to its container, which means a
bridge runner can still withhold the network from a job that asked for none.

A job that declares nothing keeps the runner's own setting, which is what every
workflow meant before this field existed. The local run applies the same rule and
refuses a bridge job when it was started without one, so `vole workflow run` either
matches CI or says what to change on the command line.

The workflows page, the run page, and the runner's log line now say what a job
asked for and what it ran with. verify.yaml and release.yaml are deliberately not
in this change: the instance validates a pushed workflow with the binary it is
running, so they can declare the field once that binary knows it.

Closes #159.

ci/agent/sandbox_test.go +54

@@ -1,7 +1,11 @@
1 1 package agent
2 2
3 3 import (
4 "strings"
4 5 "testing"
6
7 "github.com/rodent-software/vole/ci"
8 "github.com/rodent-software/vole/ci/container"
5 9 )
6 10
7 11 func TestContainerOptionsRequireRuntime(t *testing.T) {
@@ -9,3 +13,53 @@ func TestContainerOptionsRequireRuntime(t *testing.T) {
9 13 t.Fatal("an unknown container runtime was accepted")
10 14 }
11 15 }
16
17 // The job's `network:` decides what the container gets, and the runner's own flag is
18 // only the default for a job that says nothing. A job that asks for more than the
19 // machine can give is refused before anything runs, rather than starting a build that
20 // cannot reach what it needs.
21 func TestNetworkForUsesTheJobsRequest(t *testing.T) {
22 t.Parallel()
23 cases := []struct {
24 name string
25 runner string
26 job string
27 want string
28 wantError string
29 }{
30 // A job that says nothing gets the runner's default, which is what every
31 // workflow meant before the field existed.
32 {"silent job on a bridge runner", ci.NetworkBridge, "", ci.NetworkBridge, ""},
33 {"silent job on a closed runner", ci.NetworkNone, "", ci.NetworkNone, ""},
34 // An explicit request wins in both directions: a bridge runner can withhold
35 // the network, and a closed runner never silently provides one.
36 {"none on a bridge runner", ci.NetworkBridge, ci.NetworkNone, ci.NetworkNone, ""},
37 {"none on a closed runner", ci.NetworkNone, ci.NetworkNone, ci.NetworkNone, ""},
38 {"bridge on a bridge runner", ci.NetworkBridge, ci.NetworkBridge, ci.NetworkBridge, ""},
39 {"bridge on a closed runner", ci.NetworkNone, ci.NetworkBridge, ci.NetworkNone, "network: bridge"},
40 // A value the validator would refuse cannot arrive through a push, but the
41 // runner checks the job it was handed rather than trusting the row.
42 {"unknown value", ci.NetworkBridge, "host", ci.NetworkBridge, "not a network mode"},
43 }
44 for _, c := range cases {
45 t.Run(c.name, func(t *testing.T) {
46 box := runnerBox{Config: container.Config{Network: c.runner}}
47 cfg, err := box.networkFor(&runnerJob{ID: "r1", Network: c.job})
48 if c.wantError != "" {
49 if err == nil {
50 t.Fatalf("network %q on a %s runner was accepted", c.job, c.runner)
51 }
52 if !strings.Contains(err.Error(), c.wantError) {
53 t.Fatalf("error = %q, want it to contain %q", err, c.wantError)
54 }
55 return
56 }
57 if err != nil {
58 t.Fatalf("network %q on a %s runner: %v", c.job, c.runner, err)
59 }
60 if cfg.Network != c.want {
61 t.Fatalf("network %q on a %s runner = %q, want %q", c.job, c.runner, cfg.Network, c.want)
62 }
63 })
64 }
65 }