Skip to content

Commit 7ce5b18

Browse files
LawnGnomemrnugget
andauthored
campaigns: add and use volume mounts by default on Intel macOS (#412)
Co-authored-by: Thorsten Ball <[email protected]>
1 parent 8f301ce commit 7ce5b18

29 files changed

+1751
-296
lines changed

.github/workflows/docker.yml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# For more information, refer to the "Dependent Docker images" section of
2+
# DEVELOPMENT.md.
3+
name: Publish Docker image dependencies
4+
5+
# We only want to build on releases; this condition is 100% stolen from the
6+
# goreleaser action.
7+
on:
8+
push:
9+
tags:
10+
- "*"
11+
- "!latest"
12+
13+
jobs:
14+
publish:
15+
runs-on: ubuntu-20.04
16+
steps:
17+
- name: Checkout
18+
uses: actions/checkout@v2
19+
20+
# We need buildx to be able to build a multi-architecture image.
21+
- name: Set up Docker buildx
22+
uses: docker/setup-buildx-action@v1
23+
24+
# We also need QEMU, since this is running on an AMD64 host and we want to
25+
# build ARM64 images.
26+
- name: Set up QEMU
27+
uses: docker/setup-qemu-action@v1
28+
with:
29+
platforms: arm64
30+
31+
- run: ./docker/campaign-volume-workspace/push.py -d ./docker/campaign-volume-workspace/Dockerfile -i sourcegraph/src-campaign-volume-workspace -p linux/amd64,linux/arm64,linux/386
32+
env:
33+
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
34+
DOCKER_USERNAME: sourcegraphci
35+
36+
- name: Update Docker Hub description
37+
uses: peter-evans/dockerhub-description@v2
38+
with:
39+
username: sourcegraphci
40+
password: ${{ secrets.DOCKER_PASSWORD }}
41+
repository: sourcegraph/src-campaign-volume-workspace
42+
readme-filepath: ./docker/campaign-volume-workspace/README.md

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ All notable changes to `src-cli` are documented in this file.
1313

1414
### Added
1515

16+
- `src campaign [apply|preview]` can now make use of Docker volumes, rather than bind-mounting the host filesystem. This is now the default on macOS, as volume mounts have generally better performance there. The optional `-workspace` flag can be used to override the default. [#412](https://github.com/sourcegraph/src-cli/pull/412)
17+
1618
### Changed
1719

1820
- `src login` now defaults to validating against `SRC_ENDPOINT` if configured.

DEVELOPMENT.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,19 @@ For example, suppose we have the the recommended versions.
3838
If a new feature is added to a new `3.91.6` release of src-cli and this change requires only features available in Sourcegraph `3.99`, then this feature should also be present in a new `3.85.8` release of src-cli. Because a Sourcegraph instance will automatically select the highest patch version, all non-breaking changes should increment only the patch version.
3939

4040
Note that if instead the recommended src-cli version for Sourcegraph `3.99` was `3.90.4` in the example above, there is no additional step required, and the new patch version of src-cli will be available to both Sourcegraph versions.
41+
42+
## Dependent Docker images
43+
44+
`src campaign apply` and `src campaign preview` use a Docker image published as `sourcegraph/src-campaign-volume-workspace` for utility purposes when the volume workspace is selected, which is the default on macOS. This [Docker image](./docker/campaign-volume-workspace/Dockerfile) is built by [a Python script](./docker/campaign-volume-workspace/push.py) invoked by the GitHub Action workflow described in [`docker.yml`](.github/workflows/docker.yml).
45+
46+
To build and develop this locally, you can build and tag the image with:
47+
48+
```sh
49+
docker build -t sourcegraph/src-campaign-volume-workspace - < docker/campaign-volume-workspace/Dockerfile
50+
```
51+
52+
To remove it and then force the upstream image on Docker Hub to be used again:
53+
54+
```sh
55+
docker rmi sourcegraph/src-campaign-volume-workspace
56+
```

cmd/src/campaigns_apply.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ Examples:
7070
svc := campaigns.NewService(&campaigns.ServiceOpts{
7171
AllowUnsupported: flags.allowUnsupported,
7272
Client: cfg.apiClient(flags.api, flagSet.Output()),
73+
Workspace: flags.workspace,
7374
})
7475

7576
if err := svc.DetermineFeatureFlags(ctx); err != nil {

cmd/src/campaigns_common.go

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ type campaignsApplyFlags struct {
4040
namespace string
4141
parallelism int
4242
timeout time.Duration
43+
workspace string
4344
cleanArchives bool
4445
skipErrors bool
4546
}
@@ -100,6 +101,20 @@ func newCampaignsApplyFlags(flagSet *flag.FlagSet, cacheDir, tempDir string) *ca
100101
"If true, errors encountered while executing steps in a repository won't stop the execution of the campaign spec but only cause that repository to be skipped.",
101102
)
102103

104+
// We default to bind workspaces on everything except ARM64 macOS at
105+
// present. In the future, we'll likely want to switch the default for ARM64
106+
// macOS as well, but this requires access to the hardware for testing.
107+
var defaultWorkspace string
108+
if runtime.GOOS == "darwin" && runtime.GOARCH == "amd64" {
109+
defaultWorkspace = "volume"
110+
} else {
111+
defaultWorkspace = "bind"
112+
}
113+
flagSet.StringVar(
114+
&caf.workspace, "workspace", defaultWorkspace,
115+
`Workspace mode to use ("bind" or "volume")`,
116+
)
117+
103118
flagSet.BoolVar(verbose, "v", false, "print verbose output")
104119

105120
return caf
@@ -176,18 +191,20 @@ func campaignsExecute(ctx context.Context, out *output.Output, svc *campaigns.Se
176191
}
177192

178193
opts := campaigns.ExecutorOpts{
179-
Cache: svc.NewExecutionCache(flags.cacheDir),
180-
Creator: svc.NewWorkspaceCreator(flags.cacheDir, flags.cleanArchives),
181-
ClearCache: flags.clearCache,
182-
KeepLogs: flags.keepLogs,
183-
Timeout: flags.timeout,
184-
TempDir: flags.tempDir,
194+
Cache: svc.NewExecutionCache(flags.cacheDir),
195+
Creator: svc.NewWorkspaceCreator(flags.cacheDir),
196+
RepoFetcher: svc.NewRepoFetcher(flags.cacheDir, flags.cleanArchives),
197+
ClearCache: flags.clearCache,
198+
KeepLogs: flags.keepLogs,
199+
Timeout: flags.timeout,
200+
TempDir: flags.tempDir,
185201
}
186202
if flags.parallelism <= 0 {
187203
opts.Parallelism = runtime.GOMAXPROCS(0)
188204
} else {
189205
opts.Parallelism = flags.parallelism
190206
}
207+
out.VerboseLine(output.Linef("🚧", output.StyleSuccess, "Workspace creator: %T", opts.Creator))
191208
executor := svc.NewExecutor(opts)
192209

193210
if errs != nil {
@@ -210,10 +227,10 @@ func campaignsExecute(ctx context.Context, out *output.Output, svc *campaigns.Se
210227

211228
imageProgress := out.Progress([]output.ProgressBar{{
212229
Label: "Preparing container images",
213-
Max: float64(len(campaignSpec.Steps)),
230+
Max: 1.0,
214231
}}, nil)
215-
err = svc.SetDockerImages(ctx, campaignSpec, func(step int) {
216-
imageProgress.SetValue(0, float64(step))
232+
err = svc.SetDockerImages(ctx, opts.Creator, campaignSpec, func(perc float64) {
233+
imageProgress.SetValue(0, perc)
217234
})
218235
if err != nil {
219236
return "", "", err

cmd/src/campaigns_preview.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ Examples:
4545
svc := campaigns.NewService(&campaigns.ServiceOpts{
4646
AllowUnsupported: flags.allowUnsupported,
4747
Client: cfg.apiClient(flags.api, flagSet.Output()),
48+
Workspace: flags.workspace,
4849
})
4950

5051
if err := svc.DetermineFeatureFlags(ctx); err != nil {
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# This Dockerfile builds the sourcegraph/src-campaign-volume-workspace image
2+
# that we use to run curl, git, and unzip against a Docker volume when using
3+
# the volume workspace.
4+
5+
FROM alpine:3.12.3
6+
7+
# Note that we have to configure git's user.email and user.name settings to
8+
# avoid issues when committing changes. These values are not used when creating
9+
# changesets, since we only extract the diff from the container and not a full
10+
# patch, but need to be set to avoid git erroring out.
11+
RUN apk add --update curl git unzip && \
12+
git config --global user.email [email protected] && \
13+
git config --global user.name 'Sourcegraph Campaigns'
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# `src` volume workspace base image
2+
3+
Sourcegraph `src` executes campaigns using either a bind or volume workspace. In the latter case (which is the default on macOS), this utility image is used to initialise the volume workspace within Docker, and then to extract the diff used when creating the changeset.
4+
5+
This image is based on Alpine, and adds the tools we need: curl, git, and unzip.
6+
7+
For more information, please refer to the [`src-cli` repository](https://github.com/sourcegraph/src-cli/tree/main/docker/campaign-volume-workspace).
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
#!/usr/bin/env python3
2+
3+
# This is a very simple script to build and push the Docker image used by the
4+
# Docker volume workspace driver. It's normally run from the "Publish Docker
5+
# image dependencies" GitHub Action, but can be run locally if necessary.
6+
#
7+
# This script requires Python 3.8 or later, and Docker 19.03 (for buildx). You
8+
# are strongly encouraged to use Black to format this file after modifying it.
9+
#
10+
# To run it locally, you'll need to be logged into Docker Hub, and create an
11+
# image in a namespace that you have access to. For example, if your username is
12+
# "alice", you could build and push the image as follows:
13+
#
14+
# $ ./push.py -d Dockerfile -i alice/src-campaign-volume-workspace
15+
#
16+
# By default, only the "latest" tag will be built and pushed. The script refers
17+
# to the HEAD ref given to it, either via $GITHUB_REF or the -r argument. If
18+
# this is in the form refs/tags/X.Y.Z, then we'll also push X, X.Y, and X.Y.Z
19+
# tags.
20+
#
21+
# Finally, if you have your environment configured to allow multi-architecture
22+
# builds with docker buildx, you can provide a --platform argument that will be
23+
# passed through verbatim to docker buildx build. (This is how we build ARM64
24+
# builds in our CI.) For example, you could build ARM64 and AMD64 images with:
25+
#
26+
# $ ./push.py --platform linux/arm64,linux/amd64 ...
27+
#
28+
# For this to work, you will need a builder with the relevant platforms enabled.
29+
# More instructions on this can be found at
30+
# https://docs.docker.com/buildx/working-with-buildx/#build-multi-platform-images.
31+
32+
import argparse
33+
import itertools
34+
import os
35+
import subprocess
36+
37+
from typing import BinaryIO, Optional, Sequence
38+
39+
40+
def calculate_tags(ref: str) -> Sequence[str]:
41+
# The tags always include latest.
42+
tags = ["latest"]
43+
44+
# If the ref is a tag ref, then we should parse the version out and add each
45+
# component to our tag list: for example, for X.Y.Z, we want tags X, X.Y,
46+
# and X.Y.Z.
47+
if ref.startswith("refs/tags/"):
48+
tags.extend(
49+
[
50+
# Join the version components back into a string.
51+
".".join(vc)
52+
for vc in itertools.accumulate(
53+
# Split the version string into its components.
54+
ref.split("/", 2)[2].split("."),
55+
# Accumulate each component we've seen into a new list
56+
# entry.
57+
lambda vlist, v: vlist + [v],
58+
initial=[],
59+
)
60+
# We also get the initial value, so we need to skip that.
61+
if len(vc) > 0
62+
]
63+
)
64+
65+
return tags
66+
67+
68+
def docker_build(
69+
dockerfile: BinaryIO, platform: Optional[str], image: str, tags: Sequence[str]
70+
):
71+
args = ["docker", "buildx", "build", "--push"]
72+
73+
for tag in tags:
74+
args.extend(["-t", f"{image}:{tag}"])
75+
76+
if platform is not None:
77+
args.extend(["--platform", platform])
78+
79+
# Since we provide the Dockerfile via stdin, we don't need to provide it
80+
# here. (Doing so means that we don't carry an unncessary context into the
81+
# builder.)
82+
args.append("-")
83+
84+
run(args, stdin=dockerfile)
85+
86+
87+
def docker_login(username: str, password: str):
88+
run(
89+
["docker", "login", f"-u={username}", "--password-stdin"],
90+
input=password,
91+
text=True,
92+
)
93+
94+
95+
def run(args: Sequence[str], /, **kwargs) -> subprocess.CompletedProcess:
96+
print(f"+ {' '.join(args)}")
97+
return subprocess.run(args, check=True, **kwargs)
98+
99+
100+
def main():
101+
parser = argparse.ArgumentParser()
102+
parser.add_argument(
103+
"-d", "--dockerfile", required=True, help="the Dockerfile to build"
104+
)
105+
parser.add_argument("-i", "--image", required=True, help="Docker image to push")
106+
parser.add_argument(
107+
"-p",
108+
"--platform",
109+
help="platforms to build using docker buildx (if omitted, the default will be used)",
110+
)
111+
parser.add_argument(
112+
"-r",
113+
"--ref",
114+
default=os.environ.get("GITHUB_REF"),
115+
help="current ref in refs/heads/... or refs/tags/... form",
116+
)
117+
args = parser.parse_args()
118+
119+
tags = calculate_tags(args.ref)
120+
print(f"will push tags: {', '.join(tags)}")
121+
122+
print("logging into Docker Hub")
123+
try:
124+
docker_login(os.environ["DOCKER_USERNAME"], os.environ["DOCKER_PASSWORD"])
125+
except KeyError as e:
126+
print(f"error retrieving environment variables: {e}")
127+
raise
128+
129+
print("building and pushing image")
130+
docker_build(open(args.dockerfile, "rb"), args.platform, args.image, tags)
131+
132+
print("success!")
133+
134+
135+
if __name__ == "__main__":
136+
main()

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ require (
66
github.com/Masterminds/semver v1.5.0
77
github.com/dustin/go-humanize v1.0.0
88
github.com/efritz/pentimento v0.0.0-20190429011147-ade47d831101
9+
github.com/gobwas/glob v0.2.3
910
github.com/google/go-cmp v0.5.2
1011
github.com/hashicorp/errwrap v1.1.0 // indirect
1112
github.com/hashicorp/go-multierror v1.1.0

0 commit comments

Comments
 (0)