Skip to content

Commit

Permalink
Docker Backend: fully support windows container (#4381) (#4464)
Browse files Browse the repository at this point in the history
  • Loading branch information
6543 authored Nov 27, 2024
1 parent cc3f041 commit f85192b
Show file tree
Hide file tree
Showing 4 changed files with 207 additions and 2 deletions.
2 changes: 2 additions & 0 deletions pipeline/backend/docker/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ const minVolumeComponents = 2

// returns a container configuration.
func (e *docker) toConfig(step *types.Step) *container.Config {
e.windowsPathPatch(step)

config := &container.Config{
Image: step.Image,
Labels: map[string]string{
Expand Down
84 changes: 82 additions & 2 deletions pipeline/backend/docker/convert_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@
package docker

import (
"encoding/base64"
"reflect"
"sort"
"strings"
"testing"

"github.com/docker/docker/api/types"
Expand Down Expand Up @@ -92,7 +94,7 @@ func TestSplitVolumeParts(t *testing.T) {
}

func TestToConfigSmall(t *testing.T) {
engine := docker{info: types.Info{OSType: "linux/riscv64"}}
engine := docker{info: types.Info{OSType: "linux", Architecture: "riscv64"}}

conf := engine.toConfig(&backend.Step{
Name: "test",
Expand Down Expand Up @@ -122,7 +124,9 @@ func TestToConfigSmall(t *testing.T) {
}

func TestToConfigFull(t *testing.T) {
engine := docker{info: types.Info{OSType: "linux/riscv64"}}
engine := docker{
info: types.Info{OSType: "linux", Architecture: "riscv64"},
}

conf := engine.toConfig(&backend.Step{
Name: "test",
Expand Down Expand Up @@ -182,3 +186,79 @@ func TestToConfigFull(t *testing.T) {
},
}, conf)
}

func TestToWindowsConfig(t *testing.T) {
engine := docker{
info: types.Info{OSType: "windows", Architecture: "x86_64"},
}

conf := engine.toConfig(&backend.Step{
Name: "test",
UUID: "23434553",
Type: backend.StepTypeCommands,
Image: "golang:1.2.3",
WorkingDir: "/src/abc",
Environment: map[string]string{
"TAGS": "sqlite",
"CI_WORKSPACE": "/src",
},
Commands: []string{"go test", "go vet ./..."},
ExtraHosts: []backend.HostAlias{{Name: "t", IP: "1.2.3.4"}},
Volumes: []string{"wp_default_abc:/src", "/cache:/cache/some/more", "test:/test"},
Networks: []backend.Conn{{Name: "extra-net", Aliases: []string{"extra.net"}}},
DNS: []string{"9.9.9.9", "8.8.8.8"},
Failure: "fail",
AuthConfig: backend.Auth{Username: "user", Password: "123456"},
NetworkMode: "nat",
Ports: []backend.Port{{Number: 21}, {Number: 22}},
})

assert.NotNil(t, conf)
sort.Strings(conf.Env)
assert.EqualValues(t, &container.Config{
Image: "golang:1.2.3",
WorkingDir: "C:/src/abc",
AttachStdout: true,
AttachStderr: true,
Entrypoint: []string{"powershell", "-noprofile", "-noninteractive", "-command", "[System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Env:CI_SCRIPT)) | iex"},
Labels: map[string]string{
"wp_step": "test",
"wp_uuid": "23434553",
},
Env: []string{
"CI_SCRIPT=CiRFcnJvckFjdGlvblByZWZlcmVuY2UgPSAnU3RvcCc7CiZjbWQgL2MgIm1rZGlyIGM6XHJvb3QiOwppZiAoJEVudjpDSV9ORVRSQ19NQUNISU5FKSB7CiRuZXRyYz1bc3RyaW5nXTo6Rm9ybWF0KCJ7MH1cX25ldHJjIiwkRW52OkhPTUUpOwoibWFjaGluZSAkRW52OkNJX05FVFJDX01BQ0hJTkUiID4+ICRuZXRyYzsKImxvZ2luICRFbnY6Q0lfTkVUUkNfVVNFUk5BTUUiID4+ICRuZXRyYzsKInBhc3N3b3JkICRFbnY6Q0lfTkVUUkNfUEFTU1dPUkQiID4+ICRuZXRyYzsKfTsKW0Vudmlyb25tZW50XTo6U2V0RW52aXJvbm1lbnRWYXJpYWJsZSgiQ0lfTkVUUkNfUEFTU1dPUkQiLCRudWxsKTsKW0Vudmlyb25tZW50XTo6U2V0RW52aXJvbm1lbnRWYXJpYWJsZSgiQ0lfU0NSSVBUIiwkbnVsbCk7CgpXcml0ZS1PdXRwdXQgKCcrICJnbyB0ZXN0IicpOwomIGdvIHRlc3Q7IGlmICgkTEFTVEVYSVRDT0RFIC1uZSAwKSB7ZXhpdCAkTEFTVEVYSVRDT0RFfQoKV3JpdGUtT3V0cHV0ICgnKyAiZ28gdmV0IC4vLi4uIicpOwomIGdvIHZldCAuLy4uLjsgaWYgKCRMQVNURVhJVENPREUgLW5lIDApIHtleGl0ICRMQVNURVhJVENPREV9Cgo=",
"CI_WORKSPACE=C:/src",
`HOME=c:\root`,
"SHELL=powershell.exe",
"TAGS=sqlite",
},
Volumes: map[string]struct{}{
"C:/cache/some/more": {},
"C:/src": {},
"C:/test": {},
},
}, conf)

ciScript, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(conf.Env[0], "CI_SCRIPT="))
if assert.NoError(t, err) {
assert.EqualValues(t, `
$ErrorActionPreference = 'Stop';
&cmd /c "mkdir c:\root";
if ($Env:CI_NETRC_MACHINE) {
$netrc=[string]::Format("{0}\_netrc",$Env:HOME);
"machine $Env:CI_NETRC_MACHINE" >> $netrc;
"login $Env:CI_NETRC_USERNAME" >> $netrc;
"password $Env:CI_NETRC_PASSWORD" >> $netrc;
};
[Environment]::SetEnvironmentVariable("CI_NETRC_PASSWORD",$null);
[Environment]::SetEnvironmentVariable("CI_SCRIPT",$null);
Write-Output ('+ "go test"');
& go test; if ($LASTEXITCODE -ne 0) {exit $LASTEXITCODE}
Write-Output ('+ "go vet ./..."');
& go vet ./...; if ($LASTEXITCODE -ne 0) {exit $LASTEXITCODE}
`, string(ciScript))
}
}
79 changes: 79 additions & 0 deletions pipeline/backend/docker/convert_win.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Copyright 2024 Woodpecker Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package docker

import (
"path/filepath"
"regexp"
"strings"

"go.woodpecker-ci.org/woodpecker/v2/pipeline/backend/types"
)

const (
osTypeWindows = "windows"
defaultWindowsDriverLetter = "C:"
)

var MustNotAddWindowsLetterPattern = regexp.MustCompile(`^(?:` +
// Drive letter followed by colon and optional backslash (C: or C:\)
`[a-zA-Z]:(?:\\|$)|` +

// Device path starting with \\ or // followed by .\ or ./ (\\.\ or //./ or \\./ or //.\ )
`(?:\\\\|//)\.(?:\\|/).*|` +

// UNC path starting with \\ or // followed by non-dot (\server or //server)
`(?:\\\\|//)[^.]|` +

// Relative path starting with .\ or ./ (.\path or ./path)
`\.(?:\\|/)` +
`)`)

func (e *docker) windowsPathPatch(step *types.Step) {
// only patch if target is windows
if strings.ToLower(e.info.OSType) != osTypeWindows {
return
}

// patch volumes to have an letter if not already set
for i, vol := range step.Volumes {
volParts, err := splitVolumeParts(vol)
if err != nil || len(volParts) < 2 {
// ignore non valid volumes for now
continue
}

// fix source destination
if strings.HasPrefix(volParts[0], "/") {
volParts[0] = filepath.Join(defaultWindowsDriverLetter, volParts[0])
}

// fix mount destination
if !MustNotAddWindowsLetterPattern.MatchString(volParts[1]) {
volParts[1] = filepath.Join(defaultWindowsDriverLetter, volParts[1])
}
step.Volumes[i] = strings.Join(volParts, ":")
}

// patch workspace
if !MustNotAddWindowsLetterPattern.MatchString(step.WorkingDir) {
step.WorkingDir = filepath.Join(defaultWindowsDriverLetter, step.WorkingDir)
}
if ciWorkspace, ok := step.Environment["CI_WORKSPACE"]; ok {
if !MustNotAddWindowsLetterPattern.MatchString(ciWorkspace) {
step.Environment["CI_WORKSPACE"] = filepath.Join(defaultWindowsDriverLetter, ciWorkspace)
}
}
}
44 changes: 44 additions & 0 deletions pipeline/backend/docker/convert_win_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright 2024 Woodpecker Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package docker

import "testing"

func TestMustNotAddWindowsLetterPattern(t *testing.T) {
tests := map[string]bool{
`C:\Users`: true,
`D:\Data`: true,
`\\.\PhysicalDrive0`: true,
`//./COM1`: true,
`E:`: true,
`\\server\share`: true, // UNC path
`.\relative\path`: true, // Relative path
`./path`: true, // Relative with forward slash
`//server/share`: true, // UNC with forward slashes
`not/a/windows/path`: false,
``: false,
`/usr/local`: false,
`COM1`: false,
`\\.`: false, // Incomplete device path
`//`: false,
}

for testCase, expected := range tests {
result := MustNotAddWindowsLetterPattern.MatchString(testCase)
if result != expected {
t.Errorf("Test case %q: expected %v but got %v", testCase, expected, result)
}
}
}

0 comments on commit f85192b

Please sign in to comment.