Skip to content

Commit 8b321c2

Browse files
authored
Generate an empty JSON object for container registry configs (#2079)
1 parent e708511 commit 8b321c2

2 files changed

Lines changed: 184 additions & 36 deletions

File tree

‎src/code/ContainerRegistryServerAPICalls.cs‎

Lines changed: 20 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ internal class ContainerRegistryServerAPICalls : ServerApiCall
5252
const string catalogScope = "&scope=registry:catalog:*";
5353
const string grantTypeTemplate = "grant_type=access_token&service={0}{1}"; // 0 - registry, 1 - scope
5454
const string authUrlTemplate = "{0}?service={1}{2}"; // 0 - realm, 1 - service, 2 - scope
55+
const string emptyConfigContent = "{}";
5556

5657
const string containerRegistryRepositoryListTemplate = "https://{0}/v2/_catalog"; // 0 - registry
5758

@@ -1568,17 +1569,16 @@ internal bool PushNupkgContainerRegistry(
15681569
return false;
15691570
}
15701571

1571-
// Create and upload an empty file-- needed by ContainerRegistry server
1572-
CreateAndUploadEmptyFile(outputNupkgDir, packageNameLowercase, containerRegistryAccessToken, out errRecord);
1572+
// Create config file
1573+
var configFilePath = System.IO.Path.Combine(outputNupkgDir, Guid.NewGuid().ToString() + ".json");
1574+
_cmdletPassedIn.WriteVerbose($"Create config file at path: {configFilePath}");
1575+
string configDigest = CreateConfigFile(configFilePath, out errRecord);
15731576
if (errRecord != null)
15741577
{
15751578
return false;
15761579
}
15771580

1578-
// Create config.json file
1579-
var configFilePath = System.IO.Path.Combine(outputNupkgDir, "config.json");
1580-
_cmdletPassedIn.WriteVerbose($"Create config.json file at path: {configFilePath}");
1581-
string configDigest = CreateConfigFile(configFilePath, out errRecord);
1581+
UploadConfigFile(configFilePath, configDigest, packageNameLowercase, containerRegistryAccessToken, out errRecord);
15821582
if (errRecord != null)
15831583
{
15841584
return false;
@@ -1667,38 +1667,27 @@ private string UploadNupkgFile(string packageNameLowercase, string containerRegi
16671667
}
16681668

16691669
/// <summary>
1670-
/// Uploads an empty file at the start of publish as is needed.
1670+
/// Uploads the config file referenced by the package manifest.
16711671
/// </summary>
1672-
private void CreateAndUploadEmptyFile(string outputNupkgDir, string pkgNameLower, string containerRegistryAccessToken, out ErrorRecord errRecord)
1672+
private void UploadConfigFile(string configFilePath, string configDigest, string pkgNameLower, string containerRegistryAccessToken, out ErrorRecord errRecord)
16731673
{
1674-
_cmdletPassedIn.WriteDebug("In ContainerRegistryServerAPICalls::CreateAndUploadEmptyFile()");
1675-
_cmdletPassedIn.WriteVerbose("Create an empty file");
1676-
string emptyFileName = "empty" + Guid.NewGuid().ToString() + ".txt";
1677-
var emptyFilePath = System.IO.Path.Combine(outputNupkgDir, emptyFileName);
1674+
_cmdletPassedIn.WriteDebug("In ContainerRegistryServerAPICalls::UploadConfigFile()");
1675+
errRecord = null;
16781676

16791677
try
16801678
{
1681-
Utils.CreateFile(emptyFilePath);
1682-
1683-
_cmdletPassedIn.WriteVerbose("Start uploading an empty file");
1684-
string emptyLocation = GetStartUploadBlobLocation(pkgNameLower, containerRegistryAccessToken).Result;
1685-
1686-
_cmdletPassedIn.WriteVerbose("Computing digest for empty file");
1687-
string emptyFileDigest = CreateDigest(emptyFilePath, out errRecord);
1688-
if (errRecord != null)
1689-
{
1690-
return;
1691-
}
1679+
_cmdletPassedIn.WriteVerbose("Start uploading the config file");
1680+
string configLocation = GetStartUploadBlobLocation(pkgNameLower, containerRegistryAccessToken).Result;
16921681

1693-
_cmdletPassedIn.WriteVerbose("Finish uploading empty file");
1694-
var emptyResponse = EndUploadBlob(emptyLocation, emptyFilePath, emptyFileDigest, false, containerRegistryAccessToken).Result;
1695-
bool uploadSuccessful = emptyResponse.IsSuccessStatusCode;
1682+
_cmdletPassedIn.WriteVerbose("Finish uploading the config file");
1683+
var configResponse = EndUploadBlob(configLocation, configFilePath, configDigest, false, containerRegistryAccessToken).Result;
1684+
bool uploadSuccessful = configResponse.IsSuccessStatusCode;
16961685

16971686
if (!uploadSuccessful)
16981687
{
16991688
errRecord = new ErrorRecord(
1700-
new UploadBlobException($"Error occurred while uploading blob, response code was: {emptyResponse.StatusCode} with reason {emptyResponse.ReasonPhrase}"),
1701-
"UploadEmptyFileError",
1689+
new UploadBlobException($"Error occurred while uploading blob, response code was: {configResponse.StatusCode} with reason {configResponse.ReasonPhrase}"),
1690+
"UploadConfigFileError",
17021691
ErrorCategory.InvalidResult,
17031692
_cmdletPassedIn);
17041693

@@ -1709,7 +1698,7 @@ private void CreateAndUploadEmptyFile(string outputNupkgDir, string pkgNameLower
17091698
{
17101699
errRecord = new ErrorRecord(
17111700
e,
1712-
"UploadEmptyFileError",
1701+
"UploadConfigFileError",
17131702
ErrorCategory.InvalidResult,
17141703
_cmdletPassedIn);
17151704

@@ -1725,14 +1714,9 @@ private string CreateConfigFile(string configFilePath, out ErrorRecord errRecord
17251714
_cmdletPassedIn.WriteDebug("In ContainerRegistryServerAPICalls::CreateConfigFile()");
17261715
string configFileDigest = string.Empty;
17271716
_cmdletPassedIn.WriteVerbose("Create the config file");
1728-
while (File.Exists(configFilePath))
1729-
{
1730-
configFilePath = Guid.NewGuid().ToString() + ".json";
1731-
}
1732-
17331717
try
17341718
{
1735-
Utils.CreateFile(configFilePath);
1719+
File.WriteAllText(configFilePath, emptyConfigContent);
17361720

17371721
_cmdletPassedIn.WriteVerbose("Computing digest for config");
17381722
configFileDigest = CreateDigest(configFilePath, out errRecord);
@@ -1837,7 +1821,7 @@ private string CreateManifestContent(
18371821
jsonWriter.WritePropertyName("digest");
18381822
jsonWriter.WriteValue($"sha256:{configDigest}");
18391823
jsonWriter.WritePropertyName("size");
1840-
jsonWriter.WriteValue(0);
1824+
jsonWriter.WriteValue(Encoding.UTF8.GetByteCount(emptyConfigContent));
18411825
jsonWriter.WriteEndObject();
18421826

18431827
jsonWriter.WritePropertyName("layers");
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
4+
Import-Module "$PSScriptRoot\..\PSGetTestUtils.psm1" -Force
5+
6+
Describe 'Container registry config blob' -Tags 'CI' {
7+
BeforeAll {
8+
Add-Type -AssemblyName System.Net.Http
9+
$references = @(
10+
[System.Net.Http.HttpClient].Assembly.Location
11+
[System.Net.HttpStatusCode].Assembly.Location
12+
)
13+
Add-Type -ReferencedAssemblies $references -TypeDefinition @'
14+
using System;
15+
using System.Net;
16+
using System.Net.Http;
17+
using System.Threading;
18+
using System.Threading.Tasks;
19+
20+
public class ConfigBlobUploadHandler : HttpMessageHandler
21+
{
22+
public byte[] UploadedContent { get; private set; }
23+
public byte[] UploadedManifest { get; private set; }
24+
public Uri UploadUri { get; private set; }
25+
public HttpStatusCode UploadStatusCode = HttpStatusCode.Created;
26+
27+
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
28+
{
29+
if (request.Method == HttpMethod.Post)
30+
{
31+
var response = new HttpResponseMessage(HttpStatusCode.Accepted);
32+
response.Headers.Location = new Uri("/v2/test/blobs/uploads/id?upload=1", UriKind.Relative);
33+
return response;
34+
}
35+
36+
var content = await request.Content.ReadAsByteArrayAsync();
37+
if (request.RequestUri.AbsolutePath.Contains("/manifests/"))
38+
{
39+
UploadedManifest = content;
40+
}
41+
else
42+
{
43+
UploadedContent = content;
44+
UploadUri = request.RequestUri;
45+
}
46+
47+
return new HttpResponseMessage(UploadStatusCode);
48+
}
49+
}
50+
'@
51+
52+
function Invoke-ContainerRegistryMethod {
53+
[CmdletBinding()]
54+
param($MethodName, [object[]] $MethodArguments, $Handler)
55+
56+
$type = [Microsoft.PowerShell.PSResourceGet.Cmdlets.PublishPSResource].Assembly.GetType('Microsoft.PowerShell.PSResourceGet.ContainerRegistryServerAPICalls')
57+
$repository = [Microsoft.PowerShell.PSResourceGet.UtilClasses.PSRepositoryInfo]::new(
58+
'Test', [uri]'https://registry.example', 50, $true, $null, 'None', 'ContainerRegistry', $true)
59+
$api = [Activator]::CreateInstance($type, @($repository, $PSCmdlet, $null, 'test'))
60+
$flags = [Reflection.BindingFlags]'Instance,NonPublic'
61+
$clientProperty = $type.GetProperty('_sessionClient', $flags)
62+
try {
63+
if ($Handler) {
64+
$clientProperty.GetValue($api).Dispose()
65+
$clientProperty.SetValue($api, [System.Net.Http.HttpClient]::new($Handler))
66+
$type.GetField('_cachedContainterRegistryToken', $flags).SetValue($api, 'test-token')
67+
}
68+
69+
for ($i = 0; $i -lt $MethodArguments.Length; $i++) {
70+
if ($null -ne $MethodArguments[$i]) {
71+
$MethodArguments[$i] = $MethodArguments[$i].PSObject.BaseObject
72+
}
73+
}
74+
75+
$type.GetMethod($MethodName, $flags).Invoke($api, $MethodArguments)
76+
}
77+
finally {
78+
$clientProperty.GetValue($api).Dispose()
79+
}
80+
}
81+
}
82+
83+
BeforeEach {
84+
$configPath = Join-Path $TestDrive "$([guid]::NewGuid()).json"
85+
$configArguments = @($configPath, $null)
86+
$configDigest = Invoke-ContainerRegistryMethod 'CreateConfigFile' $configArguments
87+
}
88+
89+
It 'Creates a config containing exactly an empty JSON object and hashes those bytes' {
90+
$configArguments[1] | Should -BeNullOrEmpty
91+
$content = [IO.File]::ReadAllBytes($configPath)
92+
$content.Length | Should -Be 2
93+
[Text.Encoding]::UTF8.GetString($content) | Should -BeExactly '{}'
94+
$configDigest | Should -BeExactly (Get-FileHash $configPath -Algorithm SHA256).Hash.ToLowerInvariant()
95+
}
96+
97+
It 'Describes the config bytes in the manifest without changing the package layer' {
98+
$manifestArguments = @(
99+
'packageDigest', $configDigest, [long]123, 'test.nupkg', 'test',
100+
[Microsoft.PowerShell.PSResourceGet.UtilClasses.ResourceType]::Module, '{}')
101+
$manifest = Invoke-ContainerRegistryMethod 'CreateManifestContent' $manifestArguments | ConvertFrom-Json
102+
103+
$manifest.config.mediaType | Should -BeExactly 'application/vnd.oci.image.config.v1+json'
104+
$manifest.config.digest | Should -BeExactly "sha256:$configDigest"
105+
$manifest.config.size | Should -Be (Get-Item $configPath).Length
106+
$manifest.layers.Count | Should -Be 1
107+
$manifest.layers[0].digest | Should -BeExactly 'sha256:packageDigest'
108+
$manifest.layers[0].size | Should -Be 123
109+
}
110+
111+
It 'Uploads the config JSON with its matching digest' {
112+
$handler = [ConfigBlobUploadHandler]::new()
113+
$uploadArguments = @($configPath, $configDigest, 'test', 'test-token', $null)
114+
Invoke-ContainerRegistryMethod 'UploadConfigFile' $uploadArguments $handler
115+
116+
$uploadArguments[4] | Should -BeNullOrEmpty
117+
$handler.UploadedContent.Length | Should -Be 2
118+
[Text.Encoding]::UTF8.GetString($handler.UploadedContent) | Should -BeExactly '{}'
119+
$handler.UploadUri.Query | Should -Match "digest=sha256:$configDigest"
120+
}
121+
122+
It 'Reports a failed config upload' {
123+
$handler = [ConfigBlobUploadHandler]::new()
124+
$handler.UploadStatusCode = [System.Net.HttpStatusCode]::BadRequest
125+
$uploadArguments = @($configPath, $configDigest, 'test', 'test-token', $null)
126+
Invoke-ContainerRegistryMethod 'UploadConfigFile' $uploadArguments $handler
127+
128+
$uploadArguments[4].FullyQualifiedErrorId | Should -BeExactly 'UploadConfigFileError'
129+
}
130+
131+
It 'Publishes matching config bytes and manifest when the module already contains config.json' {
132+
$outputDirectory = Join-Path $TestDrive 'nupkg'
133+
$null = New-Item $outputDirectory -ItemType Directory
134+
$existingConfigPath = Join-Path $outputDirectory 'config.json'
135+
$existingConfigContent = '{"module":true}'
136+
[IO.File]::WriteAllText($existingConfigPath, $existingConfigContent)
137+
[IO.File]::WriteAllText((Join-Path $outputDirectory 'test.1.0.0.nupkg'), 'package')
138+
139+
$handler = [ConfigBlobUploadHandler]::new()
140+
$type = [Microsoft.PowerShell.PSResourceGet.Cmdlets.PublishPSResource].Assembly.GetType('Microsoft.PowerShell.PSResourceGet.ContainerRegistryServerAPICalls')
141+
$versionType = $type.GetMethod('PushNupkgContainerRegistry', [Reflection.BindingFlags]'Instance,NonPublic').GetParameters()[3].ParameterType
142+
$version = [Activator]::CreateInstance($versionType, @('1.0.0'))
143+
$publishArguments = @(
144+
$outputDirectory, 'test', $null, $version,
145+
[Microsoft.PowerShell.PSResourceGet.UtilClasses.ResourceType]::Module,
146+
@{ ModuleVersion = '1.0.0' }, @{}, $false, $null, $null)
147+
Push-Location $TestDrive
148+
try {
149+
$published = Invoke-ContainerRegistryMethod 'PushNupkgContainerRegistry' $publishArguments $handler
150+
}
151+
finally {
152+
Pop-Location
153+
}
154+
155+
$published | Should -Be $true
156+
$publishArguments[9] | Should -BeNullOrEmpty
157+
[Text.Encoding]::UTF8.GetString($handler.UploadedContent) | Should -BeExactly '{}'
158+
$handler.UploadUri.Query | Should -Match "digest=sha256:$configDigest"
159+
$manifest = [Text.Encoding]::UTF8.GetString($handler.UploadedManifest) | ConvertFrom-Json
160+
$manifest.config.digest | Should -BeExactly "sha256:$configDigest"
161+
$manifest.config.size | Should -Be $handler.UploadedContent.Length
162+
[IO.File]::ReadAllText($existingConfigPath) | Should -BeExactly $existingConfigContent
163+
}
164+
}

0 commit comments

Comments
 (0)