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 | } | ||