Skip to content

Commit 40ef079

Browse files
committed
Add basic functionality to build Windows images
Enables committing a Windows base image as the final layer. This gives the possibility of building a Windows container image on a Linux build system. Only COPY and ADD are in scope. This allows the workflow of cross compiling Windows binaries on Linux, and copying them into a Windows container image for use on Windows systems. Signed-off-by: Sebastian Soto <ssoto@redhat.com>
1 parent 12386f3 commit 40ef079

File tree

3 files changed

+166
-10
lines changed

3 files changed

+166
-10
lines changed

commit_test.go

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -296,12 +296,18 @@ func TestCommitCompression(t *testing.T) {
296296
name string
297297
expectError bool
298298
layerMediaType string
299+
os string
299300
}{
300-
{archive.Uncompressed, "uncompressed", false, v1.MediaTypeImageLayer},
301-
{archive.Gzip, "gzip", false, v1.MediaTypeImageLayerGzip},
302-
{archive.Bzip2, "bz2", true, ""},
303-
{archive.Xz, "xz", true, ""},
304-
{archive.Zstd, "zstd", false, v1.MediaTypeImageLayerZstd},
301+
{archive.Uncompressed, "uncompressed", false, v1.MediaTypeImageLayer, ""},
302+
{archive.Uncompressed, "uncompressed-win", false, v1.MediaTypeImageLayer, "windows"},
303+
{archive.Gzip, "gzip", false, v1.MediaTypeImageLayerGzip, ""},
304+
{archive.Gzip, "gzip-win", false, v1.MediaTypeImageLayerGzip, "windows"},
305+
{archive.Bzip2, "bz2", true, "", ""},
306+
{archive.Bzip2, "bz2-win", true, "", "windows"},
307+
{archive.Xz, "xz", true, "", ""},
308+
{archive.Xz, "xz-win", true, "", "windows"},
309+
{archive.Zstd, "zstd", false, v1.MediaTypeImageLayerZstd, ""},
310+
{archive.Zstd, "zstd-win", false, v1.MediaTypeImageLayerZstd, "windows"},
305311
} {
306312
t.Run(compressor.name, func(t *testing.T) {
307313
var ref types.ImageReference
@@ -310,6 +316,7 @@ func TestCommitCompression(t *testing.T) {
310316
SystemContext: &testSystemContext,
311317
Compression: compressor.compression,
312318
}
319+
b.OCIv1.OS = compressor.os
313320
imageName := compressor.name
314321
ref, err := imageStorage.Transport.ParseStoreReference(store, imageName)
315322
require.NoErrorf(t, err, "parsing reference for to-be-committed local image %q", imageName)

image.go

Lines changed: 152 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,34 @@ const (
5959
// create uniquely-named files, but we don't want to try to use their
6060
// contents until after they've been written to
6161
containerExcludesSubstring = ".tmp"
62+
63+
// Windows-specific PAX record keys
64+
keyFileAttr = "MSWINDOWS.fileattr"
65+
keySDRaw = "MSWINDOWS.rawsd"
66+
keyCreationTime = "LIBARCHIVE.creationtime"
67+
68+
// Windows Security Descriptors (base64-encoded)
69+
// SDDL: O:BAG:SYD:(A;OICI;FA;;;BA)(A;OICI;FA;;;SY)(A;;FA;;;BA)(A;OICIIO;GA;;;CO)(A;OICI;0x1200a9;;;BU)(A;CI;LC;;;BU)(A;CI;DC;;;BU)
70+
// Owner: Built-in Administrators (BA)
71+
// Group: Local System (SY)
72+
// DACL:
73+
// - Allow OBJECT_INHERIT+CONTAINER_INHERIT Full Access to Built-in Administrators (BA)
74+
// - Allow OBJECT_INHERIT+CONTAINER_INHERIT Full Access to Local System (SY)
75+
// - Allow Full Access to Built-in Administrators (BA)
76+
// - Allow OBJECT_INHERIT+CONTAINER_INHERIT+INHERIT_ONLY Generic All to Creator Owner (CO)
77+
// - Allow OBJECT_INHERIT+CONTAINER_INHERIT Read/Execute permissions to Built-in Users (BU)
78+
// - Allow CONTAINER_INHERIT List Contents to Built-in Users (BU)
79+
// - Allow CONTAINER_INHERIT Delete Child to Built-in Users (BU)
80+
winSecurityDescriptorDirectory = "AQAEgBQAAAAkAAAAAAAAADAAAAABAgAAAAAABSAAAAAgAgAAAQEAAAAAAAUSAAAAAgCoAAcAAAAAAxgA/wEfAAECAAAAAAAFIAAAACACAAAAAxQA/wEfAAEBAAAAAAAFEgAAAAAAGAD/AR8AAQIAAAAAAAUgAAAAIAIAAAALFAAAAAAQAQEAAAAAAAMAAAAAAAMYAKkAEgABAgAAAAAABSAAAAAhAgAAAAIYAAQAAAABAgAAAAAABSAAAAAhAgAAAAIYAAIAAAABAgAAAAAABSAAAAAhAgAA"
81+
82+
// SDDL: O:BAG:SYD:(A;;FA;;;BA)(A;;FA;;;SY)(A;;0x1200a9;;;BU)
83+
// Owner: Built-in Administrators (BA)
84+
// Group: Local System (SY)
85+
// DACL:
86+
// - Allow Full Access to Built-in Administrators (BA)
87+
// - Allow Full Access to Local System (SY)
88+
// - Allow Read/Execute permissions to Built-in Users (BU)
89+
winSecurityDescriptorFile = "AQAEgBQAAAAkAAAAAAAAADAAAAABAgAAAAAABSAAAAAgAgAAAQEAAAAAAAUSAAAAAgBMAAMAAAAAABgA/wEfAAECAAAAAAAFIAAAACACAAAAABQA/wEfAAEBAAAAAAAFEgAAAAAAGACpABIAAQIAAAAAAAUgAAAAIQIAAA=="
6290
)
6391

6492
// ExtractRootfsOptions is consumed by ExtractRootfs() which allows users to
@@ -112,6 +140,7 @@ type containerImageRef struct {
112140
unsetAnnotations []string
113141
setAnnotations []string
114142
createdAnnotation types.OptionalBool
143+
os string
115144
}
116145

117146
type blobLayerInfo struct {
@@ -1075,7 +1104,7 @@ func (i *containerImageRef) NewImageSource(ctx context.Context, _ *types.SystemC
10751104
return nil, fmt.Errorf("opening file for %s: %w", what, err)
10761105
}
10771106

1078-
layerFileWriter, err := newLayerWriter(layerFile, i.compression, i.layerModTime, i.layerLatestModTime, layerExclusions)
1107+
layerFileWriter, err := newLayerWriter(layerFile, i.compression, i.layerModTime, i.layerLatestModTime, layerExclusions, i.os == "windows")
10791108
if err != nil {
10801109
layerFile.Close()
10811110
rc.Close()
@@ -1097,7 +1126,9 @@ func (i *containerImageRef) NewImageSource(ctx context.Context, _ *types.SystemC
10971126
return nil, fmt.Errorf("extracting container rootfs: %w", err)
10981127
}
10991128
}
1100-
if layerFileWriter.SourceDigest() != layerFileWriter.DestDigest() {
1129+
// If the stream was transformed (compression or Windows mutation), use TotalWritten()
1130+
// Otherwise verify that io.Copy size matches TotalWritten()
1131+
if layerFileWriter.SourceDigest() != layerFileWriter.DestDigest() || i.os == "windows" {
11011132
size = layerFileWriter.TotalWritten()
11021133
} else {
11031134
if size != layerFileWriter.TotalWritten() {
@@ -1143,6 +1174,113 @@ func (i *containerImageRef) NewImageSource(ctx context.Context, _ *types.SystemC
11431174
return src, nil
11441175
}
11451176

1177+
func prepareWinHeader(h *tar.Header) {
1178+
if h.PAXRecords == nil {
1179+
h.PAXRecords = map[string]string{}
1180+
}
1181+
if h.Typeflag == tar.TypeDir {
1182+
h.Mode |= 1 << 14
1183+
h.PAXRecords[keyFileAttr] = "16"
1184+
}
1185+
1186+
if h.Typeflag == tar.TypeReg {
1187+
h.Mode |= 1 << 15
1188+
h.PAXRecords[keyFileAttr] = "32"
1189+
}
1190+
1191+
if !h.ModTime.IsZero() {
1192+
h.PAXRecords[keyCreationTime] = fmt.Sprintf("%d.%d", h.ModTime.Unix(), h.ModTime.Nanosecond())
1193+
}
1194+
1195+
h.Format = tar.FormatPAX
1196+
}
1197+
1198+
func addSecurityDescriptor(h *tar.Header) {
1199+
if h.Typeflag == tar.TypeDir {
1200+
h.PAXRecords[keySDRaw] = winSecurityDescriptorDirectory
1201+
}
1202+
1203+
if h.Typeflag == tar.TypeReg {
1204+
h.PAXRecords[keySDRaw] = winSecurityDescriptorFile
1205+
}
1206+
}
1207+
1208+
// winMutator implements io.WriteCloser and transforms an incoming tar stream to fit the expected format of a Windows
1209+
// container image
1210+
type winMutator struct {
1211+
pw *io.PipeWriter
1212+
done chan error
1213+
}
1214+
1215+
func (w *winMutator) Write(b []byte) (int, error) {
1216+
return w.pw.Write(b)
1217+
}
1218+
1219+
func (w *winMutator) Close() error {
1220+
if err := w.pw.Close(); err != nil {
1221+
return fmt.Errorf("error closing pipewriter")
1222+
}
1223+
return <-w.done
1224+
}
1225+
1226+
func newWindowsMutator(outStream io.WriteCloser) *winMutator {
1227+
pr, pw := io.Pipe()
1228+
done := make(chan error)
1229+
go func() {
1230+
tarReader := tar.NewReader(pr)
1231+
tarWriter := tar.NewWriter(outStream)
1232+
1233+
err := func() error {
1234+
h := &tar.Header{
1235+
Name: "Hives",
1236+
Typeflag: tar.TypeDir,
1237+
ModTime: time.Now(),
1238+
}
1239+
prepareWinHeader(h)
1240+
if err := tarWriter.WriteHeader(h); err != nil {
1241+
return err
1242+
}
1243+
1244+
h = &tar.Header{
1245+
Name: "Files",
1246+
Typeflag: tar.TypeDir,
1247+
ModTime: time.Now(),
1248+
}
1249+
prepareWinHeader(h)
1250+
if err := tarWriter.WriteHeader(h); err != nil {
1251+
return err
1252+
}
1253+
1254+
for {
1255+
h, err := tarReader.Next()
1256+
if err == io.EOF {
1257+
break
1258+
}
1259+
if err != nil {
1260+
return err
1261+
}
1262+
h.Name = filepath.Join("Files", h.Name)
1263+
if h.Linkname != "" {
1264+
h.Linkname = filepath.Join("Files", h.Linkname)
1265+
}
1266+
prepareWinHeader(h)
1267+
addSecurityDescriptor(h)
1268+
if err := tarWriter.WriteHeader(h); err != nil {
1269+
return err
1270+
}
1271+
if h.Size > 0 {
1272+
if _, err := io.Copy(tarWriter, tarReader); err != nil {
1273+
return err
1274+
}
1275+
}
1276+
}
1277+
return tarWriter.Close()
1278+
}()
1279+
done <- err
1280+
}()
1281+
return &winMutator{pw: pw, done: done}
1282+
}
1283+
11461284
// layerWriter represents a pipeline of writers
11471285
type layerWriter struct {
11481286
outputCounter *int64
@@ -1189,15 +1327,15 @@ func (l *layerWriter) TotalWritten() int64 {
11891327
// newLayerWriter creates a writer pipeline which processes an input stream and ultimately writes to the given destination.
11901328
// The write stream pipeline is as follows:
11911329
// input -> filter -> compressor -> destination
1192-
func newLayerWriter(destination io.Writer, compression archive.Compression, layerModTime, layerLatestModTime *time.Time, layerExclusions []copier.ConditionalRemovePath) (*layerWriter, error) {
1330+
func newLayerWriter(destination io.Writer, compression archive.Compression, layerModTime, layerLatestModTime *time.Time, layerExclusions []copier.ConditionalRemovePath, windows bool) (*layerWriter, error) {
11931331
layerWriteCounter := ioutils.NewWriteCounter(destination)
11941332
var multiWriter io.Writer
11951333
var destHasher digest.Digester
11961334
srcHasher := digest.Canonical.Digester()
11971335
var closers []func() error
11981336

11991337
// If the input stream will not be different from the output stream, avoid rehashing
1200-
if compression != archive.Uncompressed || layerModTime != nil || layerLatestModTime != nil || len(layerExclusions) != 0 {
1338+
if windows || compression != archive.Uncompressed || layerModTime != nil || layerLatestModTime != nil || len(layerExclusions) != 0 {
12011339
destHasher = digest.Canonical.Digester()
12021340
multiWriter = io.MultiWriter(layerWriteCounter, destHasher.Hash())
12031341
} else {
@@ -1212,9 +1350,17 @@ func newLayerWriter(destination io.Writer, compression archive.Compression, laye
12121350
closers = append(closers, compressor.Close)
12131351
compressorHasher := io.MultiWriter(compressor, srcHasher.Hash())
12141352

1353+
// Apply Windows layer mutation if needed, before calculating the uncompressed hash
1354+
preCompressor := ioutils.NopWriteCloser(compressorHasher)
1355+
if windows {
1356+
// preCompressor.Close is owned by makeFilteredLayerWriteCloser
1357+
preCompressor = newWindowsMutator(preCompressor)
1358+
}
1359+
12151360
// Use specified timestamps in the layer, if we're doing that for history entries.
1216-
filter := makeFilteredLayerWriteCloser(ioutils.NopWriteCloser(compressorHasher), layerModTime, layerLatestModTime, layerExclusions)
1361+
filter := makeFilteredLayerWriteCloser(preCompressor, layerModTime, layerLatestModTime, layerExclusions)
12171362
closers = append(closers, filter.Close)
1363+
12181364
return &layerWriter{
12191365
outputCounter: &layerWriteCounter.Count,
12201366
inputDigester: srcHasher,
@@ -1720,6 +1866,7 @@ func (b *Builder) makeContainerImageRef(options CommitOptions) (*containerImageR
17201866
layerMountTargets: layerMountTargets,
17211867
layerPullUps: layerPullUps,
17221868
createdAnnotation: options.CreatedAnnotation,
1869+
os: b.OCIv1.OS,
17231870
}
17241871
if ref.created != nil {
17251872
for i := range ref.preEmptyLayers {

tests/bud.bats

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
load helpers
44

5+
# do not merge
6+
57
@test "bud with a path to a Dockerfile (-f) containing a non-directory entry" {
68
run_buildah 125 build -f $BUDFILES/non-directory-in-path/non-directory/Dockerfile
79
expect_output --substring "non-directory/Dockerfile: not a directory"

0 commit comments

Comments
 (0)