Skip to content
This repository was archived by the owner on Sep 30, 2024. It is now read-only.

Commit 0b703ce

Browse files
committed
feat(executor): add option for additional mounts in docker runner
1 parent e5388c3 commit 0b703ce

File tree

11 files changed

+694
-0
lines changed

11 files changed

+694
-0
lines changed

cmd/executor/internal/config/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ go_library(
1212
visibility = ["//cmd/executor:__subpackages__"],
1313
deps = [
1414
"//internal/conf/confdefaults",
15+
"//internal/docker",
1516
"//internal/env",
1617
"//internal/executor/types",
1718
"//internal/hostname",
@@ -34,6 +35,7 @@ go_test(
3435
],
3536
deps = [
3637
":config",
38+
"//internal/docker",
3739
"//internal/env",
3840
"//lib/errors",
3941
"@com_github_stretchr_testify//assert",

cmd/executor/internal/config/config.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"k8s.io/utils/strings/slices"
1717

1818
"github.com/sourcegraph/sourcegraph/internal/conf/confdefaults"
19+
"github.com/sourcegraph/sourcegraph/internal/docker"
1920
"github.com/sourcegraph/sourcegraph/internal/env"
2021
"github.com/sourcegraph/sourcegraph/internal/executor/types"
2122
"github.com/sourcegraph/sourcegraph/internal/hostname"
@@ -55,6 +56,8 @@ type Config struct {
5556
DockerRegistryMirrorURL string
5657
DockerAddHostGateway bool
5758
DockerAuthConfig types.DockerAuthConfig
59+
DockerAdditionalMounts []string
60+
DockerAdditionalMountsStr string
5861
KubernetesConfigPath string
5962
KubernetesNodeName string
6063
KubernetesNodeSelector string
@@ -142,6 +145,7 @@ func (c *Config) Load() {
142145
c.NumTotalJobs = c.GetInt("EXECUTOR_NUM_TOTAL_JOBS", "0", "The maximum number of jobs that will be dequeued by the worker.")
143146
c.NodeExporterURL = c.GetOptional("NODE_EXPORTER_URL", "The URL of the node_exporter instance, without the /metrics path.")
144147
c.DockerRegistryNodeExporterURL = c.GetOptional("DOCKER_REGISTRY_NODE_EXPORTER_URL", "The URL of the Docker Registry instance's node_exporter, without the /metrics path.")
148+
c.DockerAdditionalMountsStr = c.GetOptional("EXECUTOR_DOCKER_ADDITIONAL_MOUNTS", "Attach filesystem mounts to the container (e.g. type=bind,source=/foo,destination=/bar), semicolon-separated")
145149
c.MaxActiveTime = c.GetInterval("EXECUTOR_MAX_ACTIVE_TIME", "0", "The maximum time that can be spent by the worker dequeueing records to be handled.")
146150
c.DockerRegistryMirrorURL = c.GetOptional("EXECUTOR_DOCKER_REGISTRY_MIRROR_URL", "The address of a docker registry mirror to use in firecracker VMs. Supports multiple values, separated with a comma.")
147151
c.KubernetesConfigPath = c.GetOptional("EXECUTOR_KUBERNETES_CONFIG_PATH", "The path to the Kubernetes config file.")
@@ -184,6 +188,10 @@ func (c *Config) Load() {
184188
c.dockerAuthConfigUnmarshalError = json.Unmarshal([]byte(c.dockerAuthConfigStr), &c.DockerAuthConfig)
185189
}
186190

191+
if c.DockerAdditionalMountsStr != "" {
192+
c.DockerAdditionalMounts = strings.Split(c.DockerAdditionalMountsStr, ";")
193+
}
194+
187195
if c.kubernetesNodeRequiredAffinityMatchExpressions != "" {
188196
c.kubernetesNodeRequiredAffinityMatchExpressionsUnmarshalError = json.Unmarshal([]byte(c.kubernetesNodeRequiredAffinityMatchExpressions), &c.KubernetesNodeRequiredAffinityMatchExpressions)
189197
}
@@ -261,6 +269,29 @@ func (c *Config) Validate() error {
261269
c.AddError(errors.Wrap(c.dockerAuthConfigUnmarshalError, "invalid EXECUTOR_DOCKER_AUTH_CONFIG, failed to parse"))
262270
}
263271

272+
if c.DockerAdditionalMountsStr != "" {
273+
// Target is a mandatory field so we can rely on it showing up if
274+
// multiple mounts are present, but we need to check for all forms.
275+
countTargets := func(text string, patterns ...string) int {
276+
count := 0
277+
for _, pattern := range patterns {
278+
count += strings.Count(text, pattern)
279+
}
280+
return count
281+
}
282+
283+
if len(c.DockerAdditionalMounts) == 1 && countTargets(c.DockerAdditionalMountsStr, "target", "dst", "destination") > 1 {
284+
c.AddError(errors.New("invalid EXECUTOR_DOCKER_ADDITIONAL_MOUNTS, failed to parse due to incorrect separator"))
285+
}
286+
287+
for _, mount := range c.DockerAdditionalMounts {
288+
_, err := docker.ParseMount(mount)
289+
if err != nil {
290+
c.AddError(errors.Wrap(err, "invalid EXECUTOR_DOCKER_ADDITIONAL_MOUNTS, failed to parse mount spec"))
291+
}
292+
}
293+
}
294+
264295
if c.kubernetesNodeRequiredAffinityMatchExpressionsUnmarshalError != nil {
265296
c.AddError(errors.Wrap(c.kubernetesNodeRequiredAffinityMatchExpressionsUnmarshalError, "invalid EXECUTOR_KUBERNETES_NODE_REQUIRED_AFFINITY_MATCH_EXPRESSIONS, failed to parse"))
266297
}

cmd/executor/internal/config/config_test.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ func TestConfig_Load(t *testing.T) {
9494
assert.Equal(t, "EXECUTOR_VM_PREFIX", cfg.VMPrefix)
9595
assert.True(t, cfg.KeepWorkspaces)
9696
assert.Equal(t, "EXECUTOR_DOCKER_HOST_MOUNT_PATH", cfg.DockerHostMountPath)
97+
assert.Equal(t, "EXECUTOR_DOCKER_ADDITIONAL_MOUNTS", cfg.DockerAdditionalMountsStr)
9798
assert.Equal(t, 8, cfg.JobNumCPUs)
9899
assert.Equal(t, "EXECUTOR_JOB_MEMORY", cfg.JobMemory)
99100
assert.Equal(t, "EXECUTOR_FIRECRACKER_DISK_SPACE", cfg.FirecrackerDiskSpace)
@@ -203,6 +204,7 @@ func TestConfig_Load_Defaults(t *testing.T) {
203204
assert.Equal(t, "executor", cfg.VMPrefix)
204205
assert.False(t, cfg.KeepWorkspaces)
205206
assert.Empty(t, cfg.DockerHostMountPath)
207+
assert.Empty(t, cfg.DockerAdditionalMountsStr)
206208
assert.Equal(t, 4, cfg.JobNumCPUs)
207209
assert.Equal(t, "12G", cfg.JobMemory)
208210
assert.Equal(t, "20G", cfg.FirecrackerDiskSpace)
@@ -253,6 +255,8 @@ func TestConfig_Validate(t *testing.T) {
253255
switch name {
254256
case "EXECUTOR_QUEUE_NAME":
255257
return "batches"
258+
case "EXECUTOR_DOCKER_ADDITIONAL_MOUNTS":
259+
return "type=bind,source=/foo,target=/bar"
256260
case "EXECUTOR_FRONTEND_URL":
257261
return "http://some-url.com"
258262
case "EXECUTOR_FRONTEND_PASSWORD":
@@ -348,6 +352,59 @@ func TestConfig_Validate(t *testing.T) {
348352
},
349353
expectedErr: errors.New("EXECUTOR_QUEUE_NAMES contains invalid queue name 'batches;codeintel', valid names are 'batches, codeintel' and should be comma-separated"),
350354
},
355+
{
356+
name: "EXECUTOR_DOCKER_ADDITIONAL_MOUNTS using invalid separator",
357+
getterFunc: func(name, defaultValue, description string) string {
358+
switch name {
359+
case "EXECUTOR_QUEUE_NAME":
360+
return "batches"
361+
case "EXECUTOR_DOCKER_ADDITIONAL_MOUNTS":
362+
return "type=bind,source=/foo,target=/bar:type=volume,source=gomodcache,target=/gomodcache"
363+
case "EXECUTOR_FRONTEND_URL":
364+
return "http://some-url.com"
365+
case "EXECUTOR_FRONTEND_PASSWORD":
366+
return "some-password"
367+
default:
368+
return defaultValue
369+
}
370+
},
371+
expectedErr: errors.New("invalid EXECUTOR_DOCKER_ADDITIONAL_MOUNTS, failed to parse due to incorrect separator"),
372+
},
373+
{
374+
name: "EXECUTOR_DOCKER_ADDITIONAL_MOUNTS using incorrect format",
375+
getterFunc: func(name, defaultValue, description string) string {
376+
switch name {
377+
case "EXECUTOR_QUEUE_NAME":
378+
return "batches"
379+
case "EXECUTOR_DOCKER_ADDITIONAL_MOUNTS":
380+
return "source=/foo;/bar"
381+
case "EXECUTOR_FRONTEND_URL":
382+
return "http://some-url.com"
383+
case "EXECUTOR_FRONTEND_PASSWORD":
384+
return "some-password"
385+
default:
386+
return defaultValue
387+
}
388+
},
389+
expectedErr: errors.New("2 errors occurred:\n\t* invalid EXECUTOR_DOCKER_ADDITIONAL_MOUNTS, failed to parse mount spec: target is required\n\t* invalid EXECUTOR_DOCKER_ADDITIONAL_MOUNTS, failed to parse mount spec: invalid field '/bar' must be a key=value pair"),
390+
},
391+
{
392+
name: "EXECUTOR_DOCKER_ADDITIONAL_MOUNTS using volume options",
393+
getterFunc: func(name, defaultValue, description string) string {
394+
switch name {
395+
case "EXECUTOR_QUEUE_NAME":
396+
return "batches"
397+
case "EXECUTOR_DOCKER_ADDITIONAL_MOUNTS":
398+
return "type=volume,source=sshvolume,target=/app,volume-opt=sshcmd=test@node2:/home/test,volume-opt=password=testpassword"
399+
case "EXECUTOR_FRONTEND_URL":
400+
return "http://some-url.com"
401+
case "EXECUTOR_FRONTEND_PASSWORD":
402+
return "some-password"
403+
default:
404+
return defaultValue
405+
}
406+
},
407+
},
351408
}
352409
for _, test := range tests {
353410
t.Run(test.name, func(t *testing.T) {

cmd/executor/internal/run/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ go_library(
2727
"//cmd/executor/internal/worker/runner",
2828
"//cmd/executor/internal/worker/workspace",
2929
"//internal/conf/deploy",
30+
"//internal/docker",
3031
"//internal/download",
3132
"//internal/executor",
3233
"//internal/executor/types",

cmd/executor/internal/run/util.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"github.com/sourcegraph/sourcegraph/cmd/executor/internal/worker/command"
2121
"github.com/sourcegraph/sourcegraph/cmd/executor/internal/worker/runner"
2222
"github.com/sourcegraph/sourcegraph/internal/conf/deploy"
23+
"github.com/sourcegraph/sourcegraph/internal/docker"
2324
executorutil "github.com/sourcegraph/sourcegraph/internal/executor/util"
2425
"github.com/sourcegraph/sourcegraph/internal/observation"
2526
"github.com/sourcegraph/sourcegraph/internal/version"
@@ -107,9 +108,23 @@ func dockerOptions(c *config.Config) command.DockerOptions {
107108
DockerAuthConfig: c.DockerAuthConfig,
108109
AddHostGateway: c.DockerAddHostGateway,
109110
Resources: resourceOptions(c),
111+
Mounts: dockerAdditionalMounts(c),
110112
}
111113
}
112114

115+
func dockerAdditionalMounts(c *config.Config) []docker.MountOptions {
116+
opts := make([]docker.MountOptions, 0)
117+
118+
for _, mount := range c.DockerAdditionalMounts {
119+
// No need to check for parsing errors here. We're already doing that
120+
// during the config validation phase.
121+
m, _ := docker.ParseMount(mount)
122+
opts = append(opts, *m)
123+
}
124+
125+
return opts
126+
}
127+
113128
func firecrackerOptions(c *config.Config) runner.FirecrackerOptions {
114129
var dockerMirrors []string
115130
if len(c.DockerRegistryMirrorURL) > 0 {

cmd/executor/internal/worker/command/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ go_library(
1818
"//cmd/executor/internal/util",
1919
"//cmd/executor/internal/worker/cmdlogger",
2020
"//cmd/executor/internal/worker/files",
21+
"//internal/docker",
2122
"//internal/executor/types",
2223
"//internal/metrics",
2324
"//internal/observation",

cmd/executor/internal/worker/command/docker.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"strconv"
77

88
"github.com/sourcegraph/sourcegraph/cmd/executor/internal/worker/files"
9+
"github.com/sourcegraph/sourcegraph/internal/docker"
910
"github.com/sourcegraph/sourcegraph/internal/executor/types"
1011
)
1112

@@ -15,6 +16,7 @@ type DockerOptions struct {
1516
ConfigPath string
1617
AddHostGateway bool
1718
Resources ResourceOptions
19+
Mounts []docker.MountOptions
1820
}
1921

2022
// ResourceOptions are the resource limits that can be applied to a container or VM.
@@ -80,6 +82,7 @@ func formatDockerCommand(hostDir string, image string, scriptPath string, spec S
8082
"--rm",
8183
dockerHostGatewayFlag(options.AddHostGateway),
8284
dockerResourceFlags(options.Resources),
85+
dockerMountFlags(options.Mounts),
8386
dockerVolumeFlags(hostDir),
8487
dockerWorkingDirectoryFlags(spec.Dir),
8588
dockerEnvFlags(spec.Env),
@@ -115,6 +118,17 @@ func dockerResourceFlags(options ResourceOptions) []string {
115118
return flags
116119
}
117120

121+
func dockerMountFlags(options []docker.MountOptions) []string {
122+
mounts := make([]string, 0)
123+
124+
for _, option := range options {
125+
mounts = append(mounts, "--mount")
126+
mounts = append(mounts, option.String())
127+
}
128+
129+
return mounts
130+
}
131+
118132
func dockerVolumeFlags(wd string) []string {
119133
return []string{"-v", wd + ":/data"}
120134
}

0 commit comments

Comments
 (0)