Skip to content

Commit 7b0463f

Browse files
authored
ACR Integration: Populate metadata into PSResourceInfo object (PowerShell#1548)
1 parent 9738f15 commit 7b0463f

File tree

2 files changed

+273
-104
lines changed

2 files changed

+273
-104
lines changed

src/code/ACRServerAPICalls.cs

Lines changed: 178 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -173,32 +173,13 @@ public override FindResults FindName(string packageName, bool includePrerelease,
173173
return new FindResults(stringResponse: new string[] { }, hashtableResponse: emptyHashResponses, responseType: acrFindResponseType);
174174
}
175175

176-
/* response returned looks something like:
177-
* "registry": "myregistry.azurecr.io"
178-
* "imageName": "hello-world"
179-
* "tags": [
180-
* {
181-
* ""name"": ""1.0.0"",
182-
* ""digest"": ""sha256:92c7f9c92844bbbb5d0a101b22f7c2a7949e40f8ea90c8b3bc396879d95e899a"",
183-
* ""createdTime"": ""2023-12-23T18:06:48.9975733Z"",
184-
* ""lastUpdateTime"": ""2023-12-23T18:06:48.9975733Z"",
185-
* ""signed"": false,
186-
* ""changeableAttributes"": {
187-
* ""deleteEnabled"": true,
188-
* ""writeEnabled"": true,
189-
* ""readEnabled"": true,
190-
* ""listEnabled"": true
191-
* }
192-
* }]
193-
*/
194176
List<Hashtable> latestVersionResponse = new List<Hashtable>();
195177
List<JToken> allVersionsList = foundTags["tags"].ToList();
196178
allVersionsList.Reverse();
197179

198-
foreach (var packageVersion in allVersionsList)
180+
foreach (var pkgVersionTagInfo in allVersionsList)
199181
{
200-
var packageVersionStr = packageVersion.ToString();
201-
using (JsonDocument pkgVersionEntry = JsonDocument.Parse(packageVersionStr))
182+
using (JsonDocument pkgVersionEntry = JsonDocument.Parse(pkgVersionTagInfo.ToString()))
202183
{
203184
JsonElement rootDom = pkgVersionEntry.RootElement;
204185
if (!rootDom.TryGetProperty("name", out JsonElement pkgVersionElement))
@@ -218,7 +199,11 @@ public override FindResults FindName(string packageName, bool includePrerelease,
218199
if (!pkgVersion.IsPrerelease || includePrerelease)
219200
{
220201
// Versions are always in descending order i.e 5.0.0, 3.0.0, 1.0.0 so grabbing the first match suffices
221-
latestVersionResponse.Add(new Hashtable() { { packageName, packageVersionStr } });
202+
latestVersionResponse.Add(GetACRMetadata(registry, packageName, pkgVersion, acrAccessToken, out errRecord));
203+
if (errRecord != null)
204+
{
205+
return new FindResults(stringResponse: new string[] { }, hashtableResponse: latestVersionResponse.ToArray(), responseType: acrFindResponseType);
206+
}
222207

223208
break;
224209
}
@@ -339,26 +324,9 @@ public override FindResults FindVersionGlobbing(string packageName, VersionRange
339324
return new FindResults(stringResponse: new string[] { }, hashtableResponse: emptyHashResponses, responseType: acrFindResponseType);
340325
}
341326

342-
/* response returned looks something like:
343-
* "registry": "myregistry.azurecr.io"
344-
* "imageName": "hello-world"
345-
* "tags": [
346-
* {
347-
* ""name"": ""1.0.0"",
348-
* ""digest"": ""sha256:92c7f9c92844bbbb5d0a101b22f7c2a7949e40f8ea90c8b3bc396879d95e899a"",
349-
* ""createdTime"": ""2023-12-23T18:06:48.9975733Z"",
350-
* ""lastUpdateTime"": ""2023-12-23T18:06:48.9975733Z"",
351-
* ""signed"": false,
352-
* ""changeableAttributes"": {
353-
* ""deleteEnabled"": true,
354-
* ""writeEnabled"": true,
355-
* ""readEnabled"": true,
356-
* ""listEnabled"": true
357-
* }
358-
* }]
359-
*/
360327
List<Hashtable> latestVersionResponse = new List<Hashtable>();
361328
List<JToken> allVersionsList = foundTags["tags"].ToList();
329+
allVersionsList.Reverse();
362330
foreach (var packageVersion in allVersionsList)
363331
{
364332
var packageVersionStr = packageVersion.ToString();
@@ -387,7 +355,11 @@ public override FindResults FindVersionGlobbing(string packageName, VersionRange
387355
continue;
388356
}
389357

390-
latestVersionResponse.Add(new Hashtable() { { packageName, packageVersionStr } });
358+
latestVersionResponse.Add(GetACRMetadata(registry, packageName, pkgVersion, acrAccessToken, out errRecord));
359+
if (errRecord != null)
360+
{
361+
return new FindResults(stringResponse: new string[] { }, hashtableResponse: latestVersionResponse.ToArray(), responseType: acrFindResponseType);
362+
}
391363
}
392364
}
393365
}
@@ -453,59 +425,17 @@ public override FindResults FindVersion(string packageName, string version, Reso
453425
}
454426

455427
_cmdletPassedIn.WriteVerbose("Getting tags");
456-
var foundTags = FindAcrImageTags(registry, packageName, requiredVersion.ToString(), acrAccessToken, out errRecord);
457-
if (errRecord != null || foundTags == null)
428+
List<Hashtable> results = new List<Hashtable>
458429
{
459-
return new FindResults(stringResponse: new string[] { }, hashtableResponse: emptyHashResponses, responseType: acrFindResponseType);
460-
}
461-
462-
/* response returned looks something like:
463-
* "registry": "myregistry.azurecr.io"
464-
* "imageName": "hello-world"
465-
* "tags": [
466-
* {
467-
* ""name"": ""1.0.0"",
468-
* ""digest"": ""sha256:92c7f9c92844bbbb5d0a101b22f7c2a7949e40f8ea90c8b3bc396879d95e899a"",
469-
* ""createdTime"": ""2023-12-23T18:06:48.9975733Z"",
470-
* ""lastUpdateTime"": ""2023-12-23T18:06:48.9975733Z"",
471-
* ""signed"": false,
472-
* ""changeableAttributes"": {
473-
* ""deleteEnabled"": true,
474-
* ""writeEnabled"": true,
475-
* ""readEnabled"": true,
476-
* ""listEnabled"": true
477-
* }
478-
* }]
479-
*/
480-
List<Hashtable> requiredVersionResponse = new List<Hashtable>();
481-
482-
var packageVersionStr = foundTags["tag"].ToString();
483-
using (JsonDocument pkgVersionEntry = JsonDocument.Parse(packageVersionStr))
430+
GetACRMetadata(registry, packageName, requiredVersion, acrAccessToken, out errRecord)
431+
};
432+
if (errRecord != null)
484433
{
485-
JsonElement rootDom = pkgVersionEntry.RootElement;
486-
if (!rootDom.TryGetProperty("name", out JsonElement pkgVersionElement))
487-
{
488-
errRecord = new ErrorRecord(
489-
new InvalidOrEmptyResponse($"Response does not contain version element ('name') for package '{packageName}' in '{Repository.Name}'."),
490-
"FindNameFailure",
491-
ErrorCategory.InvalidResult,
492-
this);
493-
494-
return new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: acrFindResponseType);
495-
}
496-
497-
if (NuGetVersion.TryParse(pkgVersionElement.ToString(), out NuGetVersion pkgVersion))
498-
{
499-
_cmdletPassedIn.WriteDebug($"'{packageName}' version parsed as '{pkgVersion}'");
500-
501-
if (pkgVersion == requiredVersion)
502-
{
503-
requiredVersionResponse.Add(new Hashtable() { { packageName, packageVersionStr } });
504-
}
505-
}
434+
return new FindResults(stringResponse: new string[] { }, hashtableResponse: results.ToArray(), responseType: acrFindResponseType);
506435
}
507436

508-
return new FindResults(stringResponse: new string[] { }, hashtableResponse: requiredVersionResponse.ToArray(), responseType: acrFindResponseType);
437+
438+
return new FindResults(stringResponse: new string[] { }, hashtableResponse: results.ToArray(), responseType: acrFindResponseType);
509439
}
510440

511441
/// <summary>
@@ -709,6 +639,24 @@ internal async Task<HttpContent> GetAcrBlobAsync(string registry, string reposit
709639

710640
internal JObject FindAcrImageTags(string registry, string repositoryName, string version, string acrAccessToken, out ErrorRecord errRecord)
711641
{
642+
/* response returned looks something like:
643+
* "registry": "myregistry.azurecr.io"
644+
* "imageName": "hello-world"
645+
* "tags": [
646+
* {
647+
* ""name"": ""1.0.0"",
648+
* ""digest"": ""sha256:92c7f9c92844bbbb5d0a101b22f7c2a7949e40f8ea90c8b3bc396879d95e899a"",
649+
* ""createdTime"": ""2023-12-23T18:06:48.9975733Z"",
650+
* ""lastUpdateTime"": ""2023-12-23T18:06:48.9975733Z"",
651+
* ""signed"": false,
652+
* ""changeableAttributes"": {
653+
* ""deleteEnabled"": true,
654+
* ""writeEnabled"": true,
655+
* ""readEnabled"": true,
656+
* ""listEnabled"": true
657+
* }
658+
* }]
659+
*/
712660
try
713661
{
714662
string resolvedVersion = string.Equals(version, "*", StringComparison.OrdinalIgnoreCase) ? null : $"/{version}";
@@ -722,6 +670,141 @@ internal JObject FindAcrImageTags(string registry, string repositoryName, string
722670
}
723671
}
724672

673+
internal Hashtable GetACRMetadata(string registry, string packageName, NuGetVersion requiredVersion, string acrAccessToken, out ErrorRecord errRecord)
674+
{
675+
Hashtable requiredVersionResponse = new Hashtable();
676+
677+
var foundTags = FindAcrManifest(registry, packageName, requiredVersion.ToNormalizedString(), acrAccessToken, out errRecord);
678+
if (errRecord != null || foundTags == null)
679+
{
680+
return requiredVersionResponse;
681+
}
682+
683+
/* Response returned looks something like:
684+
* {
685+
* "schemaVersion": 2,
686+
* "config": {
687+
* "mediaType": "application/vnd.unknown.config.v1+json",
688+
* "digest": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
689+
* "size": 0
690+
* },
691+
* "layers": [
692+
* {
693+
* "mediaType": "application/vnd.oci.image.layer.nondistributable.v1.tar+gzip'",
694+
* "digest": "sha256:7c55c7b66cb075628660d8249cc4866f16e34741c246a42ed97fb23ccd4ea956",
695+
* "size": 3533,
696+
* "annotations": {
697+
* "org.opencontainers.image.title": "test_module.1.0.0.nupkg",
698+
* "metadata": "{\"GUID\":\"45219bf4-10a4-4242-92d6-9bfcf79878fd\",\"FunctionsToExport\":[],\"CompanyName\":\"Anam\",\"CmdletsToExport\":[],\"VariablesToExport\":\"*\",\"Author\":\"Anam Navied\",\"ModuleVersion\":\"1.0.0\",\"Copyright\":\"(c) Anam Navied. All rights reserved.\",\"PrivateData\":{\"PSData\":{\"Tags\":[\"Test\",\"CommandsAndResource\",\"Tag2\"]}},\"RequiredModules\":[],\"Description\":\"This is a test module, for PSGallery team internal testing. Do not take a dependency on this package. This version contains tags for the package.\",\"AliasesToExport\":[]}"
699+
* }
700+
* }
701+
* ]
702+
* }
703+
*/
704+
705+
Tuple<string,string> metadataTuple = GetMetadataProperty(foundTags, packageName, out Exception exception);
706+
if (exception != null)
707+
{
708+
errRecord = new ErrorRecord(exception, "FindNameFailure", ErrorCategory.InvalidResult, this);
709+
710+
return requiredVersionResponse;
711+
}
712+
713+
string metadataPkgName = metadataTuple.Item1;
714+
string metadata = metadataTuple.Item2;
715+
using (JsonDocument metadataJSONDoc = JsonDocument.Parse(metadata))
716+
{
717+
JsonElement rootDom = metadataJSONDoc.RootElement;
718+
if (!rootDom.TryGetProperty("ModuleVersion", out JsonElement pkgVersionElement) &&
719+
!rootDom.TryGetProperty("Version", out pkgVersionElement))
720+
{
721+
errRecord = new ErrorRecord(
722+
new InvalidOrEmptyResponse($"Response does not contain 'ModuleVersion' or 'Version' property in metadata for package '{packageName}' in '{Repository.Name}'."),
723+
"FindNameFailure",
724+
ErrorCategory.InvalidResult,
725+
this);
726+
727+
return requiredVersionResponse;
728+
}
729+
730+
if (NuGetVersion.TryParse(pkgVersionElement.ToString(), out NuGetVersion pkgVersion))
731+
{
732+
_cmdletPassedIn.WriteDebug($"'{packageName}' version parsed as '{pkgVersion}'");
733+
734+
if (pkgVersion == requiredVersion)
735+
{
736+
requiredVersionResponse.Add(metadataPkgName, metadata);
737+
}
738+
}
739+
}
740+
741+
return requiredVersionResponse;
742+
}
743+
744+
internal Tuple<string,string> GetMetadataProperty(JObject foundTags, string packageName, out Exception exception)
745+
{
746+
exception = null;
747+
var emptyTuple = new Tuple<string, string>(string.Empty, string.Empty);
748+
var layers = foundTags["layers"];
749+
if (layers == null || layers[0] == null)
750+
{
751+
exception = new InvalidOrEmptyResponse($"Response does not contain 'layers' element in manifest for package '{packageName}' in '{Repository.Name}'.");
752+
753+
return emptyTuple;
754+
}
755+
756+
var annotations = layers[0]["annotations"];
757+
if (annotations == null)
758+
{
759+
exception = new InvalidOrEmptyResponse($"Response does not contain 'annotations' element in manifest for package '{packageName}' in '{Repository.Name}'.");
760+
761+
return emptyTuple;
762+
}
763+
764+
if (annotations["metadata"] == null)
765+
{
766+
exception = new InvalidOrEmptyResponse($"Response does not contain 'metadata' element in manifest for package '{packageName}' in '{Repository.Name}'.");
767+
768+
return emptyTuple;
769+
}
770+
771+
var metadata = annotations["metadata"].ToString();
772+
773+
var metadataPkgNameJToken = annotations["packageName"];
774+
if (metadataPkgNameJToken == null)
775+
{
776+
exception = new InvalidOrEmptyResponse($"Response does not contain 'packageName' element for package '{packageName}' in '{Repository.Name}'.");
777+
778+
return emptyTuple;
779+
}
780+
781+
string metadataPkgName = metadataPkgNameJToken.ToString();
782+
if (string.IsNullOrWhiteSpace(metadataPkgName))
783+
{
784+
exception = new InvalidOrEmptyResponse($"Response element 'packageName' is empty for package '{packageName}' in '{Repository.Name}'.");
785+
786+
return emptyTuple;
787+
}
788+
789+
return new Tuple<string, string>(metadataPkgName, metadata);
790+
}
791+
792+
internal JObject FindAcrManifest(string registry, string packageName, string version, string acrAccessToken, out ErrorRecord errRecord)
793+
{
794+
try
795+
{
796+
var createManifestUrl = string.Format(acrManifestUrlTemplate, registry, packageName, version);
797+
_cmdletPassedIn.WriteDebug($"GET manifest url: {createManifestUrl}");
798+
799+
var defaultHeaders = GetDefaultHeaders(acrAccessToken);
800+
return GetHttpResponseJObjectUsingDefaultHeaders(createManifestUrl, HttpMethod.Get, defaultHeaders, out errRecord);
801+
}
802+
catch (HttpRequestException e)
803+
{
804+
throw new HttpRequestException("Error finding ACR manifest: " + e.Message);
805+
}
806+
}
807+
725808
internal async Task<string> GetStartUploadBlobLocation(string registry, string pkgName, string acrAccessToken)
726809
{
727810
try
@@ -1113,7 +1196,7 @@ internal bool PushNupkgACR(string psd1OrPs1File, string outputNupkgDir, string p
11131196
FileInfo nupkgFile = new FileInfo(fullNupkgFile);
11141197
var fileSize = nupkgFile.Length;
11151198
var fileName = System.IO.Path.GetFileName(fullNupkgFile);
1116-
string fileContent = CreateJsonContent(nupkgDigest, configDigest, fileSize, fileName, jsonString);
1199+
string fileContent = CreateJsonContent(nupkgDigest, configDigest, fileSize, fileName, pkgName, jsonString);
11171200
File.WriteAllText(configFilePath, fileContent);
11181201

11191202
_cmdletPassedIn.WriteVerbose("Create the manifest layer");
@@ -1130,7 +1213,8 @@ private string CreateJsonContent(
11301213
string nupkgDigest,
11311214
string configDigest,
11321215
long nupkgFileSize,
1133-
string fileName,
1216+
string fileName,
1217+
string packageName,
11341218
string jsonString)
11351219
{
11361220
StringBuilder stringBuilder = new StringBuilder();
@@ -1167,6 +1251,8 @@ private string CreateJsonContent(
11671251
jsonWriter.WritePropertyName("annotations");
11681252
jsonWriter.WriteStartObject();
11691253
jsonWriter.WritePropertyName("org.opencontainers.image.title");
1254+
jsonWriter.WriteValue(packageName);
1255+
jsonWriter.WritePropertyName("org.opencontainers.image.description");
11701256
jsonWriter.WriteValue(fileName);
11711257
jsonWriter.WritePropertyName("metadata");
11721258
jsonWriter.WriteValue(jsonString);

0 commit comments

Comments
 (0)