-
Notifications
You must be signed in to change notification settings - Fork 1.1k
/
Copy pathLayer.cs
283 lines (235 loc) · 11.7 KB
/
Layer.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics;
using System.Formats.Tar;
using System.IO.Compression;
using System.IO.Enumeration;
using System.Security.Cryptography;
using Microsoft.NET.Build.Containers.Resources;
namespace Microsoft.NET.Build.Containers;
internal class Layer
{
// NOTE: The SID string below was created using the following snippet. As the code is Windows only we keep the constant
// private static string CreateUserOwnerAndGroupSID()
// {
// var descriptor = new RawSecurityDescriptor(
// ControlFlags.SelfRelative,
// new SecurityIdentifier(WellKnownSidType.BuiltinUsersSid, null),
// new SecurityIdentifier(WellKnownSidType.BuiltinUsersSid, null),
// null,
// null
// );
//
// var raw = new byte[descriptor.BinaryLength];
// descriptor.GetBinaryForm(raw, 0);
// return Convert.ToBase64String(raw);
// }
private const string BuiltinUsersSecurityDescriptor = "AQAAgBQAAAAkAAAAAAAAAAAAAAABAgAAAAAABSAAAAAhAgAAAQIAAAAAAAUgAAAAIQIAAA==";
public virtual Descriptor Descriptor { get; }
public string BackingFile { get; }
internal Layer()
{
Descriptor = new Descriptor();
BackingFile = "";
}
internal Layer(string backingFile, Descriptor descriptor)
{
BackingFile = backingFile;
Descriptor = descriptor;
}
public static Layer FromDescriptor(Descriptor descriptor)
{
return new(ContentStore.PathForDescriptor(descriptor), descriptor);
}
public static Layer FromDirectory(string directory, string containerPath, bool isWindowsLayer, string manifestMediaType)
{
long fileSize;
Span<byte> hash = stackalloc byte[SHA256.HashSizeInBytes];
Span<byte> uncompressedHash = stackalloc byte[SHA256.HashSizeInBytes];
// Docker treats a COPY instruction that copies to a path like `/app` by
// including `app/` as a directory, with no leading slash. Emulate that here.
containerPath = containerPath.TrimStart(PathSeparators);
// For Windows layers we need to put files into a "Files" directory without drive letter.
if (isWindowsLayer)
{
// Cut of drive letter: /* C:\ */
if (containerPath[1] == ':')
{
containerPath = containerPath[3..];
}
containerPath = "Files/" + containerPath;
}
// Trim training path separator (if present).
containerPath = containerPath.TrimEnd(PathSeparators);
// Use only '/' as directory separator.
containerPath = containerPath.Replace('\\', '/');
var entryAttributes = new Dictionary<string, string>();
if (isWindowsLayer)
{
// We grant all users access to the application directory
// https://github.com/buildpacks/rfcs/blob/main/text/0076-windows-security-identifiers.md
entryAttributes["MSWINDOWS.rawsd"] = BuiltinUsersSecurityDescriptor;
}
string tempTarballPath = ContentStore.GetTempFile();
using (FileStream fs = File.Create(tempTarballPath))
{
using (HashDigestGZipStream gz = new(fs, leaveOpen: true))
{
using (TarWriter writer = new(gz, TarEntryFormat.Pax, leaveOpen: true))
{
// Windows layers need a Files folder
if (isWindowsLayer)
{
var entry = new PaxTarEntry(TarEntryType.Directory, "Files", entryAttributes);
writer.WriteEntry(entry);
}
// Write an entry for the application directory.
WriteTarEntryForFile(writer, new DirectoryInfo(directory), containerPath, entryAttributes);
// Write entries for the application directory contents.
var fileList = new FileSystemEnumerable<(FileSystemInfo file, string containerPath)>(
directory: directory,
transform: (ref FileSystemEntry entry) =>
{
FileSystemInfo fsi = entry.ToFileSystemInfo();
string relativePath = Path.GetRelativePath(directory, fsi.FullName);
if (OperatingSystem.IsWindows())
{
// Use only '/' directory separators.
relativePath = relativePath.Replace('\\', '/');
}
return (fsi, $"{containerPath}/{relativePath}");
},
options: new EnumerationOptions()
{
AttributesToSkip = FileAttributes.System, // Include hidden files
RecurseSubdirectories = true
});
foreach (var item in fileList)
{
WriteTarEntryForFile(writer, item.file, item.containerPath, entryAttributes);
}
// Windows layers need a Hives folder, we do not need to create any Registry Hive deltas inside
if (isWindowsLayer)
{
var entry = new PaxTarEntry(TarEntryType.Directory, "Hives", entryAttributes);
writer.WriteEntry(entry);
}
} // Dispose of the TarWriter before getting the hash so the final data get written to the tar stream
int bytesWritten = gz.GetCurrentUncompressedHash(uncompressedHash);
Debug.Assert(bytesWritten == uncompressedHash.Length);
}
fileSize = fs.Length;
fs.Position = 0;
int bW = SHA256.HashData(fs, hash);
Debug.Assert(bW == hash.Length);
// Writes a tar entry corresponding to the file system item.
static void WriteTarEntryForFile(TarWriter writer, FileSystemInfo file, string containerPath, IEnumerable<KeyValuePair<string, string>> entryAttributes)
{
UnixFileMode mode = DetermineFileMode(file);
if (file is FileInfo)
{
using var fileStream = File.OpenRead(file.FullName);
PaxTarEntry entry = new(TarEntryType.RegularFile, containerPath, entryAttributes)
{
Mode = mode,
DataStream = fileStream
};
writer.WriteEntry(entry);
}
else
{
PaxTarEntry entry = new(TarEntryType.Directory, containerPath, entryAttributes)
{
Mode = mode
};
writer.WriteEntry(entry);
}
static UnixFileMode DetermineFileMode(FileSystemInfo file)
{
const UnixFileMode nonExecuteMode = UnixFileMode.UserRead | UnixFileMode.UserWrite |
UnixFileMode.GroupRead |
UnixFileMode.OtherRead;
const UnixFileMode executeMode = nonExecuteMode | UnixFileMode.UserExecute | UnixFileMode.GroupExecute | UnixFileMode.OtherExecute;
// On Unix, we can determine the x-bit based on the filesystem permission.
// On Windows, we use executable permissions for all entries.
return (OperatingSystem.IsWindows() || ((file.UnixFileMode | UnixFileMode.UserExecute) != 0)) ? executeMode : nonExecuteMode;
}
}
}
string contentHash = Convert.ToHexStringLower(hash);
string uncompressedContentHash = Convert.ToHexStringLower(uncompressedHash);
string layerMediaType = manifestMediaType switch
{
// TODO: configurable? gzip always?
SchemaTypes.DockerManifestV2 => SchemaTypes.DockerLayerGzip,
SchemaTypes.OciManifestV1 => SchemaTypes.OciLayerGzipV1,
_ => throw new ArgumentException(Resource.FormatString(nameof(Strings.UnrecognizedMediaType), manifestMediaType))
};
Descriptor descriptor = new()
{
MediaType = layerMediaType,
Size = fileSize,
Digest = $"sha256:{contentHash}",
UncompressedDigest = $"sha256:{uncompressedContentHash}",
};
string storedContent = ContentStore.PathForDescriptor(descriptor);
Directory.CreateDirectory(ContentStore.ContentRoot);
File.Move(tempTarballPath, storedContent, overwrite: true);
return new(storedContent, descriptor);
}
internal virtual Stream OpenBackingFile() => File.OpenRead(BackingFile);
private static readonly char[] PathSeparators = new char[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar };
/// <summary>
/// A stream capable of computing the hash digest of raw uncompressed data while also compressing it.
/// </summary>
private sealed class HashDigestGZipStream : Stream
{
private readonly IncrementalHash sha256Hash;
private readonly GZipStream compressionStream;
public HashDigestGZipStream(Stream writeStream, bool leaveOpen)
{
sha256Hash = IncrementalHash.CreateHash(HashAlgorithmName.SHA256);
compressionStream = new GZipStream(writeStream, CompressionMode.Compress, leaveOpen);
}
public override bool CanWrite => true;
public override void Write(byte[] buffer, int offset, int count)
{
sha256Hash.AppendData(buffer, offset, count);
compressionStream.Write(buffer, offset, count);
}
public override void Write(ReadOnlySpan<byte> buffer)
{
sha256Hash.AppendData(buffer);
compressionStream.Write(buffer);
}
public override void Flush()
{
compressionStream.Flush();
}
internal int GetCurrentUncompressedHash(Span<byte> buffer) => sha256Hash.GetCurrentHash(buffer);
protected override void Dispose(bool disposing)
{
try
{
sha256Hash.Dispose();
compressionStream.Dispose();
}
finally
{
base.Dispose(disposing);
}
}
// This class is never used with async writes, but if it ever is, implement these overrides
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
=> throw new NotImplementedException();
public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken)
=> throw new NotImplementedException();
public override bool CanRead => false;
public override bool CanSeek => false;
public override long Length => throw new NotImplementedException();
public override long Position { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
public override int Read(byte[] buffer, int offset, int count) => throw new NotImplementedException();
public override long Seek(long offset, SeekOrigin origin) => throw new NotImplementedException();
public override void SetLength(long value) => throw new NotImplementedException();
}
}