Skip to content

Commit 5bc3fa9

Browse files
[ONPREM-1829] [HACKWEEK] Add initial support for Windows containers (#96)
* Support Windows containers * Start building binaries and images * Set executable file extension * Try 2019 * Only copy binaries on Windows Appears we don't have privileges to create symlinks * Add comments * Add changelog entry * Support multiple versions * Refactor platform-specific code * Flesh out changelog entry
1 parent 7565019 commit 5bc3fa9

24 files changed

+389
-111
lines changed

.circleci/config.yml

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ executors:
2626
docker:
2727
- image: *goimage
2828
resource_class: circleci-runner/rum-large
29+
windows:
30+
machine:
31+
image: windows-server-2022-gui:current
32+
shell: bash.exe -login
33+
resource_class: windows.medium
2934
ccc:
3035
docker:
3136
- image: circleci/command-convenience:0.1
@@ -46,7 +51,10 @@ workflows:
4651
- equal: [ false, << pipeline.parameters.trigger_nightly_workflow >> ]
4752
jobs:
4853
- lint
49-
- test
54+
- test:
55+
matrix:
56+
parameters:
57+
os: [ go, windows ]
5058
- build
5159
- scan:
5260
context: [ org-global ]
@@ -58,7 +66,7 @@ workflows:
5866
context: [ org-global ]
5967
- images:
6068
context: [ org-global, runner-image-signing ]
61-
requires: [ lint, test, build, scan, vuln-scanner/vuln_scan ]
69+
requires: [ lint, build, scan, vuln-scanner/vuln_scan ]
6270
- smoke-tests:
6371
context: [ org-global, runner-smoke-tests ]
6472
requires: [ images ]
@@ -116,12 +124,26 @@ jobs:
116124
- notify_failing_main
117125

118126
test:
119-
executor: go
127+
parameters:
128+
os:
129+
type: string
130+
executor: << parameters.os >>
120131
steps:
121132
- setup
133+
- when:
134+
condition:
135+
equal: [ << parameters.os >>, "windows" ]
136+
steps:
137+
- run:
138+
name: "Install GCC, since we need cgo for the race detector"
139+
command: |
140+
choco install mingw -y
141+
echo 'export PATH="$PATH:/c/ProgramData/mingw64/mingw64/bin"' >> ~/.bash_profile
142+
source ~/.bash_profile
143+
gcc -v
122144
- with-go-cache:
123145
steps:
124-
- run: ./do test ./... -count 3
146+
- run: ./do test ./... -count 2
125147
- notify_failing_main
126148

127149
build:

.goreleaser/binaries.yaml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,11 @@ builds:
1111
- -X github.com/circleci/runner-init/cmd.Version={{.Env.BUILD_VERSION}}
1212
- -X github.com/circleci/runner-init/cmd.Date={{.Date}}
1313
env: [CGO_ENABLED=0]
14-
goos: [linux]
14+
goos: [linux, windows]
1515
goarch: [amd64, arm64]
16+
ignore:
17+
- goos: windows
18+
goarch: arm64
1619
no_unique_dist_dir: true
1720

1821
- id: fake-task-agent

.goreleaser/dockers.yaml

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,55 @@ dockers:
3131
extra_files:
3232
- ./target/bin/linux/arm64/orchestrator
3333

34+
# Windows Images: Note that Windows containers require a container OS and have nuanced version compatibility
35+
# (see https://learn.microsoft.com/en-us/virtualization/windowscontainers/deploy-containers/version-compatibility).
36+
# Therefore, we target various versions for the base image. Currently, we provide images for Server 2019, 2022, and 2025.
37+
- id: init-windows-server-2019
38+
image_templates: ["circleci/runner-init:agent-windows-server-2019{{.Env.IMAGE_TAG_SUFFIX}}"]
39+
dockerfile: ./docker/windows.Dockerfile
40+
skip_push: "true" # we push during the build step since we cannot load a Windows image on Linux
41+
use: buildx
42+
build_flag_templates:
43+
- "--builder=circleci-runner-init-windows-builder"
44+
- "--build-arg=PICARD_VERSION={{.Env.PICARD_VERSION}}"
45+
- "--build-arg=WINDOWS_VERSION=ltsc2019"
46+
- "--platform=windows/amd64"
47+
- "--load=false"
48+
- "--push={{.Env.PUSH_WINDOWS}}"
49+
- "--provenance=false"
50+
extra_files:
51+
- ./target/bin/windows/amd64/orchestrator.exe
52+
- id: init-windows-server-2022
53+
image_templates: ["circleci/runner-init:agent-windows-server-2022{{.Env.IMAGE_TAG_SUFFIX}}"]
54+
dockerfile: ./docker/windows.Dockerfile
55+
skip_push: "true" # we push during the build step since we cannot load a Windows image on Linux
56+
use: buildx
57+
build_flag_templates:
58+
- "--builder=circleci-runner-init-windows-builder"
59+
- "--build-arg=PICARD_VERSION={{.Env.PICARD_VERSION}}"
60+
- "--build-arg=WINDOWS_VERSION=ltsc2022"
61+
- "--platform=windows/amd64"
62+
- "--load=false"
63+
- "--push={{.Env.PUSH_WINDOWS}}"
64+
- "--provenance=false"
65+
extra_files:
66+
- ./target/bin/windows/amd64/orchestrator.exe
67+
- id: init-windows-server-2025
68+
image_templates: ["circleci/runner-init:agent-windows-server-2025{{.Env.IMAGE_TAG_SUFFIX}}"]
69+
dockerfile: ./docker/windows.Dockerfile
70+
skip_push: "true" # we push during the build step since we cannot load a Windows image on Linux
71+
use: buildx
72+
build_flag_templates:
73+
- "--builder=circleci-runner-init-windows-builder"
74+
- "--build-arg=PICARD_VERSION={{.Env.PICARD_VERSION}}"
75+
- "--build-arg=WINDOWS_VERSION=ltsc2025"
76+
- "--platform=windows/amd64"
77+
- "--load=false"
78+
- "--push={{.Env.PUSH_WINDOWS}}"
79+
- "--provenance=false"
80+
extra_files:
81+
- ./target/bin/windows/amd64/orchestrator.exe
82+
3483
# Image used in the `circleci-runner` acceptance tests
3584
- id: testinit-amd64
3685
image_templates: ["circleci/runner-init:test-agent-amd64"]
@@ -48,6 +97,9 @@ docker_manifests:
4897
image_templates:
4998
- "circleci/runner-init:agent-amd64{{.Env.IMAGE_TAG_SUFFIX}}"
5099
- "circleci/runner-init:agent-arm64{{.Env.IMAGE_TAG_SUFFIX}}"
100+
- "circleci/runner-init:agent-windows-server-2019{{.Env.IMAGE_TAG_SUFFIX}}"
101+
- "circleci/runner-init:agent-windows-server-2022{{.Env.IMAGE_TAG_SUFFIX}}"
102+
- "circleci/runner-init:agent-windows-server-2025{{.Env.IMAGE_TAG_SUFFIX}}"
51103
skip_push: "{{.Env.SKIP_PUSH}}"
52104

53105
- name_template: "circleci/runner-init:test-agent"

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ By following these guidelines, we can easily determine which changes should be i
1212

1313
- [#133](https://github.com/circleci/runner-init/pull/133) Don't re-handle task errors. If GOAT handles a task error (either with an infra-fail or retry), don't exit with a nonzero status code. Doing so causes container agent to overwrite the original error message in the UI.
1414
- [#98](https://github.com/circleci/runner-init/pull/98) [INTERNAL] A small refactor to the builds and Dockerfiles in preparation for adding Windows support.
15+
- [#96](https://github.com/circleci/runner-init/pull/96) [INTERNAL] Introduce initial support for Windows containers. Additional follow-up work is needed to fully support Windows, including the implementation of a smoke test and supporting service containers on Windows, which is a known limitation at this time.
1516
- [#97](https://github.com/circleci/runner-init/pull/97) Add timeout for the "wait-for-readiness" check on startup. This is so that GOAT doesn't wait indefinitely if there's a problem, ensuring a timely reaping of the task pod.
1617
- [#89](https://github.com/circleci/runner-init/pull/89) [INTERNAL] Add an option to wait for a readiness file, which is used via a shared volume to signal the readiness of all containers in the task pod.
1718
- [#71](https://github.com/circleci/runner-init/pull/71) [INTERNAL] Bump `ex` to `v1.0.12715-ada3e6b` and Go to `1.23`, which also required a bump in `golangci-lint` to `1.62.0` and addressing new lint errors that came along with that.

acceptance/acceptance_test.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package acceptance
33
import (
44
"fmt"
55
"os"
6+
"path/filepath"
67
"runtime"
78
"testing"
89

@@ -69,8 +70,16 @@ func runTests(m *testing.M) (int, error) {
6970

7071
// A little hack to get around limitations of the test runner on positional arguments
7172
func createRunTaskScript() error {
72-
script := "#!/bin/bash\nexec " + orchestratorTestBinary + " run-task"
73-
scriptPath := binariesPath + "/orchestratorRunTask.sh"
73+
var script string
74+
var scriptPath string
75+
76+
if runtime.GOOS == "windows" {
77+
script = "@echo off\n" + orchestratorTestBinary + " run-task"
78+
scriptPath = filepath.Join(binariesPath, "orchestratorRunTask.bat")
79+
} else {
80+
script = "#!/bin/bash\nexec " + orchestratorTestBinary + " run-task"
81+
scriptPath = filepath.Join(binariesPath, "orchestratorRunTask.sh")
82+
}
7483

7584
if err := os.WriteFile(scriptPath, []byte(script), 0750); err != nil { //nolint:gosec
7685
return err

acceptance/init_test.go

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package acceptance
22

33
import (
44
"os"
5+
"path/filepath"
6+
"runtime"
57
"testing"
68
"time"
79

@@ -13,11 +15,11 @@ import (
1315
func TestInit(t *testing.T) {
1416
srcDir := createMockSourceFiles(t)
1517
destDir := t.TempDir()
16-
orchSrc := srcDir + "/orchestrator"
17-
orchDest := destDir + "/orchestrator"
18-
agentSrc := srcDir + "/circleci-agent"
19-
agentDest := destDir + "/circleci-agent"
20-
circleciDest := destDir + "/circleci"
18+
orchSrc := path(t, srcDir, "orchestrator")
19+
orchDest := path(t, destDir, "orchestrator")
20+
agentSrc := path(t, srcDir, "circleci-agent")
21+
agentDest := path(t, destDir, "circleci-agent")
22+
circleciDest := path(t, destDir, "circleci")
2123

2224
r := runner.New(
2325
"SOURCE="+srcDir,
@@ -41,9 +43,13 @@ func TestInit(t *testing.T) {
4143
assertFileIsCopied(t, orchSrc, orchDest)
4244
assertFileIsCopied(t, agentSrc, agentDest)
4345

44-
agentLink, err := os.Readlink(circleciDest)
45-
assert.NilError(t, err)
46-
assert.Check(t, cmp.DeepEqual(agentLink, agentDest))
46+
if runtime.GOOS == "windows" {
47+
assertFileIsCopied(t, agentSrc, circleciDest)
48+
} else {
49+
agentLink, err := os.Readlink(circleciDest)
50+
assert.NilError(t, err)
51+
assert.Check(t, cmp.DeepEqual(agentLink, agentDest))
52+
}
4753
})
4854
}
4955

@@ -53,10 +59,10 @@ func createMockSourceFiles(t *testing.T) string {
5359

5460
srcDir := t.TempDir()
5561

56-
err := os.WriteFile(srcDir+"/orchestrator", []byte("mock orchestrator data"), 0600)
62+
err := os.WriteFile(path(t, srcDir, "orchestrator"), []byte("mock orchestrator data"), 0600)
5763
assert.NilError(t, err)
5864

59-
err = os.WriteFile(srcDir+"/circleci-agent", []byte("mock agent data"), 0600)
65+
err = os.WriteFile(path(t, srcDir, "circleci-agent"), []byte("mock agent data"), 0600)
6066
assert.NilError(t, err)
6167

6268
return srcDir
@@ -77,3 +83,14 @@ func assertFileIsCopied(t *testing.T, src, dest string) {
7783
assert.NilError(t, err)
7884
assert.Check(t, cmp.DeepEqual(srcContents, destContents), "files should have same contents")
7985
}
86+
87+
func path(t *testing.T, a, b string) string {
88+
t.Helper()
89+
90+
p := filepath.Join(a, b)
91+
92+
if runtime.GOOS == "windows" {
93+
return p + ".exe"
94+
}
95+
return p
96+
}

acceptance/task_test.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package acceptance
33
import (
44
"fmt"
55
"os"
6+
"path/filepath"
7+
"strings"
68
"testing"
79
"time"
810

@@ -11,18 +13,18 @@ import (
1113
)
1214

1315
func TestRunTask(t *testing.T) {
14-
readinessFilePath := t.TempDir() + "/ready"
16+
readinessFilePath := filepath.Join(t.TempDir(), "ready")
1517
goodConfig := fmt.Sprintf(`
1618
{
1719
"cmd": [],
1820
"enable_unsafe_retries": false,
1921
"token": "testtoken",
20-
"readiness_file_path": "%s",
22+
"readiness_file_path": "%v",
2123
"task_agent_path": "%v",
2224
"runner_api_base_url": "https://runner.circleci.com",
2325
"allocation": "testallocation",
2426
"max_run_time": 60000000000
25-
}`, readinessFilePath, taskAgentBinary)
27+
}`, strings.ReplaceAll(readinessFilePath, `\`, `\\`), strings.ReplaceAll(taskAgentBinary, `\`, `\\`))
2628

2729
r := runner.New(
2830
"CIRCLECI_GOAT_SHUTDOWN_DELAY=10s",
@@ -37,7 +39,8 @@ func TestRunTask(t *testing.T) {
3739
})
3840

3941
go func() {
40-
_, err := os.Create(readinessFilePath) //nolint:gosec
42+
f, err := os.Create(readinessFilePath) //nolint:gosec
43+
defer func() { assert.NilError(t, f.Close()) }()
4144
assert.NilError(t, err)
4245
}()
4346

cmd/orchestrator/help_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package main
22

33
import (
44
"bytes"
5+
"runtime"
56
"testing"
67

78
"github.com/alecthomas/kong"
@@ -11,6 +12,10 @@ import (
1112
)
1213

1314
func TestHelp(t *testing.T) {
15+
if runtime.GOOS == "windows" {
16+
t.Skip("Can't be bothered to add golden files for Windows")
17+
}
18+
1419
cli := &cli{}
1520

1621
var tests = []struct {

do

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,15 @@ help_images="Build and push the Docker images and manifests."
3030
images() {
3131
set -x
3232

33+
docker buildx create --name circleci-runner-init-windows-builder \
34+
--driver=docker-container --driver-opt image=moby/buildkit:rootless || true
35+
3336
skip="${SKIP_PUSH:-true}"
37+
[ "${SKIP_PUSH:-true}" = "true" ] && push_windows="false" || push_windows="true"
38+
3439
SKIP_PUSH="${skip}" \
3540
SKIP_PUSH_TEST_AGENT="${SKIP_PUSH_TEST_AGENT:-${skip}}" \
41+
PUSH_WINDOWS="${push_windows}" \
3642
IMAGE_TAG_SUFFIX="${IMAGE_TAG_SUFFIX:-""}" \
3743
PICARD_VERSION="${PICARD_VERSION:-agent}" \
3844
go tool goreleaser \
@@ -92,7 +98,7 @@ help_test="Run the tests"
9298
test() {
9399
mkdir -p "${reportDir}"
94100
# -count=1 is used to forcibly disable test result caching
95-
go tool gotestsum --junitfile="${reportDir}/junit.xml" -- -race -count=1 "${@:-./...}"
101+
CGO_ENABLED=1 go tool gotestsum --junitfile="${reportDir}/junit.xml" -- -race -count=1 "${@:-./...}"
96102
}
97103

98104
# This variable is used, but shellcheck can't tell.

docker/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ FROM scratch AS builder
66

77
ARG TARGETPLATFORM
88

9-
COPY --from=task-agent-image /opt/circleci/${TARGETPLATFORM}/circleci-agent /
9+
COPY --from=task-agent-image /opt/circleci/${TARGETPLATFORM}/circleci-agent* /
1010
COPY ./target/bin/${TARGETPLATFORM}/orchestrator /
1111

1212
FROM scratch

0 commit comments

Comments
 (0)