Skip to content

Building Windows container images #4010

@sebsoto

Description

@sebsoto

Desired behavior:
I would expect that when no RUN commands are present in a container file, buildah build --os=windows would be able to build a working Windows container image.

Current state:
In my testing I have found that buildah does not create functional Windows container images.
This seems to be due to at least two reasons:

  1. The required tar headers specified by the OCI image-spec are not being applied.
    (image-spec issue with slightly more info)
  2. The rootfs of Windows container images is actually the Files/ directory. This can be seen by pulling a Windows container image and inspecting the layers.

There may be other reasons, but if there are, they are not documented.

I have only tested this by building an image with a Linux build of buildah, with the image being used by containerd on a Windows VM.

I built two images, one using docker buildx, the other with buildah:

Using the following containerfile:

FROM mcr.microsoft.com/windows/nanoserver:ltsc2022
ADD emptyfile .

An image built using docker buildx works with no issue, containerd logs:

time="2022-05-20T16:46:52.533976500+02:00" level=info msg="PullImage \"quay.io/ssoto/test:nanoBuildx\""
time="2022-05-20T16:47:07.411314400+02:00" level=info msg="ImageCreate event &ImageCreate{Name:quay.io/ssoto/test:nanoBuildx,Labels:map[string]string{io.cri-containerd.image: managed,},XXX_unrecognized:[],}"
time="2022-05-20T16:47:07.422273200+02:00" level=info msg="ImageCreate event &ImageCreate{Name:sha256:43aba2088aa29238b8dc6ce66f9fdea52bc9401da781c03dc7c0bbfb5e9b0b67,Labels:map[string]string{io.cri-containerd.image: managed,},XXX_unrecognized:[],}"
time="2022-05-20T16:47:07.430670500+02:00" level=info msg="ImageUpdate event &ImageUpdate{Name:quay.io/ssoto/test:nanoBuildx,Labels:map[string]string{io.cri-containerd.image: managed,},XXX_unrecognized:[],}"
time="2022-05-20T16:47:07.438064300+02:00" level=info msg="ImageCreate event &ImageCreate{Name:quay.io/ssoto/test@sha256:fab9aeb28d8b11c65595143fcfb80a44a4f1a8d917b5559be981d8353011fde1,Labels:map[string]string{io.cri-containerd.image: managed,},XXX_unrecognized:[],}"
time="2022-05-20T16:47:07.438593200+02:00" level=info msg="PullImage \"quay.io/ssoto/test:nanoBuildx\" returns image reference \"sha256:43aba2088aa29238b8dc6ce66f9fdea52bc9401da781c03dc7c0bbfb5e9b0b67\""

buildah built images cause the following in containerd logs when pulling the image:

time="2022-05-20T16:41:34.956100200+02:00" level=info msg="PullImage \"quay.io/ssoto/test:nanoShouldWork\""
time="2022-05-20T16:41:50.274201300+02:00" level=error msg="PullImage \"quay.io/ssoto/test:nanoShouldWork\" failed" error="failed to pull and unpack image \"quay.io/ssoto/test:nanoShouldWork\": failed to extract layer sha256:dff44eadebe5e0f7717289f95e61260f45df7951a5fc2adea46801dcded8ae6d: hcsshim::ImportLayer failed in Win32: The system cannot find the path specified. (0x3): unknown"

The difference between the two images can be seen in the image layer created from the ADD directive
buildkit added layer:

header: &{Typeflag:53 Name:Hives Linkname: Size:0 Mode:16384 Uid:0 Gid:0 Uname: Gname: ModTime:2022-05-20 10:45:06.607995006 -0400 EDT AccessTime:0001-01-01 00:00:00 +0000 UTC ChangeTime:0001-01-01 00:00:00 +0000 UTC Devmajor:0 Devminor:0 Xattrs:map[] PAXRecords:map[LIBARCHIVE.creationtime:1653057906.607995006 MSWIND
OWS.fileattr:16 mtime:1653057906.607995006] Format:PAX}
header: &{Typeflag:53 Name:Files Linkname: Size:0 Mode:16384 Uid:0 Gid:0 Uname: Gname: ModTime:2022-05-20 10:45:06.60847413 -0400 EDT AccessTime:0001-01-01 00:00:00 +0000 UTC ChangeTime:0001-01-01 00:00:00 +0000 UTC Devmajor:0 Devminor:0 Xattrs:map[] PAXRecords:map[LIBARCHIVE.creationtime:1653057906.608474130 MSWINDO
WS.fileattr:16 mtime:1653057906.60847413] Format:PAX}
header: &{Typeflag:48 Name:Files/emptyfile Linkname: Size:0 Mode:33204 Uid:0 Gid:0 Uname:root Gname:root ModTime:2022-05-20 09:46:28 -0400 EDT AccessTime:0001-01-01 00:00:00 +0000 UTC ChangeTime:0001-01-01 00:00:00 +0000 UTC Devmajor:0 Devminor:0 Xattrs:map[] PAXRecords:map[LIBARCHIVE.creationtime:1653054388.0 MSWIND
OWS.fileattr:32 MSWINDOWS.rawsd:AQAEgBQAAAAkAAAAAAAAADAAAAABAgAAAAAABSAAAAAgAgAAAQEAAAAAAAUSAAAAAgBMAAMAAAAAABgA/wEfAAECAAAAAAAFIAAAACACAAAAABQA/wEfAAEBAAAAAAAFEgAAAAAAGACpABIAAQIAAAAAAAUgAAAAIQIAAA==] Format:PAX}

buildah added layer:

header: &{Typeflag:48 Name:emptyfile Linkname: Size:0 Mode:33204 Uid:0 Gid:0 Uname:root Gname:root ModTime:2022-05-20 09:46:28 -0400 EDT AccessTime:0001-01-01 00:00:00 +0000 UTC ChangeTime:0001-01-01 00:00:00 +0000 UTC Devmajor:0 Devminor:0 Xattrs:map[] PAXRecords:map[] Format:USTAR}

What I've tried:
I've done some hacking on add.go, to try and see the minimal changes possible to make things work, mainly around to add Windows expected headers to a new directory I'm creating.

diff --git a/add.go b/add.go
index 8aa53a29..66032170 100644
--- a/add.go
+++ b/add.go
@@ -376,9 +376,66 @@ func (b *Builder) Add(destination string, extract bool, options AddAndCopyOption
        if err := copier.Mkdir(mountPoint, extractDirectory, mkdirOptions); err != nil {
                return errors.Wrapf(err, "error ensuring target directory exists")
        }
+       ////////
+       {
+               logrus.Infof("starting creating some directories")
+               var putErr error
+               var wg sync.WaitGroup
 
+               pipeReader, pipeWriter := io.Pipe()
+               wg.Add(1)
+               go func() {
+                       tw := tar.NewWriter(pipeWriter)
+                       defer tw.Close()
+                       hdr := tar.Header{
+                               Typeflag: tar.TypeDir,
+                               Name:     "testdir",
+                               Size:     0,
+                               Format:   tar.FormatPAX,
+                               Mode:     1 << 14,
+                               PAXRecords: map[string]string{
+                                       "MSWINDOWS.fileattr": "16",
+                                       "MSWINDOWS.rawsd":    "",
+                               },
+                       }
+                       err = tw.WriteHeader(&hdr)
+                       if err != nil {
+                               logrus.Infof("error %s", err)
+                       }
+                       pipeWriter.Close()
+                       wg.Done()
+               }()
+               wg.Add(1)
+               go func() {
+                       b.ContentDigester.Start("")
+                       hashCloser := b.ContentDigester.Hash()
+                       hasher := io.Writer(hashCloser)
+                       if options.Hasher != nil {
+                               hasher = io.MultiWriter(hasher, options.Hasher)
+                       }
+                       if options.DryRun {
+                               _, putErr = io.Copy(hasher, pipeReader)
+                       } else {
+                               putOptions := copier.PutOptions{
+                                       UIDMap:        destUIDMap,
+                                       GIDMap:        destGIDMap,
+                                       ChownDirs:     nil,
+                                       ChmodDirs:     nil,
+                                       ChownFiles:    nil,
+                                       ChmodFiles:    nil,
+                                       IgnoreDevices: userns.RunningInUserNS(),
+                               }
+                               putErr = copier.Put(extractDirectory, extractDirectory, putOptions, io.TeeReader(pipeReader, hasher))
+                       }
+                       hashCloser.Close()
+                       pipeReader.Close()
+                       wg.Done()
+               }()
+       }
+       ///////

Built and saved the resulting image

make buildah
./bin/buildah build --log-level=info -f Dockerfile.nano -t handmade:latest . 
./bin/buildah push handmade:latest dir:handmade

For a reason unknown to me, I can't get the header changes to stick. I have some ideas as to why this is happening but I'm not familiar enough with this codebase to fully figure this out.

Extra info:
What buildkit is doing differently from buildah
Related containerd issue: https://github.com/containerd/containerd/issues/2469

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions