Skip to content

Commit

Permalink
Build working Windows container images
Browse files Browse the repository at this point in the history
Add e2e tests that run on Windows and cover kodata behavior
  • Loading branch information
imjasonh committed Jul 23, 2021
1 parent 30716ad commit 8094159
Show file tree
Hide file tree
Showing 9 changed files with 181 additions and 61 deletions.
45 changes: 45 additions & 0 deletions .github/workflows/e2e.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: Basic e2e test

on:
pull_request:
branches: ['main']

jobs:
e2e:
strategy:
fail-fast: false
matrix:
platform:
- ubuntu-latest
- windows-latest
name: e2e ${{ matrix.platform }}
runs-on: ${{ matrix.platform }}

steps:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
with:
go-version: 1.16.x

- name: Build and run ko container
env:
KO_DOCKER_REPO: ko.local
shell: bash
run: |
set -euxo pipefail
# eval `go env`, compatible with Windows and Linux
# cribbed from https://gist.github.com/Syeberman/39d81b1e17d091be5657ecd6fbff0753
eval $(go env | sed -r 's/^(set )?(\w+)=("?)(.*)\3$/\2="\4"/gm')
if [[ "${{ matrix.platform }}" == "windows-latest" ]]; then
export KO_DEFAULTBASEIMAGE=mcr.microsoft.com/windows/nanoserver:1809
fi
echo platform is ${GOOS}/${GOARCH}
# Build and run the ko binary, which should be runnable.
docker run $(go run ./ publish ./ --platform=${GOOS}/${GOARCH} --preserve-import-paths) version
# Build and run the test/ binary, which should log "Hello there" served from KO_DATA_PATH
docker run $(go run ./ publish ./test --platform=${GOOS}/${GOARCH} --preserve-import-paths) --wait=false 2>&1 | grep "Hello there"
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# Ignore GoLand (IntelliJ) files.
.idea/

ko
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,21 @@ timestamp with:
export KO_DATA_DATE_EPOCH=$(git log -1 --format='%ct')
```

## Can I build Windows containers?

Yes, but support for Windows containers is new, experimental, and tenuous. Be
prepared to file bugs. 🐛

You can try out building a Windows container image by [setting the base image](#overriding-base-images) to
a Windows base image and building with `--platform=windows/amd64` or
`--platform=all`:

For example, to build a Windows container image for `ko`, from within this repo:

```
KO_DEFAULTBASEIMAGE=mcr.microsoft.com/windows/nanoserver:1809 ko publish ./ --platform=windows/amd64
```

## Can I optimize images for [eStargz support](https://github.com/containerd/stargz-snapshotter/blob/v0.2.0/docs/stargz-estargz.md)?

Yes! Set the environment variable `GGCR_EXPERIMENT_ESTARGZ=1` to produce
Expand Down
Binary file added ko
Binary file not shown.
171 changes: 114 additions & 57 deletions pkg/build/gobuild.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ import (
)

const (
appDir = "/ko-app"
defaultAppFilename = "ko-app"

gorootWarningTemplate = `NOTICE!
Expand Down Expand Up @@ -173,7 +172,6 @@ func moduleInfo(ctx context.Context, dir string) (*modules, error) {

for {
var info modInfo

err := dec.Decode(&info)
if err == io.EOF {
// all done
Expand Down Expand Up @@ -428,40 +426,42 @@ func appFilename(importpath string) string {
return base
}

func tarAddDirectories(tw *tar.Writer, dir string, creationTime v1.Time) error {
if dir == "." || dir == string(filepath.Separator) {
return nil
}

// Write parent directories first
if err := tarAddDirectories(tw, filepath.Dir(dir), creationTime); err != nil {
return err
}

// write the directory header to the tarball archive
if err := tw.WriteHeader(&tar.Header{
Name: dir,
Typeflag: tar.TypeDir,
// Use a fixed Mode, so that this isn't sensitive to the directory and umask
// under which it was created. Additionally, windows can only set 0222,
// 0444, or 0666, none of which are executable.
Mode: 0555,
ModTime: creationTime.Time,
}); err != nil {
return err
}

return nil
}
// userOwnerAndGroupSID is a magic value needed to make the binary executable
// in a Windows container.
//
// owner: BUILTIN/Users group: BUILTIN/Users ($sddlValue="O:BUG:BU")
const userOwnerAndGroupSID = "AQAAgBQAAAAkAAAAAAAAAAAAAAABAgAAAAAABSAAAAAhAgAAAQIAAAAAAAUgAAAAIQIAAA=="

func tarBinary(name, binary string, creationTime v1.Time) (*bytes.Buffer, error) {
func tarBinary(name, binary string, creationTime v1.Time, platform *v1.Platform) (*bytes.Buffer, error) {
buf := bytes.NewBuffer(nil)
tw := tar.NewWriter(buf)
defer tw.Close()

// write the parent directories to the tarball archive
if err := tarAddDirectories(tw, path.Dir(name), creationTime); err != nil {
return nil, err
// Write the parent directories to the tarball archive.
// For Windows, the layer must contain a Hives/ directory, and the root
// of the actual filesystem goes in a Files/ directory.
// For Linux, the binary goes into /ko-app/
dirs := []string{"ko-app"}
if platform.OS == "windows" {
dirs = []string{
"Hives",
"Files",
"Files/ko-app",
}
name = "Files" + name + ".exe"
}
for _, dir := range dirs {
if err := tw.WriteHeader(&tar.Header{
Name: dir,
Typeflag: tar.TypeDir,
// Use a fixed Mode, so that this isn't sensitive to the directory and umask
// under which it was created. Additionally, windows can only set 0222,
// 0444, or 0666, none of which are executable.
Mode: 0555,
ModTime: creationTime.Time,
}); err != nil {
return nil, fmt.Errorf("writing dir %q: %v", dir, err)
}
}

file, err := os.Open(binary)
Expand All @@ -483,6 +483,13 @@ func tarBinary(name, binary string, creationTime v1.Time) (*bytes.Buffer, error)
Mode: 0555,
ModTime: creationTime.Time,
}
if platform.OS == "windows" {
// This magic value is for some reason needed for Windows to be
// able to execute the binary.
header.PAXRecords = map[string]string{
"MSWINDOWS.rawsd": userOwnerAndGroupSID,
}
}
// write the header to the tarball archive
if err := tw.WriteHeader(header); err != nil {
return nil, err
Expand All @@ -509,19 +516,10 @@ const kodataRoot = "/var/run/ko"
// walkRecursive performs a filepath.Walk of the given root directory adding it
// to the provided tar.Writer with root -> chroot. All symlinks are dereferenced,
// which is what leads to recursion when we encounter a directory symlink.
func walkRecursive(tw *tar.Writer, root, chroot string, creationTime v1.Time) error {
func walkRecursive(tw *tar.Writer, root, chroot string, creationTime v1.Time, platform *v1.Platform) error {
return filepath.Walk(root, func(hostPath string, info os.FileInfo, err error) error {
if hostPath == root {
// Add an entry for the root directory of our walk.
return tw.WriteHeader(&tar.Header{
Name: chroot,
Typeflag: tar.TypeDir,
// Use a fixed Mode, so that this isn't sensitive to the directory and umask
// under which it was created. Additionally, windows can only set 0222,
// 0444, or 0666, none of which are executable.
Mode: 0555,
ModTime: creationTime.Time,
})
return nil
}
if err != nil {
return fmt.Errorf("filepath.Walk(%q): %w", root, err)
Expand All @@ -532,6 +530,16 @@ func walkRecursive(tw *tar.Writer, root, chroot string, creationTime v1.Time) er
}
newPath := path.Join(chroot, filepath.ToSlash(hostPath[len(root):]))

// Don't chase symlinks on Windows, where cross-compiled symlink support is not possible.
if platform.OS == "windows" {
log.Println("PLATFORM IS WINDOWS")
if info.Mode()&os.ModeSymlink != 0 {
log.Println("skipping symlink for windows:", info.Name())
return nil
}
log.Println(info.Name(), info.Mode()) // TODO remove this noisy logging
}

evalPath, err := filepath.EvalSymlinks(hostPath)
if err != nil {
return fmt.Errorf("filepath.EvalSymlinks(%q): %w", hostPath, err)
Expand All @@ -544,7 +552,7 @@ func walkRecursive(tw *tar.Writer, root, chroot string, creationTime v1.Time) er
}
// Skip other directories.
if info.Mode().IsDir() {
return walkRecursive(tw, evalPath, newPath, creationTime)
return walkRecursive(tw, evalPath, newPath, creationTime, platform)
}

// Open the file to copy it into the tarball.
Expand All @@ -555,7 +563,7 @@ func walkRecursive(tw *tar.Writer, root, chroot string, creationTime v1.Time) er
defer file.Close()

// Copy the file into the image tarball.
if err := tw.WriteHeader(&tar.Header{
header := &tar.Header{
Name: newPath,
Size: info.Size(),
Typeflag: tar.TypeReg,
Expand All @@ -564,7 +572,15 @@ func walkRecursive(tw *tar.Writer, root, chroot string, creationTime v1.Time) er
// 0444, or 0666, none of which are executable.
Mode: 0555,
ModTime: creationTime.Time,
}); err != nil {
}
if platform.OS == "windows" {
// This magic value is for some reason needed for Windows to be
// able to execute the binary.
header.PAXRecords = map[string]string{
"MSWINDOWS.rawsd": userOwnerAndGroupSID,
}
}
if err := tw.WriteHeader(header); err != nil {
return fmt.Errorf("tar.Writer.WriteHeader(%q): %w", newPath, err)
}
if _, err := io.Copy(tw, file); err != nil {
Expand All @@ -574,7 +590,7 @@ func walkRecursive(tw *tar.Writer, root, chroot string, creationTime v1.Time) er
})
}

func (g *gobuild) tarKoData(ref reference) (*bytes.Buffer, error) {
func (g *gobuild) tarKoData(ref reference, platform *v1.Platform) (*bytes.Buffer, error) {
buf := bytes.NewBuffer(nil)
tw := tar.NewWriter(buf)
defer tw.Close()
Expand All @@ -586,7 +602,41 @@ func (g *gobuild) tarKoData(ref reference) (*bytes.Buffer, error) {

creationTime := g.kodataCreationTime

return buf, walkRecursive(tw, root, kodataRoot, creationTime)
// Write the parent directories to the tarball archive.
// For Windows, the layer must contain a Hives/ directory, and the root
// of the actual filesystem goes in a Files/ directory.
// For Linux, kodata starts at /var/run/ko.
chroot := kodataRoot
dirs := []string{
"/var",
"/var/run",
"/var/run/ko",
}
if platform.OS == "windows" {
chroot = "Files" + kodataRoot
dirs = []string{
"Hives",
"Files",
"Files/var",
"Files/var/run",
"Files/var/run/ko",
}
}
for _, dir := range dirs {
if err := tw.WriteHeader(&tar.Header{
Name: dir,
Typeflag: tar.TypeDir,
// Use a fixed Mode, so that this isn't sensitive to the directory and umask
// under which it was created. Additionally, windows can only set 0222,
// 0444, or 0666, none of which are executable.
Mode: 0555,
ModTime: creationTime.Time,
}); err != nil {
return nil, fmt.Errorf("writing dir %q: %v", dir, err)
}
}

return buf, walkRecursive(tw, root, chroot, creationTime, platform)
}

func createTemplateData() map[string]interface{} {
Expand Down Expand Up @@ -681,8 +731,9 @@ func (g *gobuild) buildOne(ctx context.Context, refStr string, baseRef name.Refe
defer os.RemoveAll(filepath.Dir(file))

var layers []mutate.Addendum

// Create a layer from the kodata directory under this import path.
dataLayerBuf, err := g.tarKoData(ref)
dataLayerBuf, err := g.tarKoData(ref, platform)
if err != nil {
return nil, err
}
Expand All @@ -702,10 +753,10 @@ func (g *gobuild) buildOne(ctx context.Context, refStr string, baseRef name.Refe
},
})

appPath := path.Join(appDir, appFilename(ref.Path()))
appPath := path.Join("/ko-app", appFilename(ref.Path()))

// Construct a tarball with the binary and produce a layer.
binaryLayerBuf, err := tarBinary(appPath, file, v1.Time{})
binaryLayerBuf, err := tarBinary(appPath, file, v1.Time{}, platform)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -748,8 +799,14 @@ func (g *gobuild) buildOne(ctx context.Context, refStr string, baseRef name.Refe

cfg = cfg.DeepCopy()
cfg.Config.Entrypoint = []string{appPath}
updatePath(cfg)
cfg.Config.Env = append(cfg.Config.Env, "KO_DATA_PATH="+kodataRoot)
if platform.OS == "windows" {
cfg.Config.Entrypoint = []string{`C:\ko-app\` + appFilename(ref.Path()) + ".exe"}
updatePath(cfg, `C:\ko-app`)
cfg.Config.Env = append(cfg.Config.Env, `KO_DATA_PATH=C:\var\run\ko`)
} else {
updatePath(cfg, appPath)
cfg.Config.Env = append(cfg.Config.Env, "KO_DATA_PATH="+kodataRoot)
}
cfg.Author = "github.com/google/ko"

if cfg.Config.Labels == nil {
Expand All @@ -771,9 +828,9 @@ func (g *gobuild) buildOne(ctx context.Context, refStr string, baseRef name.Refe
return image, nil
}

// Append appDir to the PATH environment variable, if it exists. Otherwise,
// set the PATH environment variable to appDir.
func updatePath(cf *v1.ConfigFile) {
// Append appPath to the PATH environment variable, if it exists. Otherwise,
// set the PATH environment variable to appPath.
func updatePath(cf *v1.ConfigFile, appPath string) {
for i, env := range cf.Config.Env {
parts := strings.SplitN(env, "=", 2)
if len(parts) != 2 {
Expand All @@ -782,14 +839,14 @@ func updatePath(cf *v1.ConfigFile) {
}
key, value := parts[0], parts[1]
if key == "PATH" {
value = fmt.Sprintf("%s:%s", value, appDir)
value = fmt.Sprintf("%s:%s", value, appPath)
cf.Config.Env[i] = "PATH=" + value
return
}
}

// If we get here, we never saw PATH.
cf.Config.Env = append(cf.Config.Env, "PATH="+appDir)
cf.Config.Env = append(cf.Config.Env, "PATH="+appPath)
}

// Build implements build.Interface
Expand Down
2 changes: 1 addition & 1 deletion pkg/build/gobuild_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -458,7 +458,7 @@ func validateImage(t *testing.T, img v1.Image, baseLayers int64, creationTime v1
pathValue := strings.TrimPrefix(envVar, "PATH=")
pathEntries := strings.Split(pathValue, ":")
for _, pathEntry := range pathEntries {
if pathEntry == appDir {
if pathEntry == "/ko-app/test" {
found = true
}
}
Expand Down
5 changes: 4 additions & 1 deletion pkg/publish/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,9 +145,12 @@ func (d *demon) Publish(ctx context.Context, br build.Result, s string) (name.Re
}

log.Printf("Loading %v", digestTag)
if _, err := daemon.Write(digestTag, img, d.getOpts(ctx)...); err != nil {
// TODO: if resp, err := ...
resp, err := daemon.Write(digestTag, img, d.getOpts(ctx)...)
if err != nil {
return nil, err
}
log.Println("daemon.Write response: ", resp) // TODO: remove this
log.Printf("Loaded %v", digestTag)

for _, tagName := range d.tags {
Expand Down
1 change: 0 additions & 1 deletion test/kenobi

This file was deleted.

1 change: 0 additions & 1 deletion test/kodata/kenobi

This file was deleted.

Loading

0 comments on commit 8094159

Please sign in to comment.