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/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)