Skip to content

[ONPREM-1829] [HACKWEEK] Add initial support for Windows containers #96

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 26 additions & 4 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ executors:
docker:
- image: *goimage
resource_class: circleci-runner/rum-large
windows:
machine:
image: windows-server-2022-gui:current
shell: bash.exe -login
resource_class: windows.medium
ccc:
docker:
- image: circleci/command-convenience:0.1
Expand All @@ -46,7 +51,10 @@ workflows:
- equal: [ false, << pipeline.parameters.trigger_nightly_workflow >> ]
jobs:
- lint
- test
- test:
matrix:
parameters:
os: [ go, windows ]
- build
- scan:
context: [ org-global ]
Expand All @@ -58,7 +66,7 @@ workflows:
context: [ org-global ]
- images:
context: [ org-global, runner-image-signing ]
requires: [ lint, test, build, scan, vuln-scanner/vuln_scan ]
requires: [ lint, build, scan, vuln-scanner/vuln_scan ]
- smoke-tests:
context: [ org-global, runner-smoke-tests ]
requires: [ images ]
Expand Down Expand Up @@ -116,12 +124,26 @@ jobs:
- notify_failing_main

test:
executor: go
parameters:
os:
type: string
executor: << parameters.os >>
steps:
- setup
- when:
condition:
equal: [ << parameters.os >>, "windows" ]
steps:
- run:
name: "Install GCC, since we need cgo for the race detector"
command: |
choco install mingw -y
echo 'export PATH="$PATH:/c/ProgramData/mingw64/mingw64/bin"' >> ~/.bash_profile
source ~/.bash_profile
gcc -v
- with-go-cache:
steps:
- run: ./do test ./... -count 3
- run: ./do test ./... -count 2
- notify_failing_main

build:
Expand Down
5 changes: 4 additions & 1 deletion .goreleaser/binaries.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@ builds:
- -X github.com/circleci/runner-init/cmd.Version={{.Env.BUILD_VERSION}}
- -X github.com/circleci/runner-init/cmd.Date={{.Date}}
env: [CGO_ENABLED=0]
goos: [linux]
goos: [linux, windows]
goarch: [amd64, arm64]
ignore:
- goos: windows
goarch: arm64
no_unique_dist_dir: true

- id: fake-task-agent
Expand Down
52 changes: 52 additions & 0 deletions .goreleaser/dockers.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,55 @@ dockers:
extra_files:
- ./target/bin/linux/arm64/orchestrator

# Windows Images: Note that Windows containers require a container OS and have nuanced version compatibility
# (see https://learn.microsoft.com/en-us/virtualization/windowscontainers/deploy-containers/version-compatibility).
# Therefore, we target various versions for the base image. Currently, we provide images for Server 2019, 2022, and 2025.
- id: init-windows-server-2019
image_templates: ["circleci/runner-init:agent-windows-server-2019{{.Env.IMAGE_TAG_SUFFIX}}"]
dockerfile: ./docker/windows.Dockerfile
skip_push: "true" # we push during the build step since we cannot load a Windows image on Linux
use: buildx
build_flag_templates:
- "--builder=circleci-runner-init-windows-builder"
- "--build-arg=PICARD_VERSION={{.Env.PICARD_VERSION}}"
- "--build-arg=WINDOWS_VERSION=ltsc2019"
- "--platform=windows/amd64"
- "--load=false"
- "--push={{.Env.PUSH_WINDOWS}}"
- "--provenance=false"
extra_files:
- ./target/bin/windows/amd64/orchestrator.exe
- id: init-windows-server-2022
image_templates: ["circleci/runner-init:agent-windows-server-2022{{.Env.IMAGE_TAG_SUFFIX}}"]
dockerfile: ./docker/windows.Dockerfile
skip_push: "true" # we push during the build step since we cannot load a Windows image on Linux
use: buildx
build_flag_templates:
- "--builder=circleci-runner-init-windows-builder"
- "--build-arg=PICARD_VERSION={{.Env.PICARD_VERSION}}"
- "--build-arg=WINDOWS_VERSION=ltsc2022"
- "--platform=windows/amd64"
- "--load=false"
- "--push={{.Env.PUSH_WINDOWS}}"
- "--provenance=false"
extra_files:
- ./target/bin/windows/amd64/orchestrator.exe
- id: init-windows-server-2025
image_templates: ["circleci/runner-init:agent-windows-server-2025{{.Env.IMAGE_TAG_SUFFIX}}"]
dockerfile: ./docker/windows.Dockerfile
skip_push: "true" # we push during the build step since we cannot load a Windows image on Linux
use: buildx
build_flag_templates:
- "--builder=circleci-runner-init-windows-builder"
- "--build-arg=PICARD_VERSION={{.Env.PICARD_VERSION}}"
- "--build-arg=WINDOWS_VERSION=ltsc2025"
- "--platform=windows/amd64"
- "--load=false"
- "--push={{.Env.PUSH_WINDOWS}}"
- "--provenance=false"
extra_files:
- ./target/bin/windows/amd64/orchestrator.exe

# Image used in the `circleci-runner` acceptance tests
- id: testinit-amd64
image_templates: ["circleci/runner-init:test-agent-amd64"]
Expand All @@ -48,6 +97,9 @@ docker_manifests:
image_templates:
- "circleci/runner-init:agent-amd64{{.Env.IMAGE_TAG_SUFFIX}}"
- "circleci/runner-init:agent-arm64{{.Env.IMAGE_TAG_SUFFIX}}"
- "circleci/runner-init:agent-windows-server-2019{{.Env.IMAGE_TAG_SUFFIX}}"
- "circleci/runner-init:agent-windows-server-2022{{.Env.IMAGE_TAG_SUFFIX}}"
- "circleci/runner-init:agent-windows-server-2025{{.Env.IMAGE_TAG_SUFFIX}}"
skip_push: "{{.Env.SKIP_PUSH}}"

- name_template: "circleci/runner-init:test-agent"
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ By following these guidelines, we can easily determine which changes should be i

- [#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.
- [#98](https://github.com/circleci/runner-init/pull/98) [INTERNAL] A small refactor to the builds and Dockerfiles in preparation for adding Windows support.
- [#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.
- [#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.
- [#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.
- [#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.
Expand Down
13 changes: 11 additions & 2 deletions acceptance/acceptance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package acceptance
import (
"fmt"
"os"
"path/filepath"
"runtime"
"testing"

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

// A little hack to get around limitations of the test runner on positional arguments
func createRunTaskScript() error {
script := "#!/bin/bash\nexec " + orchestratorTestBinary + " run-task"
scriptPath := binariesPath + "/orchestratorRunTask.sh"
var script string
var scriptPath string

if runtime.GOOS == "windows" {
script = "@echo off\n" + orchestratorTestBinary + " run-task"
scriptPath = filepath.Join(binariesPath, "orchestratorRunTask.bat")
} else {
script = "#!/bin/bash\nexec " + orchestratorTestBinary + " run-task"
scriptPath = filepath.Join(binariesPath, "orchestratorRunTask.sh")
}

if err := os.WriteFile(scriptPath, []byte(script), 0750); err != nil { //nolint:gosec
return err
Expand Down
37 changes: 27 additions & 10 deletions acceptance/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package acceptance

import (
"os"
"path/filepath"
"runtime"
"testing"
"time"

Expand All @@ -13,11 +15,11 @@ import (
func TestInit(t *testing.T) {
srcDir := createMockSourceFiles(t)
destDir := t.TempDir()
orchSrc := srcDir + "/orchestrator"
orchDest := destDir + "/orchestrator"
agentSrc := srcDir + "/circleci-agent"
agentDest := destDir + "/circleci-agent"
circleciDest := destDir + "/circleci"
orchSrc := path(t, srcDir, "orchestrator")
orchDest := path(t, destDir, "orchestrator")
agentSrc := path(t, srcDir, "circleci-agent")
agentDest := path(t, destDir, "circleci-agent")
circleciDest := path(t, destDir, "circleci")

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

agentLink, err := os.Readlink(circleciDest)
assert.NilError(t, err)
assert.Check(t, cmp.DeepEqual(agentLink, agentDest))
if runtime.GOOS == "windows" {
assertFileIsCopied(t, agentSrc, circleciDest)
} else {
agentLink, err := os.Readlink(circleciDest)
assert.NilError(t, err)
assert.Check(t, cmp.DeepEqual(agentLink, agentDest))
}
})
}

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

srcDir := t.TempDir()

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

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

return srcDir
Expand All @@ -77,3 +83,14 @@ func assertFileIsCopied(t *testing.T, src, dest string) {
assert.NilError(t, err)
assert.Check(t, cmp.DeepEqual(srcContents, destContents), "files should have same contents")
}

func path(t *testing.T, a, b string) string {
t.Helper()

p := filepath.Join(a, b)

if runtime.GOOS == "windows" {
return p + ".exe"
}
return p
}
11 changes: 7 additions & 4 deletions acceptance/task_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package acceptance
import (
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"time"

Expand All @@ -11,18 +13,18 @@ import (
)

func TestRunTask(t *testing.T) {
readinessFilePath := t.TempDir() + "/ready"
readinessFilePath := filepath.Join(t.TempDir(), "ready")
goodConfig := fmt.Sprintf(`
{
"cmd": [],
"enable_unsafe_retries": false,
"token": "testtoken",
"readiness_file_path": "%s",
"readiness_file_path": "%v",
"task_agent_path": "%v",
"runner_api_base_url": "https://runner.circleci.com",
"allocation": "testallocation",
"max_run_time": 60000000000
}`, readinessFilePath, taskAgentBinary)
}`, strings.ReplaceAll(readinessFilePath, `\`, `\\`), strings.ReplaceAll(taskAgentBinary, `\`, `\\`))

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

go func() {
_, err := os.Create(readinessFilePath) //nolint:gosec
f, err := os.Create(readinessFilePath) //nolint:gosec
defer func() { assert.NilError(t, f.Close()) }()
assert.NilError(t, err)
}()

Expand Down
5 changes: 5 additions & 0 deletions cmd/orchestrator/help_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"bytes"
"runtime"
"testing"

"github.com/alecthomas/kong"
Expand All @@ -11,6 +12,10 @@ import (
)

func TestHelp(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("Can't be bothered to add golden files for Windows")
}

cli := &cli{}

var tests = []struct {
Expand Down
8 changes: 7 additions & 1 deletion do
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,15 @@ help_images="Build and push the Docker images and manifests."
images() {
set -x

docker buildx create --name circleci-runner-init-windows-builder \
--driver=docker-container --driver-opt image=moby/buildkit:rootless || true

skip="${SKIP_PUSH:-true}"
[ "${SKIP_PUSH:-true}" = "true" ] && push_windows="false" || push_windows="true"

SKIP_PUSH="${skip}" \
SKIP_PUSH_TEST_AGENT="${SKIP_PUSH_TEST_AGENT:-${skip}}" \
PUSH_WINDOWS="${push_windows}" \
IMAGE_TAG_SUFFIX="${IMAGE_TAG_SUFFIX:-""}" \
PICARD_VERSION="${PICARD_VERSION:-agent}" \
go tool goreleaser \
Expand Down Expand Up @@ -92,7 +98,7 @@ help_test="Run the tests"
test() {
mkdir -p "${reportDir}"
# -count=1 is used to forcibly disable test result caching
go tool gotestsum --junitfile="${reportDir}/junit.xml" -- -race -count=1 "${@:-./...}"
CGO_ENABLED=1 go tool gotestsum --junitfile="${reportDir}/junit.xml" -- -race -count=1 "${@:-./...}"
}

# This variable is used, but shellcheck can't tell.
Expand Down
2 changes: 1 addition & 1 deletion docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ FROM scratch AS builder

ARG TARGETPLATFORM

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

FROM scratch
Expand Down
2 changes: 1 addition & 1 deletion docker/test.Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM scratch as builder
FROM scratch AS builder

ARG TARGETPLATFORM

Expand Down
17 changes: 17 additions & 0 deletions docker/windows.Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
ARG PICARD_VERSION=agent
ARG WINDOWS_VERSION

FROM --platform=${BUILDPLATFORM} circleci/picard:${PICARD_VERSION} AS task-agent-image

FROM --platform=${BUILDPLATFORM} scratch AS builder

ARG TARGETPLATFORM

COPY --from=task-agent-image /opt/circleci/${TARGETPLATFORM}/circleci-agent /circleci-agent.exe
COPY ./target/bin/${TARGETPLATFORM}/orchestrator.exe /

FROM mcr.microsoft.com/windows/nanoserver:${WINDOWS_VERSION}

COPY --from=builder / /

ENTRYPOINT ["/orchestrator", "init"]
9 changes: 9 additions & 0 deletions init/binaries_unix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
//go:build !windows

package init

const (
binOrchestrator = "orchestrator"
binCircleciAgent = "circleci-agent"
binCircleci = "circleci"
)
7 changes: 7 additions & 0 deletions init/binaries_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package init

const (
binOrchestrator = "orchestrator.exe"
binCircleciAgent = "circleci-agent.exe"
binCircleci = "circleci.exe"
)
Loading