Skip to content

Commit 9c4aa2c

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 <[email protected]>
1 parent 12386f3 commit 9c4aa2c

File tree

2 files changed

+138
-5
lines changed

2 files changed

+138
-5
lines changed

image.go

Lines changed: 136 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,98 @@ 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+
func windowsMutator(w io.WriteCloser) io.WriteCloser {
1209+
pr, pw := io.Pipe()
1210+
1211+
go func() {
1212+
tarReader := tar.NewReader(pr)
1213+
tarWriter := tar.NewWriter(w)
1214+
1215+
err := func() error {
1216+
h := &tar.Header{
1217+
Name: "Hives",
1218+
Typeflag: tar.TypeDir,
1219+
ModTime: time.Now(),
1220+
}
1221+
prepareWinHeader(h)
1222+
if err := tarWriter.WriteHeader(h); err != nil {
1223+
return err
1224+
}
1225+
1226+
h = &tar.Header{
1227+
Name: "Files",
1228+
Typeflag: tar.TypeDir,
1229+
ModTime: time.Now(),
1230+
}
1231+
prepareWinHeader(h)
1232+
if err := tarWriter.WriteHeader(h); err != nil {
1233+
return err
1234+
}
1235+
1236+
for {
1237+
h, err := tarReader.Next()
1238+
if err == io.EOF {
1239+
break
1240+
}
1241+
if err != nil {
1242+
return err
1243+
}
1244+
h.Name = filepath.Join("Files", h.Name)
1245+
if h.Linkname != "" {
1246+
h.Linkname = filepath.Join("Files", h.Linkname)
1247+
}
1248+
prepareWinHeader(h)
1249+
addSecurityDescriptor(h)
1250+
if err := tarWriter.WriteHeader(h); err != nil {
1251+
return err
1252+
}
1253+
if h.Size > 0 {
1254+
if _, err := io.Copy(tarWriter, tarReader); err != nil {
1255+
return err
1256+
}
1257+
}
1258+
}
1259+
return tarWriter.Close()
1260+
}()
1261+
if err != nil {
1262+
logrus.Errorf("makeWindowsLayer %v", err)
1263+
}
1264+
pw.CloseWithError(err)
1265+
}()
1266+
return pw
1267+
}
1268+
11461269
// layerWriter represents a pipeline of writers
11471270
type layerWriter struct {
11481271
outputCounter *int64
@@ -1189,15 +1312,15 @@ func (l *layerWriter) TotalWritten() int64 {
11891312
// newLayerWriter creates a writer pipeline which processes an input stream and ultimately writes to the given destination.
11901313
// The write stream pipeline is as follows:
11911314
// input -> filter -> compressor -> destination
1192-
func newLayerWriter(destination io.Writer, compression archive.Compression, layerModTime, layerLatestModTime *time.Time, layerExclusions []copier.ConditionalRemovePath) (*layerWriter, error) {
1315+
func newLayerWriter(destination io.Writer, compression archive.Compression, layerModTime, layerLatestModTime *time.Time, layerExclusions []copier.ConditionalRemovePath, windows bool) (*layerWriter, error) {
11931316
layerWriteCounter := ioutils.NewWriteCounter(destination)
11941317
var multiWriter io.Writer
11951318
var destHasher digest.Digester
11961319
srcHasher := digest.Canonical.Digester()
11971320
var closers []func() error
11981321

11991322
// 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 {
1323+
if windows || compression != archive.Uncompressed || layerModTime != nil || layerLatestModTime != nil || len(layerExclusions) != 0 {
12011324
destHasher = digest.Canonical.Digester()
12021325
multiWriter = io.MultiWriter(layerWriteCounter, destHasher.Hash())
12031326
} else {
@@ -1212,9 +1335,16 @@ func newLayerWriter(destination io.Writer, compression archive.Compression, laye
12121335
closers = append(closers, compressor.Close)
12131336
compressorHasher := io.MultiWriter(compressor, srcHasher.Hash())
12141337

1338+
// Apply Windows layer mutation if needed, before calculating the uncompressed hash
1339+
preCompressor := ioutils.NopWriteCloser(compressorHasher)
1340+
if windows {
1341+
preCompressor = windowsMutator(preCompressor)
1342+
}
1343+
12151344
// Use specified timestamps in the layer, if we're doing that for history entries.
1216-
filter := makeFilteredLayerWriteCloser(ioutils.NopWriteCloser(compressorHasher), layerModTime, layerLatestModTime, layerExclusions)
1345+
filter := makeFilteredLayerWriteCloser(preCompressor, layerModTime, layerLatestModTime, layerExclusions)
12171346
closers = append(closers, filter.Close)
1347+
12181348
return &layerWriter{
12191349
outputCounter: &layerWriteCounter.Count,
12201350
inputDigester: srcHasher,
@@ -1720,6 +1850,7 @@ func (b *Builder) makeContainerImageRef(options CommitOptions) (*containerImageR
17201850
layerMountTargets: layerMountTargets,
17211851
layerPullUps: layerPullUps,
17221852
createdAnnotation: options.CreatedAnnotation,
1853+
os: b.OCIv1.OS,
17231854
}
17241855
if ref.created != nil {
17251856
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)