-
Notifications
You must be signed in to change notification settings - Fork 193
Expand file tree
/
Copy pathAcrSdkProvider.cs
More file actions
212 lines (189 loc) · 9.04 KB
/
AcrSdkProvider.cs
File metadata and controls
212 lines (189 loc) · 9.04 KB
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
// --------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
// --------------------------------------------------------------------------------------------
using System;
using System.Formats.Tar;
using System.IO;
using System.IO.Compression;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Oryx.BuildScriptGenerator.Common;
namespace Microsoft.Oryx.BuildScriptGenerator
{
/// <summary>
/// Fetches SDK tarballs directly from an OCI container registry.
/// SDK images are single-layer <c>FROM scratch</c> images containing a single
/// <c>.tar.gz</c> SDK file. The OCI layer blob is a tar archive of the image
/// filesystem, so this provider downloads the layer, extracts the inner SDK
/// tarball from it, and caches it locally.
/// </summary>
/// <remarks>
/// Makes direct HTTP calls to the registry (no Unix socket).
/// See <see cref="ExternalAcrSdkProvider"/> for the socket-based variant.
/// </remarks>
public class AcrSdkProvider : IAcrSdkProvider
{
private readonly ILogger<AcrSdkProvider> logger;
private readonly IStandardOutputWriter outputWriter;
private readonly BuildScriptGeneratorOptions options;
private readonly OciRegistryClient ociClient;
public AcrSdkProvider(
IStandardOutputWriter outputWriter,
ILogger<AcrSdkProvider> logger,
IOptions<BuildScriptGeneratorOptions> options,
OciRegistryClient ociClient)
{
this.logger = logger;
this.outputWriter = outputWriter;
this.options = options.Value;
this.ociClient = ociClient;
}
/// <inheritdoc/>
public async Task<bool> RequestSdkFromAcrAsync(string platformName, string version, string debianFlavor, string runtimeVersion = null)
{
if (string.IsNullOrEmpty(platformName))
{
throw new ArgumentException("Platform name cannot be null or empty.", nameof(platformName));
}
if (string.IsNullOrEmpty(version))
{
throw new ArgumentException("Version cannot be null or empty.", nameof(version));
}
if (string.IsNullOrEmpty(debianFlavor))
{
debianFlavor = this.options.DebianFlavor ?? "bookworm";
}
var repository = SdkStorageConstants.GetSdkImageRepository(platformName, this.options.OryxAcrSdkRepositoryPrefix);
var tag = string.IsNullOrEmpty(runtimeVersion)
? $"{debianFlavor}-{version}"
: $"{debianFlavor}-{version}_{runtimeVersion}";
var blobName = $"{platformName}-{debianFlavor}-{version}.tar.gz";
this.logger.LogInformation(
"Requesting SDK from ACR: {Repository}:{Tag}",
repository,
tag);
this.outputWriter.WriteLine(
$"Requesting SDK from ACR: {repository}:{tag}");
// Download to the writable dynamic install directory, NOT /var/OryxSdks (read-only external mount).
var downloadDir = Path.Combine(this.options.DynamicInstallRootDir, platformName);
var tarballPath = Path.Combine(downloadDir, blobName);
var digestPath = Path.Combine(downloadDir, $".{blobName}.digest");
try
{
// Get image manifest
var remoteDigest = await this.ociClient.GetManifestDigestAsync(repository, tag);
// Check if cached tarball is still fresh
if (File.Exists(tarballPath) && File.Exists(digestPath) && remoteDigest != null)
{
var localDigest = File.ReadAllText(digestPath).Trim();
if (string.Equals(localDigest, remoteDigest, StringComparison.OrdinalIgnoreCase))
{
this.logger.LogInformation(
"SDK cache is fresh (digest match): {FilePath}",
tarballPath);
this.outputWriter.WriteLine(
$"SDK tarball already cached and fresh at {tarballPath}");
return true;
}
this.logger.LogInformation(
"SDK cache is stale (digest mismatch). Re-downloading.");
}
// Get manifest → extract single layer digest
var manifest = await this.ociClient.GetManifestAsync(repository, tag);
var layerDigest = OciRegistryClient.GetFirstLayerDigest(manifest);
if (string.IsNullOrEmpty(layerDigest))
{
this.logger.LogWarning(
"No layer found in manifest for {Repository}:{Tag}",
repository,
tag);
this.outputWriter.WriteLine($"No layer found in ACR manifest for {platformName} {version}.");
return false;
}
Directory.CreateDirectory(downloadDir);
// 2. Download the OCI layer blob to a temp file.
// The layer is a tar archive of the image filesystem (not the SDK tarball itself).
var layerTempPath = Path.Combine(downloadDir, $".layer-{Guid.NewGuid():N}.tmp");
try
{
var downloadSuccess = await this.ociClient.DownloadLayerBlobAsync(
repository,
layerDigest,
layerTempPath);
if (!downloadSuccess)
{
this.logger.LogWarning(
"ACR SDK pull failed digest verification: {Repository}:{Tag}",
repository,
tag);
this.outputWriter.WriteLine(
$"Failed to pull SDK from ACR (digest mismatch): {platformName} {version}");
return false;
}
// 3. Extract the inner SDK .tar.gz from the layer tar.
// The image is FROM scratch with a single COPY of the SDK tarball,
// so the layer contains the .tar.gz as a top-level entry.
this.ExtractFileFromTar(layerTempPath, tarballPath, blobName);
}
finally
{
// Always clean up the temporary layer file
if (File.Exists(layerTempPath))
{
File.Delete(layerTempPath);
}
}
this.logger.LogInformation(
"Successfully pulled SDK from ACR: {Repository}:{Tag} → {FilePath}",
repository,
tag,
tarballPath);
this.outputWriter.WriteLine(
$"Successfully pulled SDK from ACR: {platformName} {version}");
// Write manifest digest sidecar for future freshness checks
if (!string.IsNullOrEmpty(remoteDigest))
{
File.WriteAllText(digestPath, remoteDigest);
}
return true;
}
catch (Exception ex)
{
this.logger.LogError(
ex,
"Error pulling SDK from ACR: {Repository}:{Tag}",
repository,
tag);
this.outputWriter.WriteLine(
$"Error pulling SDK from ACR: {platformName} {version}: {ex.Message}");
return false;
}
}
/// <summary>
/// Extracts the expected SDK .tar.gz file from an OCI layer tar archive.
/// OCI layers use media type "application/vnd.docker.image.rootfs.diff.tar.gzip",
/// so the blob must be decompressed before reading tar entries.
/// </summary>
private void ExtractFileFromTar(string layerPath, string outputPath, string expectedFileName)
{
using (var stream = File.OpenRead(layerPath))
using (var gzipStream = new GZipStream(stream, CompressionMode.Decompress))
using (var tarReader = new TarReader(gzipStream))
{
TarEntry entry;
while ((entry = tarReader.GetNextEntry()) != null)
{
var name = entry.Name.TrimStart('.', '/');
if (entry.DataStream != null && name.Equals(expectedFileName, StringComparison.OrdinalIgnoreCase))
{
entry.ExtractToFile(outputPath, overwrite: true);
return;
}
}
}
throw new InvalidOperationException($"Expected entry '{expectedFileName}' not found in OCI layer: {layerPath}");
}
}
}