1
0
mirror of https://github.com/ko-build/ko.git synced 2025-11-06 09:19:12 +02:00

Build working Windows container images (#374)

* Build working Windows container images

Add e2e tests that run on Windows and cover kodata behavior

* now successfully skipping symlinks on windows :-/

* fix e2e test on windows, that relied on a symlink in kodata after all

* document windows symlink issue

* review feedback

* re-add kodata symlink tests for linux
This commit is contained in:
Jason Hall
2021-07-27 16:19:21 -04:00
committed by GitHub
parent e947aa378d
commit 690533235a
10 changed files with 188 additions and 70 deletions

51
.github/workflows/e2e.yaml vendored Normal file
View File

@@ -0,0 +1,51 @@
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
testimg=$(go run ./ publish ./test --platform=${GOOS}/${GOARCH} --preserve-import-paths)
docker run ${testimg} --wait=false 2>&1 | grep "Hello there"
# Check that symlinks in kodata are chased.
# Skip this test on Windows.
if [[ "$RUNNER_OS" == "Linux" ]]; then
docker run ${testimg} --wait=false -f HEAD
fi

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
# Ignore GoLand (IntelliJ) files. # Ignore GoLand (IntelliJ) files.
.idea/ .idea/
ko

View File

@@ -408,7 +408,24 @@ timestamp with:
export KO_DATA_DATE_EPOCH=$(git log -1 --format='%ct') export KO_DATA_DATE_EPOCH=$(git log -1 --format='%ct')
``` ```
## Can I optimize images for [eStargz support](https://github.com/containerd/stargz-snapshotter/blob/v0.2.0/docs/stargz-estargz.md)? ## Can I build Windows containers?
Yes, but support for Windows containers is new, experimental, and tenuous. Be prepared to file bugs. 🐛
The default base image does not provide a Windows image.
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
```
### Known issues 🐛
- Symlinks in `kodata` are ignored when building Windows images; only regular files and directories will be included in the Windows image.
## Can I optimize images for [eStargz support](https://github.com/containerd/stargz-snapshotter/blob/v0.7.0/docs/stargz-estargz.md)?
Yes! Set the environment variable `GGCR_EXPERIMENT_ESTARGZ=1` to produce Yes! Set the environment variable `GGCR_EXPERIMENT_ESTARGZ=1` to produce
eStargz-optimized images. eStargz-optimized images.

BIN
ko Executable file

Binary file not shown.

View File

@@ -46,7 +46,6 @@ import (
) )
const ( const (
appDir = "/ko-app"
defaultAppFilename = "ko-app" defaultAppFilename = "ko-app"
gorootWarningTemplate = `NOTICE! gorootWarningTemplate = `NOTICE!
@@ -173,7 +172,6 @@ func moduleInfo(ctx context.Context, dir string) (*modules, error) {
for { for {
var info modInfo var info modInfo
err := dec.Decode(&info) err := dec.Decode(&info)
if err == io.EOF { if err == io.EOF {
// all done // all done
@@ -428,40 +426,42 @@ func appFilename(importpath string) string {
return base return base
} }
func tarAddDirectories(tw *tar.Writer, dir string, creationTime v1.Time) error { // userOwnerAndGroupSID is a magic value needed to make the binary executable
if dir == "." || dir == string(filepath.Separator) { // in a Windows container.
return nil //
} // owner: BUILTIN/Users group: BUILTIN/Users ($sddlValue="O:BUG:BU")
const userOwnerAndGroupSID = "AQAAgBQAAAAkAAAAAAAAAAAAAAABAgAAAAAABSAAAAAhAgAAAQIAAAAAAAUgAAAAIQIAAA=="
// Write parent directories first func tarBinary(name, binary string, creationTime v1.Time, platform *v1.Platform) (*bytes.Buffer, error) {
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
}
func tarBinary(name, binary string, creationTime v1.Time) (*bytes.Buffer, error) {
buf := bytes.NewBuffer(nil) buf := bytes.NewBuffer(nil)
tw := tar.NewWriter(buf) tw := tar.NewWriter(buf)
defer tw.Close() defer tw.Close()
// write the parent directories to the tarball archive // Write the parent directories to the tarball archive.
if err := tarAddDirectories(tw, path.Dir(name), creationTime); err != nil { // For Windows, the layer must contain a Hives/ directory, and the root
return nil, err // 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) file, err := os.Open(binary)
@@ -483,6 +483,13 @@ func tarBinary(name, binary string, creationTime v1.Time) (*bytes.Buffer, error)
Mode: 0555, Mode: 0555,
ModTime: creationTime.Time, 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 // write the header to the tarball archive
if err := tw.WriteHeader(header); err != nil { if err := tw.WriteHeader(header); err != nil {
return nil, err return nil, err
@@ -509,19 +516,10 @@ const kodataRoot = "/var/run/ko"
// walkRecursive performs a filepath.Walk of the given root directory adding it // walkRecursive performs a filepath.Walk of the given root directory adding it
// to the provided tar.Writer with root -> chroot. All symlinks are dereferenced, // to the provided tar.Writer with root -> chroot. All symlinks are dereferenced,
// which is what leads to recursion when we encounter a directory symlink. // 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 { return filepath.Walk(root, func(hostPath string, info os.FileInfo, err error) error {
if hostPath == root { if hostPath == root {
// Add an entry for the root directory of our walk. return nil
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,
})
} }
if err != nil { if err != nil {
return fmt.Errorf("filepath.Walk(%q): %w", root, err) return fmt.Errorf("filepath.Walk(%q): %w", root, err)
@@ -532,6 +530,14 @@ func walkRecursive(tw *tar.Writer, root, chroot string, creationTime v1.Time) er
} }
newPath := path.Join(chroot, filepath.ToSlash(hostPath[len(root):])) 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" {
if info.Mode()&os.ModeSymlink != 0 {
log.Println("skipping symlink in kodata for windows:", info.Name())
return nil
}
}
evalPath, err := filepath.EvalSymlinks(hostPath) evalPath, err := filepath.EvalSymlinks(hostPath)
if err != nil { if err != nil {
return fmt.Errorf("filepath.EvalSymlinks(%q): %w", hostPath, err) return fmt.Errorf("filepath.EvalSymlinks(%q): %w", hostPath, err)
@@ -544,7 +550,7 @@ func walkRecursive(tw *tar.Writer, root, chroot string, creationTime v1.Time) er
} }
// Skip other directories. // Skip other directories.
if info.Mode().IsDir() { 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. // Open the file to copy it into the tarball.
@@ -555,7 +561,7 @@ func walkRecursive(tw *tar.Writer, root, chroot string, creationTime v1.Time) er
defer file.Close() defer file.Close()
// Copy the file into the image tarball. // Copy the file into the image tarball.
if err := tw.WriteHeader(&tar.Header{ header := &tar.Header{
Name: newPath, Name: newPath,
Size: info.Size(), Size: info.Size(),
Typeflag: tar.TypeReg, Typeflag: tar.TypeReg,
@@ -564,7 +570,15 @@ func walkRecursive(tw *tar.Writer, root, chroot string, creationTime v1.Time) er
// 0444, or 0666, none of which are executable. // 0444, or 0666, none of which are executable.
Mode: 0555, Mode: 0555,
ModTime: creationTime.Time, 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) return fmt.Errorf("tar.Writer.WriteHeader(%q): %w", newPath, err)
} }
if _, err := io.Copy(tw, file); err != nil { if _, err := io.Copy(tw, file); err != nil {
@@ -574,7 +588,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) buf := bytes.NewBuffer(nil)
tw := tar.NewWriter(buf) tw := tar.NewWriter(buf)
defer tw.Close() defer tw.Close()
@@ -586,7 +600,41 @@ func (g *gobuild) tarKoData(ref reference) (*bytes.Buffer, error) {
creationTime := g.kodataCreationTime 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{} { func createTemplateData() map[string]interface{} {
@@ -681,8 +729,9 @@ func (g *gobuild) buildOne(ctx context.Context, refStr string, baseRef name.Refe
defer os.RemoveAll(filepath.Dir(file)) defer os.RemoveAll(filepath.Dir(file))
var layers []mutate.Addendum var layers []mutate.Addendum
// Create a layer from the kodata directory under this import path. // 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 { if err != nil {
return nil, err return nil, err
} }
@@ -702,10 +751,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. // 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 { if err != nil {
return nil, err return nil, err
} }
@@ -748,8 +797,14 @@ func (g *gobuild) buildOne(ctx context.Context, refStr string, baseRef name.Refe
cfg = cfg.DeepCopy() cfg = cfg.DeepCopy()
cfg.Config.Entrypoint = []string{appPath} cfg.Config.Entrypoint = []string{appPath}
updatePath(cfg) if platform.OS == "windows" {
cfg.Config.Env = append(cfg.Config.Env, "KO_DATA_PATH="+kodataRoot) 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" cfg.Author = "github.com/google/ko"
if cfg.Config.Labels == nil { if cfg.Config.Labels == nil {
@@ -771,9 +826,9 @@ func (g *gobuild) buildOne(ctx context.Context, refStr string, baseRef name.Refe
return image, nil return image, nil
} }
// Append appDir to the PATH environment variable, if it exists. Otherwise, // Append appPath to the PATH environment variable, if it exists. Otherwise,
// set the PATH environment variable to appDir. // set the PATH environment variable to appPath.
func updatePath(cf *v1.ConfigFile) { func updatePath(cf *v1.ConfigFile, appPath string) {
for i, env := range cf.Config.Env { for i, env := range cf.Config.Env {
parts := strings.SplitN(env, "=", 2) parts := strings.SplitN(env, "=", 2)
if len(parts) != 2 { if len(parts) != 2 {
@@ -782,14 +837,14 @@ func updatePath(cf *v1.ConfigFile) {
} }
key, value := parts[0], parts[1] key, value := parts[0], parts[1]
if key == "PATH" { if key == "PATH" {
value = fmt.Sprintf("%s:%s", value, appDir) value = fmt.Sprintf("%s:%s", value, appPath)
cf.Config.Env[i] = "PATH=" + value cf.Config.Env[i] = "PATH=" + value
return return
} }
} }
// If we get here, we never saw PATH. // 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 // Build implements build.Interface

View File

@@ -458,7 +458,7 @@ func validateImage(t *testing.T, img v1.Image, baseLayers int64, creationTime v1
pathValue := strings.TrimPrefix(envVar, "PATH=") pathValue := strings.TrimPrefix(envVar, "PATH=")
pathEntries := strings.Split(pathValue, ":") pathEntries := strings.Split(pathValue, ":")
for _, pathEntry := range pathEntries { for _, pathEntry := range pathEntries {
if pathEntry == appDir { if pathEntry == "/ko-app/test" {
found = true found = true
} }
} }

View File

@@ -145,7 +145,8 @@ func (d *demon) Publish(ctx context.Context, br build.Result, s string) (name.Re
} }
log.Printf("Loading %v", digestTag) log.Printf("Loading %v", digestTag)
if _, err := daemon.Write(digestTag, img, d.getOpts(ctx)...); err != nil { if resp, err := daemon.Write(digestTag, img, d.getOpts(ctx)...); err != nil {
log.Println("daemon.Write response: ", resp)
return nil, err return nil, err
} }
log.Printf("Loaded %v", digestTag) log.Printf("Loaded %v", digestTag)

View File

@@ -1 +0,0 @@
Hello there

View File

@@ -1 +0,0 @@
../kenobi

1
test/kodata/kenobi Normal file
View File

@@ -0,0 +1 @@
Hello there

View File

@@ -25,6 +25,7 @@ import (
) )
var ( var (
f = flag.String("f", "kenobi", "File in kodata to print")
wait = flag.Bool("wait", true, "Whether to wait for SIGTERM") wait = flag.Bool("wait", true, "Whether to wait for SIGTERM")
) )
@@ -32,20 +33,13 @@ func main() {
flag.Parse() flag.Parse()
dp := os.Getenv("KO_DATA_PATH") dp := os.Getenv("KO_DATA_PATH")
file := filepath.Join(dp, "kenobi") file := filepath.Join(dp, *f)
bytes, err := ioutil.ReadFile(file) bytes, err := ioutil.ReadFile(file)
if err != nil { if err != nil {
log.Fatalf("Error reading %q: %v", file, err) log.Fatalf("Error reading %q: %v", file, err)
} }
log.Print(string(bytes)) log.Print(string(bytes))
file = filepath.Join(dp, "HEAD")
bytes, err = ioutil.ReadFile(file)
if err != nil {
log.Fatalf("Error reading %q: %v", file, err)
}
log.Print(string(bytes))
// Cause the pod to "hang" to allow us to check for a readiness state. // Cause the pod to "hang" to allow us to check for a readiness state.
if *wait { if *wait {
sigs := make(chan os.Signal, 1) sigs := make(chan os.Signal, 1)