diff --git a/release.yml b/release.yml index d30eaff2f..35c9f32ed 100644 --- a/release.yml +++ b/release.yml @@ -17,8 +17,8 @@ variables: functionalTests: "**/*FunctionalTests/*.csproj" buildConfiguration: 'Release' major: 5 - minor: 0 - patch: 5 + minor: 1 + patch: 0 buildnum: $[counter(format('{0}.{1}.{2}',variables['major'],variables['minor'], variables['patch']), 1)] version: $(major).$(minor).$(patch).$(buildnum) diff --git a/src/Microsoft.Health.Fhir.TemplateManagement.FunctionalTests/OCIArtifactFunctionalTests.cs b/src/Microsoft.Health.Fhir.TemplateManagement.FunctionalTests/OCIArtifactFunctionalTests.cs index 3abd824ab..a75c3ec22 100644 --- a/src/Microsoft.Health.Fhir.TemplateManagement.FunctionalTests/OCIArtifactFunctionalTests.cs +++ b/src/Microsoft.Health.Fhir.TemplateManagement.FunctionalTests/OCIArtifactFunctionalTests.cs @@ -7,7 +7,6 @@ using System.IO; using System.Linq; using System.Threading.Tasks; -using EnsureThat; using Microsoft.Health.Fhir.TemplateManagement.Client; using Microsoft.Health.Fhir.TemplateManagement.Exceptions; using Microsoft.Health.Fhir.TemplateManagement.Models; @@ -18,7 +17,6 @@ namespace Microsoft.Health.Fhir.TemplateManagement.FunctionalTests { public class OciArtifactFunctionalTests : IAsyncLifetime { - private const string _orasCacheEnvironmentVariableName = "ORAS_CACHE"; private static readonly string _testTarGzPath = Path.Join("TestData", "TarGzFiles"); private readonly string _containerRegistryServer; private readonly string _baseLayerTemplatePath = Path.Join(_testTarGzPath, "layerbase.tar.gz"); @@ -31,8 +29,12 @@ public class OciArtifactFunctionalTests : IAsyncLifetime private readonly string _testMultiLayersWithValidSequenceNumberImageReference; private readonly string _testMultiLayersWithInValidSequenceNumberImageReference; private readonly string _testInvalidCompressedImageReference; + private string _testOneLayerImageDigest; + private string _testMultiLayerImageDigest; private bool _isOrasValid = true; private readonly string _orasErrorMessage = "Oras tool invalid."; + private const string _orasCacheEnvironmentVariableName = "ORAS_CACHE"; + private const string _defaultOrasCacheEnvironmentVariable = ".oras/cache"; public OciArtifactFunctionalTests() { @@ -43,6 +45,11 @@ public OciArtifactFunctionalTests() _testMultiLayersWithValidSequenceNumberImageReference = _containerRegistryServer + "/templatetest:multilayers_valid_sequence"; _testMultiLayersWithInValidSequenceNumberImageReference = _containerRegistryServer + "/templatetest:multilayers_invalid_sequence"; _testInvalidCompressedImageReference = _containerRegistryServer + "/templatetest:invalid_image"; + + if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable(_orasCacheEnvironmentVariableName))) + { + Environment.SetEnvironmentVariable(_orasCacheEnvironmentVariableName, _defaultOrasCacheEnvironmentVariable); + } } public async Task InitializeAsync() @@ -57,14 +64,13 @@ public async Task InitializeAsync() public Task DisposeAsync() { - DirectoryHelper.ClearFolder(Environment.GetEnvironmentVariable(_orasCacheEnvironmentVariableName)); return Task.CompletedTask; } private async Task PushOneLayerWithValidSequenceNumberAsync() { string command = $"push {_testOneLayerWithValidSequenceNumberImageReference} {_baseLayerTemplatePath}"; - await ExecuteOrasCommandAsync(command); + _testOneLayerImageDigest = await ExecuteOrasCommandAsync(command); } private async Task PushOneLayerWithoutSequenceNumberAsync() @@ -83,6 +89,7 @@ private async Task PushMultiLayersWithValidSequenceNumberAsync() { string command = $"push {_testMultiLayersWithValidSequenceNumberImageReference} {_baseLayerTemplatePath} {_userLayerTemplatePath}"; await ExecuteOrasCommandAsync(command); + _testMultiLayerImageDigest = await ExecuteOrasCommandAsync(command); } private async Task PushMultiLayersWithInValidSequenceNumberAsync() @@ -97,18 +104,32 @@ private async Task PushInvalidCompressedImageAsync() await ExecuteOrasCommandAsync(command); } - private async Task ExecuteOrasCommandAsync(string command) + private async Task ExecuteOrasCommandAsync(string command) { try { - await OrasClient.OrasExecutionAsync(command, Directory.GetCurrentDirectory()); + var output = await OrasClient.OrasExecutionAsync(command, Directory.GetCurrentDirectory()); + var digest = GetImageDigest(output); + return digest.Value; } catch { _isOrasValid = false; + return null; } } + private Digest GetImageDigest(string input) + { + var digests = Digest.GetDigest(input); + if (digests.Count == 0) + { + throw new OciClientException(TemplateManagementErrorCode.OrasProcessFailed, "Failed to parse image digest."); + } + + return digests[0]; + } + // Pull one layer image with valid sequence number, successfully pulled with base layer copied. [Fact] public async Task GivenOneLayerImage_WhenPulled_ArtifactsWillBePulledWithBaseLayerCopiedAsync() @@ -143,6 +164,20 @@ public async Task GivenOneLayerImageWithoutSequenceNumber_WhenPulled_ArtifactsWi DirectoryHelper.ClearFolder(outputFolder); } + [Fact] + public async Task GivenOneLayerImage_WhenPulledUsingDigest_ArtifactsWillBePulledWithBaseLayerCopiedAsync() + { + Assert.True(_isOrasValid, _orasErrorMessage); + string outputFolder = "TestData/testOneLayerWithDigest"; + DirectoryHelper.ClearFolder(outputFolder); + + var testManager = new OciFileManager(_containerRegistryServer, outputFolder); + await testManager.PullOciImageAsync("templatetest", _testOneLayerImageDigest, true); + Assert.Equal(843, Directory.EnumerateFiles(outputFolder, "*.*", SearchOption.AllDirectories).Count()); + Assert.Single(Directory.EnumerateFiles(Path.Combine(outputFolder, ".image", "base"), "*.tar.gz", SearchOption.AllDirectories)); + DirectoryHelper.ClearFolder(outputFolder); + } + // Pull one layer image with invalid sequence number, successfully pulled with base layer copied. [Fact] public async Task GivenOneLayerImageWithInvalidSequenceNumber_WhenPulled_ArtifactsWillBePulledWithBaseLayerCopiedAsync() @@ -194,6 +229,20 @@ public async Task GivenMultiLayersImageWithInvalidSequenceNumber_WhenPulled_Arti DirectoryHelper.ClearFolder(outputFolder); } + [Fact] + public async Task GivenMultiLayerImage_WhenPulledUsingDigest_ArtifactsWillBePulledWithBaseLayerCopiedAsync() + { + Assert.True(_isOrasValid, _orasErrorMessage); + string outputFolder = "TestData/testMultiLayerWithDigest"; + DirectoryHelper.ClearFolder(outputFolder); + + var testManager = new OciFileManager(_containerRegistryServer, outputFolder); + await testManager.PullOciImageAsync("templatetest", _testMultiLayerImageDigest, true); + Assert.Equal(10, Directory.EnumerateFiles(outputFolder, "*.*", SearchOption.AllDirectories).Count()); + Assert.Single(Directory.EnumerateFiles(Path.Combine(outputFolder, ".image", "base"), "*.tar.gz", SearchOption.AllDirectories)); + DirectoryHelper.ClearFolder(outputFolder); + } + // Pull invalid image, exception will be thrown. [Fact] public async Task GivenInvalidCompressedImage_WhenPulled_ExceptionWillBeThrownAsync() diff --git a/src/Microsoft.Health.Fhir.TemplateManagement.FunctionalTests/TemplateCollectionFunctionalTests.cs b/src/Microsoft.Health.Fhir.TemplateManagement.FunctionalTests/TemplateCollectionFunctionalTests.cs index c910a0156..6e67778b3 100644 --- a/src/Microsoft.Health.Fhir.TemplateManagement.FunctionalTests/TemplateCollectionFunctionalTests.cs +++ b/src/Microsoft.Health.Fhir.TemplateManagement.FunctionalTests/TemplateCollectionFunctionalTests.cs @@ -18,8 +18,10 @@ using Microsoft.Health.Fhir.Liquid.Converter.Exceptions; using Microsoft.Health.Fhir.Liquid.Converter.Models; using Microsoft.Health.Fhir.Liquid.Converter.Processors; +using Microsoft.Health.Fhir.TemplateManagement.Client; using Microsoft.Health.Fhir.TemplateManagement.Exceptions; using Microsoft.Health.Fhir.TemplateManagement.Models; +using Microsoft.Health.Fhir.TemplateManagement.Utilities; using Newtonsoft.Json.Linq; using Xunit; @@ -40,13 +42,24 @@ public class TemplateCollectionFunctionalTests : IAsyncLifetime private readonly string _defaultStu3ToR4TemplateImageReference = "microsofthealth/stu3tor4templates:default"; private readonly string testOneLayerImageReference; private readonly string testMultiLayerImageReference; + private readonly string testOneLayerOCIImageReference; + private readonly string testMultiLayerOCIImageReference; private readonly string testInvalidImageReference; private readonly string testInvalidTemplateImageReference; + private string testOneLayerImageDigest; + private string testMultiLayerImageDigest; private readonly ContainerRegistry _containerRegistry = new ContainerRegistry(); private readonly ContainerRegistryInfo _containerRegistryInfo; private static readonly string _templateDirectory = Path.Join("..", "..", "data", "Templates"); private static readonly string _sampleDataDirectory = Path.Join("..", "..", "data", "SampleData"); + private static readonly string _testTarGzPath = Path.Join("TestData", "TarGzFiles"); + private readonly string _baseLayerTemplatePath = Path.Join(_testTarGzPath, "layerbase.tar.gz"); + private readonly string _userLayerTemplatePath = Path.Join(_testTarGzPath, "layer2.tar.gz"); private static readonly ProcessorSettings _processorSettings = new ProcessorSettings(); + private bool _isOrasValid = true; + private readonly string _orasErrorMessage = "Oras tool invalid."; + private const string _orasCacheEnvironmentVariableName = "ORAS_CACHE"; + private const string _defaultOrasCacheEnvironmentVariable = ".oras/cache"; public TemplateCollectionFunctionalTests() { @@ -60,7 +73,14 @@ public TemplateCollectionFunctionalTests() testMultiLayerImageReference = _containerRegistryInfo.ContainerRegistryServer + "/templatetest:multilayers"; testInvalidImageReference = _containerRegistryInfo.ContainerRegistryServer + "/templatetest:invalidlayers"; testInvalidTemplateImageReference = _containerRegistryInfo.ContainerRegistryServer + "/templatetest:invalidtemplateslayers"; + testOneLayerOCIImageReference = _containerRegistryInfo.ContainerRegistryServer + "/templatetest:ocionelayer"; + testMultiLayerOCIImageReference = _containerRegistryInfo.ContainerRegistryServer + "/templatetest:ocimultilayer"; token = "Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes($"{_containerRegistryInfo.ContainerRegistryUsername}:{_containerRegistryInfo.ContainerRegistryPassword}")); + + if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable(_orasCacheEnvironmentVariableName))) + { + Environment.SetEnvironmentVariable(_orasCacheEnvironmentVariableName, _defaultOrasCacheEnvironmentVariable); + } } public async Task InitializeAsync() @@ -74,6 +94,10 @@ public async Task InitializeAsync() await InitMultiLayerImageAsync(); await InitInvalidTarGzImageAsync(); await InitInvalidTemplateImageAsync(); + + await OrasLogin(); + await PushOneLayerOCIImageAsync(); + await PushMultiLayersOCIImageAsync(); } public Task DisposeAsync() @@ -87,6 +111,12 @@ public static IEnumerable GetValidImageInfoWithTag() yield return new object[] { new List { 767, 838 }, "templatetest", "multilayers" }; } + public static IEnumerable GetValidOCIImageInfoWithTag() + { + yield return new object[] { new List { 838 }, "templatetest", "ocionelayer" }; + yield return new object[] { new List { 834, 838 }, "templatetest", "ocimultilayer" }; + } + public static IEnumerable GetHl7v2DataAndEntryTemplate() { var data = new List @@ -310,6 +340,50 @@ public async Task GiveImageReference_WhenGetTemplateCollection_ACorrectTemplateC } } + [Theory] + [MemberData(nameof(GetValidOCIImageInfoWithTag))] + public async Task GiveOCIImageReference_WhenGetTemplateCollection_ACorrectTemplateCollectionWillBeReturnedAsync(List expectedTemplatesCounts, string imageName, string tag) + { + if (_containerRegistryInfo == null) + { + return; + } + + Assert.True(_isOrasValid, _orasErrorMessage); + + string imageReference = string.Format("{0}/{1}:{2}", _containerRegistryInfo.ContainerRegistryServer, imageName, tag); + TemplateCollectionProviderFactory factory = new TemplateCollectionProviderFactory(cache, Options.Create(_config)); + var templateCollectionProvider = factory.CreateTemplateCollectionProvider(imageReference, token); + var templateCollection = await templateCollectionProvider.GetTemplateCollectionAsync(); + Assert.Equal(expectedTemplatesCounts.Count(), templateCollection.Count()); + for (var i = 0; i < expectedTemplatesCounts.Count(); i++) + { + Assert.Equal(expectedTemplatesCounts[i], templateCollection[i].Count()); + } + } + + [Fact] + public async Task GiveOCIImageReferenceWithDigest_WhenGetTemplateCollection_ACorrectTemplateCollectionWillBeReturnedAsync() + { + if (_containerRegistryInfo == null) + { + return; + } + + Assert.True(_isOrasValid, _orasErrorMessage); + + string imageReference = string.Format("{0}/{1}@{2}", _containerRegistryInfo.ContainerRegistryServer, "templatetest", testOneLayerImageDigest); + TemplateCollectionProviderFactory factory = new TemplateCollectionProviderFactory(cache, Options.Create(_config)); + var templateCollectionProvider = factory.CreateTemplateCollectionProvider(imageReference, token); + var templateCollection = await templateCollectionProvider.GetTemplateCollectionAsync(); + Assert.Single(templateCollection); + + imageReference = string.Format("{0}/{1}@{2}", _containerRegistryInfo.ContainerRegistryServer, "templatetest", testMultiLayerImageDigest); + templateCollectionProvider = factory.CreateTemplateCollectionProvider(imageReference, token); + templateCollection = await templateCollectionProvider.GetTemplateCollectionAsync(); + Assert.Equal(2, templateCollection.Count()); + } + [Theory] [MemberData(nameof(GetHl7v2DataAndEntryTemplate))] public async Task GetTemplateCollectionFromAcr_WhenGivenHl7v2DataForConverting__ExpectedFhirResourceShouldBeReturnedAsync(string hl7v2Data, string entryTemplate) @@ -486,5 +560,56 @@ private async Task InitInvalidTemplateImageAsync() List templateFiles = new List { invalidTemplatePath }; await _containerRegistry.GenerateTemplateImageAsync(_containerRegistryInfo, testInvalidTemplateImageReference, templateFiles); } + + private async Task PushOneLayerOCIImageAsync() + { + string command = $"push {testOneLayerOCIImageReference} {_baseLayerTemplatePath}"; + testOneLayerImageDigest = await ExecuteOrasCommandAsync(command); + } + + private async Task PushMultiLayersOCIImageAsync() + { + string command = $"push {testMultiLayerOCIImageReference} {_baseLayerTemplatePath} {_userLayerTemplatePath}"; + testMultiLayerImageDigest = await ExecuteOrasCommandAsync(command); + } + + private async Task OrasLogin() + { + try + { + var command = $"login {_containerRegistryInfo.ContainerRegistryServer} -u {_containerRegistryInfo.ContainerRegistryUsername} -p {_containerRegistryInfo.ContainerRegistryPassword}"; + await OrasClient.OrasExecutionAsync(command); + } + catch + { + _isOrasValid = false; + } + } + + private async Task ExecuteOrasCommandAsync(string command) + { + try + { + var output = await OrasClient.OrasExecutionAsync(command, Directory.GetCurrentDirectory()); + var digest = GetImageDigest(output); + return digest.Value; + } + catch + { + _isOrasValid = false; + return null; + } + } + + private Digest GetImageDigest(string input) + { + var digests = Digest.GetDigest(input); + if (digests.Count == 0) + { + throw new OciClientException(TemplateManagementErrorCode.OrasProcessFailed, "Failed to parse image digest."); + } + + return digests[0]; + } } } \ No newline at end of file diff --git a/src/Microsoft.Health.Fhir.TemplateManagement.UnitTests/Client/OrasClientTests.cs b/src/Microsoft.Health.Fhir.TemplateManagement.UnitTests/Client/OrasClientTests.cs index 6e89397ef..fabecf567 100644 --- a/src/Microsoft.Health.Fhir.TemplateManagement.UnitTests/Client/OrasClientTests.cs +++ b/src/Microsoft.Health.Fhir.TemplateManagement.UnitTests/Client/OrasClientTests.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Threading.Tasks; using Microsoft.Health.Fhir.TemplateManagement.Client; using Microsoft.Health.Fhir.TemplateManagement.Exceptions; @@ -19,22 +20,27 @@ public class OrasClientTests : IAsyncLifetime { private readonly string _containerRegistryServer; private readonly string _baseLayerTemplatePath = "TestData/TarGzFiles/baseLayer.tar.gz"; - private readonly string _userLayerTemplatePath = "TestData/TarGzFiles/userV1.tar.gz"; private readonly string _testOneLayerImageReference; - private readonly string _testMultiLayersImageReference; + private readonly string _testMultiLayerImageReference; + private string _testOneLayerImageDigest; + private string _testMultiLayerImageDigest; private bool _isOrasValid = true; + private const string _orasCacheEnvironmentVariableName = "ORAS_CACHE"; public OrasClientTests() { _containerRegistryServer = Environment.GetEnvironmentVariable("TestContainerRegistryServer"); _testOneLayerImageReference = _containerRegistryServer + "/templatetest:v1"; - _testMultiLayersImageReference = _containerRegistryServer + "/templatetest:v2"; + _testMultiLayerImageReference = _containerRegistryServer + "/templatetest:v2"; + + OrasUtility.InitOrasCache(); } public async Task InitializeAsync() { - await PushOneLayerImageAsync(); - await PushMultiLayersImageAsync(); + _testOneLayerImageDigest = await OrasUtility.PushOneLayerImageAsync(_testOneLayerImageReference); + _testMultiLayerImageDigest = await OrasUtility.PushMultiLayersImageAsync(_testMultiLayerImageReference); + _isOrasValid = !(_testOneLayerImageDigest == null || _testMultiLayerImageDigest == null); } public Task DisposeAsync() @@ -200,41 +206,41 @@ public async Task GivenAValidImageReference_WhenPullImageUseOras_ImageWillBePull return; } - DirectoryHelper.ClearFolder("TestData/PullTest"); + var folder = "TestData/PullTest"; + DirectoryHelper.ClearFolder(folder); + // Pull one layer image by tag string imageReference = _testOneLayerImageReference; - OrasClient orasClient = new OrasClient(_containerRegistryServer, "TestData/PullTest"); + OrasClient orasClient = new OrasClient(_containerRegistryServer, folder); var imageInfo = ImageInfo.CreateFromImageReference(imageReference); var ex = await Record.ExceptionAsync(async () => await orasClient.PullImageAsync(imageInfo.ImageName, imageInfo.Tag)); Assert.Null(ex); + Assert.Single(Directory.EnumerateFiles(folder, "*.*", SearchOption.AllDirectories)); - DirectoryHelper.ClearFolder("TestData/PullTest"); - } + DirectoryHelper.ClearFolder(folder); - private async Task PushOneLayerImageAsync() - { - string command = $"push {_testOneLayerImageReference} {_baseLayerTemplatePath}"; - try - { - await OrasClient.OrasExecutionAsync(command, Directory.GetCurrentDirectory()); - } - catch - { - _isOrasValid = false; - } - } + // Pull one layer image by digest + ex = await Record.ExceptionAsync(async () => await orasClient.PullImageAsync(imageInfo.ImageName, _testOneLayerImageDigest)); + Assert.Null(ex); + Assert.Single(Directory.EnumerateFiles(folder, "*.*", SearchOption.AllDirectories)); - private async Task PushMultiLayersImageAsync() - { - string command = $"push {_testMultiLayersImageReference} {_baseLayerTemplatePath} {_userLayerTemplatePath}"; - try - { - await OrasClient.OrasExecutionAsync(command, Directory.GetCurrentDirectory()); - } - catch - { - _isOrasValid = false; - } + DirectoryHelper.ClearFolder(folder); + + // Pull multi layer image by tag + imageReference = _testMultiLayerImageReference; + imageInfo = ImageInfo.CreateFromImageReference(imageReference); + ex = await Record.ExceptionAsync(async () => await orasClient.PullImageAsync(imageInfo.ImageName, imageInfo.Tag)); + Assert.Null(ex); + Assert.Equal(2, Directory.EnumerateFiles(folder, "*.*", SearchOption.AllDirectories).Count()); + + DirectoryHelper.ClearFolder(folder); + + // Pull multi layer image by digest + ex = await Record.ExceptionAsync(async () => await orasClient.PullImageAsync(imageInfo.ImageName, _testMultiLayerImageDigest)); + Assert.Null(ex); + Assert.Equal(2, Directory.EnumerateFiles(folder, "*.*", SearchOption.AllDirectories).Count()); + + DirectoryHelper.ClearFolder(folder); } } } \ No newline at end of file diff --git a/src/Microsoft.Health.Fhir.TemplateManagement.UnitTests/Models/DigestTest.cs b/src/Microsoft.Health.Fhir.TemplateManagement.UnitTests/Models/DigestTest.cs index 01d003ccb..37a67c952 100644 --- a/src/Microsoft.Health.Fhir.TemplateManagement.UnitTests/Models/DigestTest.cs +++ b/src/Microsoft.Health.Fhir.TemplateManagement.UnitTests/Models/DigestTest.cs @@ -48,6 +48,21 @@ public static IEnumerable GetInputStringContainsNoDigests() yield return new object[] { "sha256:d377125165eb6d770f" }; } + public static IEnumerable GetValidDigest() + { + yield return new object[] { "sha256:d377125165eb6d770f344429a7a55379d4028774aebe267fe620cd1fcd2daab7" }; + yield return new object[] { "sha256:123425165eb6d770f344429a7a55379d4028774aebe267fe620cd1fcd2daab7" }; + yield return new object[] { "sha128:123425165eb6d770f344429a7a55379d4028774aebe267fe620cd1fcd2daab7" }; + } + + public static IEnumerable GetInvalidDigest() + { + yield return new object[] { "sha256d377125165eb6d770f344429a7a55379d4028774aebe267fe620cd1fcd2daab7" }; + yield return new object[] { "256:123425165eb6d770f344429a7a55379d4028774aebe267fe620cd1fcd2daab7" }; + yield return new object[] { "sha128:55379d4028774ae:be267fe620cd1fcd2daab7" }; + yield return new object[] { "test" }; + } + [Theory] [MemberData(nameof(GetInputStringContainsDigests))] public void GivenAnInputStringContainsDigests_WhenGetDigest_CorrectDigestsWillBeReturned(string input, List expectedDigests) @@ -70,5 +85,19 @@ public void GivenAnInputStringWithoutDigests_WhenGetDigest_EmptyResultWillBeRetu var results = Digest.GetDigest(input); Assert.Empty(results); } + + [Theory] + [MemberData(nameof(GetValidDigest))] + public void GivenValidDigest_WhenCheckIsDigest_TrueWillBeReturned(string input) + { + Assert.True(Digest.IsDigest(input)); + } + + [Theory] + [MemberData(nameof(GetInvalidDigest))] + public void GivenInvalidDigest_WhenCheckIsDigest_FalseWillBeReturned(string input) + { + Assert.False(Digest.IsDigest(input)); + } } } diff --git a/src/Microsoft.Health.Fhir.TemplateManagement.UnitTests/OCIFileManagerTests.cs b/src/Microsoft.Health.Fhir.TemplateManagement.UnitTests/OCIFileManagerTests.cs index d1cddf990..a610b5c6e 100644 --- a/src/Microsoft.Health.Fhir.TemplateManagement.UnitTests/OCIFileManagerTests.cs +++ b/src/Microsoft.Health.Fhir.TemplateManagement.UnitTests/OCIFileManagerTests.cs @@ -8,7 +8,6 @@ using System.IO; using System.Linq; using System.Threading.Tasks; -using Microsoft.Health.Fhir.TemplateManagement.Client; using Microsoft.Health.Fhir.TemplateManagement.Exceptions; using Microsoft.Health.Fhir.TemplateManagement.Models; using Microsoft.Health.Fhir.TemplateManagement.Utilities; @@ -19,23 +18,27 @@ namespace Microsoft.Health.Fhir.TemplateManagement.UnitTests public class OciFileManagerTests : IAsyncLifetime { private readonly string _containerRegistryServer; - private readonly string _baseLayerTemplatePath = "TestData/TarGzFiles/layer1.tar.gz"; - private readonly string _userLayerTemplatePath = "TestData/TarGzFiles/layer2.tar.gz"; private readonly string _testOneLayerImageReference; private readonly string _testMultiLayersImageReference; + private string _testOneLayerImageDigest; + private string _testMultiLayerImageDigest; private bool _isOrasValid = true; + private const string _orasCacheEnvironmentVariableName = "ORAS_CACHE"; public OciFileManagerTests() { _containerRegistryServer = Environment.GetEnvironmentVariable("TestContainerRegistryServer"); _testOneLayerImageReference = _containerRegistryServer + "/templatetest:user1"; _testMultiLayersImageReference = _containerRegistryServer + "/templatetest:user2"; + + OrasUtility.InitOrasCache(); } public async Task InitializeAsync() { - await PushOneLayerImageAsync(); - await PushMultiLayersImageAsync(); + _testOneLayerImageDigest = await OrasUtility.PushOneLayerImageAsync(_testOneLayerImageReference); + _testMultiLayerImageDigest = await OrasUtility.PushMultiLayersImageAsync(_testMultiLayersImageReference); + _isOrasValid = !(_testOneLayerImageDigest == null || _testMultiLayerImageDigest == null); } public Task DisposeAsync() @@ -109,6 +112,10 @@ public async Task GivenValidOutputFolder_WhenPullOciFiles_CorrectFilesWillBePull await testManager.PullOciImageAsync(imageInfo.ImageName, imageInfo.Tag, true); Assert.Equal(843, Directory.EnumerateFiles(outputFolder, "*.*", SearchOption.AllDirectories).Count()); DirectoryHelper.ClearFolder(outputFolder); + + await testManager.PullOciImageAsync(imageInfo.ImageName, _testOneLayerImageDigest, true); + Assert.Equal(843, Directory.EnumerateFiles(outputFolder, "*.*", SearchOption.AllDirectories).Count()); + DirectoryHelper.ClearFolder(outputFolder); } [Fact] @@ -126,6 +133,10 @@ public async Task GivenAnImageReferenceAndOutputFolder_WhenPullOciFiles_CorrectF await testManager.PullOciImageAsync(imageInfo.ImageName, imageInfo.Tag, true); Assert.Equal(10, Directory.EnumerateFiles(outputFolder, "*.*", SearchOption.AllDirectories).Count()); DirectoryHelper.ClearFolder(outputFolder); + + await testManager.PullOciImageAsync(imageInfo.ImageName, _testMultiLayerImageDigest, true); + Assert.Equal(10, Directory.EnumerateFiles(outputFolder, "*.*", SearchOption.AllDirectories).Count()); + DirectoryHelper.ClearFolder(outputFolder); } [Fact] @@ -143,31 +154,5 @@ public async Task GivenAnImageReferenceAndInputFolder_WhenPushOciFiles_CorrectIm var ex = await Record.ExceptionAsync(async () => await testManager.PushOciImageAsync(imageInfo.ImageName, imageInfo.Tag, true)); Assert.Null(ex); } - - private async Task PushOneLayerImageAsync() - { - string command = $"push {_testOneLayerImageReference} {_baseLayerTemplatePath}"; - try - { - await OrasClient.OrasExecutionAsync(command, Directory.GetCurrentDirectory()); - } - catch - { - _isOrasValid = false; - } - } - - private async Task PushMultiLayersImageAsync() - { - string command = $"push {_testMultiLayersImageReference} {_baseLayerTemplatePath} {_userLayerTemplatePath}"; - try - { - await OrasClient.OrasExecutionAsync(command, Directory.GetCurrentDirectory()); - } - catch - { - _isOrasValid = false; - } - } } } diff --git a/src/Microsoft.Health.Fhir.TemplateManagement.UnitTests/OrasUtility.cs b/src/Microsoft.Health.Fhir.TemplateManagement.UnitTests/OrasUtility.cs new file mode 100644 index 000000000..ee7d59eae --- /dev/null +++ b/src/Microsoft.Health.Fhir.TemplateManagement.UnitTests/OrasUtility.cs @@ -0,0 +1,71 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +// ------------------------------------------------------------------------------------------------- + +using System; +using System.IO; +using System.Threading.Tasks; +using Microsoft.Health.Fhir.TemplateManagement.Client; +using Microsoft.Health.Fhir.TemplateManagement.Exceptions; +using Microsoft.Health.Fhir.TemplateManagement.Models; + +namespace Microsoft.Health.Fhir.TemplateManagement.UnitTests +{ + public static class OrasUtility + { + private static readonly string _baseLayerTemplatePath = "TestData/TarGzFiles/layer1.tar.gz"; + private static readonly string _userLayerTemplatePath = "TestData/TarGzFiles/layer2.tar.gz"; + private const string _orasCacheEnvironmentVariableName = "ORAS_CACHE"; + private const string _defaultOrasCacheEnvironmentVariable = ".oras/cache"; + + public static void InitOrasCache() + { + if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable(_orasCacheEnvironmentVariableName))) + { + Environment.SetEnvironmentVariable(_orasCacheEnvironmentVariableName, _defaultOrasCacheEnvironmentVariable); + } + } + + public static async Task PushOneLayerImageAsync(string testOneLayerImageReference) + { + string command = $"push {testOneLayerImageReference} {_baseLayerTemplatePath}"; + try + { + var output = await OrasClient.OrasExecutionAsync(command, Directory.GetCurrentDirectory()); + var digest = GetImageDigest(output); + return digest.Value; + } + catch + { + return null; + } + } + + public static async Task PushMultiLayersImageAsync(string testMultiLayersImageReference) + { + string command = $"push {testMultiLayersImageReference} {_baseLayerTemplatePath} {_userLayerTemplatePath}"; + try + { + var output = await OrasClient.OrasExecutionAsync(command, Directory.GetCurrentDirectory()); + var digest = GetImageDigest(output); + return digest.Value; + } + catch + { + return null; + } + } + + private static Digest GetImageDigest(string input) + { + var digests = Digest.GetDigest(input); + if (digests.Count == 0) + { + throw new OciClientException(TemplateManagementErrorCode.OrasProcessFailed, "Failed to parse image digest."); + } + + return digests[0]; + } + } +} diff --git a/src/Microsoft.Health.Fhir.TemplateManagement/Client/ACRClient.cs b/src/Microsoft.Health.Fhir.TemplateManagement/Client/ACRClient.cs index 70e93b9f9..c4811e118 100644 --- a/src/Microsoft.Health.Fhir.TemplateManagement/Client/ACRClient.cs +++ b/src/Microsoft.Health.Fhir.TemplateManagement/Client/ACRClient.cs @@ -4,6 +4,7 @@ // ------------------------------------------------------------------------------------------------- using System; +using System.Collections.Generic; using System.IO; using System.Net; using System.Threading; @@ -23,6 +24,14 @@ public class AcrClient : IOciClient { private readonly IAzureContainerRegistryClient _client; + // Accept media type for manifest. + private readonly List _acceptedManifestMediatype = + new List() + { + Constants.V2MediaTypeManifest, + Constants.OCIMediaTypeImageManifest, + }; + public AcrClient(string registry, string token) { EnsureArg.IsNotNull(registry, nameof(registry)); @@ -76,7 +85,7 @@ public async Task GetManifestAsync(string imageName, string ref try { cancellationToken.ThrowIfCancellationRequested(); - var manifestInfo = await _client.Manifests.GetAsync(imageName, reference, Constants.MediatypeV2Manifest, cancellationToken); + var manifestInfo = await _client.Manifests.GetAsync(imageName, reference, string.Join(",", _acceptedManifestMediatype), cancellationToken); return manifestInfo; } catch (TemplateManagementException) diff --git a/src/Microsoft.Health.Fhir.TemplateManagement/Client/OrasClient.cs b/src/Microsoft.Health.Fhir.TemplateManagement/Client/OrasClient.cs index 91eb06fe1..d9c1a73c4 100644 --- a/src/Microsoft.Health.Fhir.TemplateManagement/Client/OrasClient.cs +++ b/src/Microsoft.Health.Fhir.TemplateManagement/Client/OrasClient.cs @@ -42,7 +42,16 @@ public async Task PullImageAsync(string name, string reference, C DirectoryHelper.ClearFolder(_imageFolder); - string imageReference = string.Format("{0}/{1}:{2}", _registry, name, reference); + string imageReference; + if (Digest.IsDigest(reference)) + { + imageReference = string.Format("{0}/{1}@{2}", _registry, name, reference); + } + else + { + imageReference = string.Format("{0}/{1}:{2}", _registry, name, reference); + } + string command = $"pull \"{imageReference}\" -o \"{_imageFolder}\""; string output = await OrasExecutionAsync(command, null); diff --git a/src/Microsoft.Health.Fhir.TemplateManagement/Constants.cs b/src/Microsoft.Health.Fhir.TemplateManagement/Constants.cs index 5f5cb73c1..c70e77e0a 100644 --- a/src/Microsoft.Health.Fhir.TemplateManagement/Constants.cs +++ b/src/Microsoft.Health.Fhir.TemplateManagement/Constants.cs @@ -7,8 +7,11 @@ namespace Microsoft.Health.Fhir.TemplateManagement { internal static class Constants { - // Accept media type for manifest. - internal const string MediatypeV2Manifest = "application/vnd.docker.distribution.manifest.v2+json"; + // Accepted media type for manifest. https://github.com/distribution/distribution/blob/main/docs/spec/manifest-v2-2.md + internal const string V2MediaTypeManifest = "application/vnd.docker.distribution.manifest.v2+json"; + + // Accepted media type for OCI manifest https://github.com/opencontainers/image-spec/blob/main/manifest.md#image-manifest + internal const string OCIMediaTypeImageManifest = "application/vnd.oci.image.manifest.v1+json"; internal const string ImageReferenceFormat = "{0}/{1}:{2}"; diff --git a/src/Microsoft.Health.Fhir.TemplateManagement/Microsoft.Health.Fhir.TemplateManagement.csproj b/src/Microsoft.Health.Fhir.TemplateManagement/Microsoft.Health.Fhir.TemplateManagement.csproj index 63925939b..3c191d994 100644 --- a/src/Microsoft.Health.Fhir.TemplateManagement/Microsoft.Health.Fhir.TemplateManagement.csproj +++ b/src/Microsoft.Health.Fhir.TemplateManagement/Microsoft.Health.Fhir.TemplateManagement.csproj @@ -17,6 +17,7 @@ $(SolutionFolder)data\ $(SolutionFolder)bin\ copy + mkdir @@ -24,6 +25,7 @@ $(SolutionFolder)data/ $(SolutionFolder)bin/ cp + mkdir -p @@ -76,7 +78,7 @@ - + @@ -86,7 +88,7 @@ - + diff --git a/src/Microsoft.Health.Fhir.TemplateManagement/Models/Digest.cs b/src/Microsoft.Health.Fhir.TemplateManagement/Models/Digest.cs index e86e426a0..696505ed0 100644 --- a/src/Microsoft.Health.Fhir.TemplateManagement/Models/Digest.cs +++ b/src/Microsoft.Health.Fhir.TemplateManagement/Models/Digest.cs @@ -33,5 +33,10 @@ public static List GetDigest(string input) return digests; } + + public static bool IsDigest(string input) + { + return _digestRegex.IsMatch(input); + } } }