Skip to content
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

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

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
40 changes: 31 additions & 9 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
@@ -15,10 +15,15 @@ orbs:
vuln-scanner: cci-internal/snyk-vuln-scanner@0.9.1

executors:
go:
docker-go:
docker:
- image: cimg/go:1.24
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
@@ -39,7 +44,10 @@ workflows:
- equal: [ false, << pipeline.parameters.trigger_nightly_workflow >> ]
jobs:
- lint
- test
- test:
matrix:
parameters:
os: [ docker-go, windows ]
- build
- scan:
context: [ org-global ]
@@ -52,7 +60,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 ]
@@ -72,7 +80,7 @@ workflows:

jobs:
lint:
executor: go
executor: docker-go
steps:
- setup
- with-go-cache:
@@ -110,16 +118,30 @@ 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:
executor: go
executor: docker-go
steps:
- setup
- with-go-cache:
@@ -202,7 +224,7 @@ jobs:
oss-acknowledgement-check:
environment:
SNYK_LICENSE_RESULT_FILE: "scan_results.csv"
executor: go
executor: docker-go
steps:
- checkout
- run:
@@ -262,7 +284,7 @@ jobs:


smoke-tests:
executor: go
executor: docker-go
steps:
- checkout
- attach_workspace:
5 changes: 4 additions & 1 deletion .goreleaser/binaries.yaml
Original file line number Diff line number Diff line change
@@ -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
52 changes: 52 additions & 0 deletions .goreleaser/dockers.yaml
Original file line number Diff line number Diff line change
@@ -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"]
@@ -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"
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -11,6 +11,7 @@ By following these guidelines, we can easily determine which changes should be i
## Edge

- [#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.
13 changes: 11 additions & 2 deletions acceptance/acceptance_test.go
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@ package acceptance
import (
"fmt"
"os"
"path/filepath"
"runtime"
"testing"

@@ -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
37 changes: 27 additions & 10 deletions acceptance/init_test.go
Original file line number Diff line number Diff line change
@@ -2,6 +2,8 @@ package acceptance

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

@@ -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,
@@ -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))
}
})
}

@@ -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
@@ -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
@@ -3,6 +3,8 @@ package acceptance
import (
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"time"

@@ -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",
@@ -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)
}()

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

import (
"bytes"
"runtime"
"testing"

"github.com/alecthomas/kong"
@@ -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 {
8 changes: 7 additions & 1 deletion do
Original file line number Diff line number Diff line change
@@ -36,9 +36,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 \
@@ -98,7 +104,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.
2 changes: 1 addition & 1 deletion docker/Dockerfile
Original file line number Diff line number Diff line change
@@ -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
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

Loading
Oops, something went wrong.