vole
Clone
Read only, no credentials:
https://rodent.software/git/vole.git
Write over SSH with a registered key:
git clone ssh://git@0.0.0.0: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 · 6d 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/workflow.go +38
| @@ -64,6 +64,12 @@ const ( | |||
| 64 | 64 | // MaxFilterAccounts bounds one event filter. A filter naming more accounts than | |
| 65 | 65 | // this is describing a roster, which is not what an event trigger is for. | |
| 66 | 66 | MaxFilterAccounts = 10 | |
| 67 | // NetworkNone and NetworkBridge are the isolation a job may ask for with | ||
| 68 | // `network:`. There are two because the container has one switch: a job has the | ||
| 69 | // network or it does not. "host" is not a third value, because a job that shares | ||
| 70 | // the runner's network is not a container any more. | ||
| 71 | NetworkNone = "none" | ||
| 72 | NetworkBridge = "bridge" | ||
| 67 | 73 | // MaxEventPayloadBytes bounds the event JSON pinned with a run. It is read as | |
| 68 | 74 | // data from a mounted file, never interpolated into a step. | |
| 69 | 75 | MaxEventPayloadBytes = 64 << 10 | |
| @@ -130,6 +136,18 @@ func ValidLabel(s string) error { | |||
| 130 | 136 | return nil | |
| 131 | 137 | } | |
| 132 | 138 | ||
| 139 | // ValidNetwork reports whether s is a network mode a job may ask for. It is | ||
| 140 | // exported for the same reason ValidLabel is: the runner checks the pinned job it | ||
| 141 | // was handed, and the local run applies the job's request, so all three callers | ||
| 142 | // have to agree on what the two values mean. | ||
| 143 | func ValidNetwork(s string) error { | ||
| 144 | switch s { | ||
| 145 | case NetworkNone, NetworkBridge: | ||
| 146 | return nil | ||
| 147 | } | ||
| 148 | return fmt.Errorf("%q is not a network mode; use %q or %q", s, NetworkNone, NetworkBridge) | ||
| 149 | } | ||
| 150 | |||
| 133 | 151 | type Step struct { | |
| 134 | 152 | Name string | |
| 135 | 153 | Run string | |
| @@ -162,6 +180,14 @@ type Job struct { | |||
| 162 | 180 | // had before targeting existed. A job is only leased to a runner carrying every | |
| 163 | 181 | // label here, so this is a filter on machines, not a preference among them. | |
| 164 | 182 | Labels []string | |
| 183 | // Network is the isolation the job asks for with `network:`, or empty when the | ||
| 184 | // workflow does not say. Empty is not "no network": it is "whatever the runner | ||
| 185 | // was started with", which is what every workflow meant before this field existed. | ||
| 186 | // A job that asks for NetworkBridge is only leased to a runner that was started | ||
| 187 | // able to provide it; a job that asks for NetworkNone gets none even from a runner | ||
| 188 | // that could provide bridge. The job decides, so the same workflow behaves the same | ||
| 189 | // way on every runner. | ||
| 190 | Network string | ||
| 165 | 191 | // Image is the image the job runs in. It is either a registry reference from | |
| 166 | 192 | // `image:` or `file:<name>` for a definition under .vole/images. | |
| 167 | 193 | Image string | |
| @@ -733,6 +759,7 @@ type rawJob struct { | |||
| 733 | 759 | Description string `yaml:"description"` | |
| 734 | 760 | Steps []rawStep `yaml:"steps"` | |
| 735 | 761 | RunsOn rawLabels `yaml:"runs-on"` | |
| 762 | Network string `yaml:"network"` | ||
| 736 | 763 | Image string `yaml:"image"` | |
| 737 | 764 | ImageFile string `yaml:"image-file"` | |
| 738 | 765 | Artifacts []rawArtifact `yaml:"artifacts"` | |
| @@ -1082,6 +1109,17 @@ func Parse(file string, content string) (*Workflow, []string) { | |||
| 1082 | 1109 | seenLabels[label] = true | |
| 1083 | 1110 | job.Labels = append(job.Labels, label) | |
| 1084 | 1111 | } | |
| 1112 | // network is a request, not a machine: a job that needs to reach something | ||
| 1113 | // outside the container says so, and the queue only hands it to a runner that | ||
| 1114 | // can keep the promise. It is validated here for the same reason runs-on is: a | ||
| 1115 | // value no runner can satisfy would leave the job queued forever. | ||
| 1116 | if network := strings.TrimSpace(jr.Network); network != "" { | ||
| 1117 | if err := ValidNetwork(network); err != nil { | ||
| 1118 | bad("jobs.%s.network: %v", id, err) | ||
| 1119 | } else { | ||
| 1120 | job.Network = network | ||
| 1121 | } | ||
| 1122 | } | ||
| 1085 | 1123 | for i, sr := range jr.Steps { | |
| 1086 | 1124 | where := fmt.Sprintf("jobs.%s.steps[%d]", id, i) | |
| 1087 | 1125 | run := strings.TrimSpace(sr.Run) | |