From 862836d3a32cba5e3fb02f10f8fb61d209998af7 Mon Sep 17 00:00:00 2001 From: Marc Paine Date: Wed, 15 Apr 2026 16:30:57 -0700 Subject: [PATCH 1/3] Fix launch settings stderr test assertions and container digest validation - Update test assertions in GivenDotnetRunBuildsCsProj.cs, GivenDotnetRunBuildsVbProj.cs, and RunCommandTests.cs to expect the 'Using launch settings from' message on stderr instead of stdout, matching the production change in commit 1ebcc5ee011. - Fix VB test to use 'My Project' path instead of 'Properties' for launchSettings.json, matching VB project conventions. - Add strict sha256 digest validation in ContentStore.PathForDescriptor using an anchored regex requiring exactly 64 hex characters, replacing the permissive DigestRegexp that accepted 32+ characters unanchored. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ContentStore.cs | 5 ++++- .../Run/GivenDotnetRunBuildsCsProj.cs | 18 ++++++++++-------- .../Run/GivenDotnetRunBuildsVbProj.cs | 12 +++++------- .../CommandTests/Run/RunCommandTests.cs | 2 +- 4 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/Containers/Microsoft.NET.Build.Containers/ContentStore.cs b/src/Containers/Microsoft.NET.Build.Containers/ContentStore.cs index f0f78a471062..ebd1e3984d8b 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/ContentStore.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/ContentStore.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Text.RegularExpressions; using Microsoft.NET.Build.Containers.Resources; namespace Microsoft.NET.Build.Containers; @@ -32,10 +33,12 @@ public static string TempPath } } + private static readonly Regex s_sha256DigestRegex = new(@"^sha256:[0-9A-Fa-f]{64}$", RegexOptions.Compiled); + public static string PathForDescriptor(Descriptor descriptor) { string digestString = descriptor.Digest; - if (!ReferenceParser.DigestRegexp.IsMatch(digestString)) + if (!s_sha256DigestRegex.IsMatch(digestString)) { throw new ArgumentException($"Invalid digest: {digestString}", nameof(descriptor.Digest)); } diff --git a/test/dotnet.Tests/CommandTests/Run/GivenDotnetRunBuildsCsProj.cs b/test/dotnet.Tests/CommandTests/Run/GivenDotnetRunBuildsCsProj.cs index c3d1ff0781da..166c994df7b8 100644 --- a/test/dotnet.Tests/CommandTests/Run/GivenDotnetRunBuildsCsProj.cs +++ b/test/dotnet.Tests/CommandTests/Run/GivenDotnetRunBuildsCsProj.cs @@ -400,7 +400,9 @@ public void ItUsesLaunchProfileOfTheSpecifiedName() cmd.Should().Pass() .And.HaveStdOutContaining("Second"); - cmd.StdErr.Should().BeEmpty(); + cmd.StdErr.Should().Contain( + string.Format(CliCommandStrings.UsingLaunchSettingsFromMessage, + Path.Combine(testProjectDirectory, "Properties", "launchSettings.json"))); } [Fact] @@ -441,7 +443,8 @@ public void ItSetsTheDotnetLaunchProfileEnvironmentVariableToDefaultLaunchProfil cmd.Should().Pass() .And.HaveStdOutContaining("DOTNET_LAUNCH_PROFILE=<<>>"); - cmd.StdErr.Should().BeEmpty(); + cmd.StdErr.Should().Contain( + string.Format(CliCommandStrings.UsingLaunchSettingsFromMessage, launchSettingsPath)); } [Fact] @@ -461,7 +464,8 @@ public void ItSetsTheDotnetLaunchProfileEnvironmentVariableToSuppliedLaunchProfi cmd.Should().Pass() .And.HaveStdOutContaining("DOTNET_LAUNCH_PROFILE=<<>>"); - cmd.StdErr.Should().BeEmpty(); + cmd.StdErr.Should().Contain( + string.Format(CliCommandStrings.UsingLaunchSettingsFromMessage, launchSettingsPath)); } [Fact] @@ -518,10 +522,8 @@ public void ItPrintsUsingLaunchSettingsMessageWhenNotQuiet() .Execute("-v:m"); cmd.Should().Pass() - .And.HaveStdOutContaining(string.Format(CliCommandStrings.UsingLaunchSettingsFromMessage, launchSettingsPath)) + .And.HaveStdErrContaining(string.Format(CliCommandStrings.UsingLaunchSettingsFromMessage, launchSettingsPath)) .And.HaveStdOutContaining("First"); - - cmd.StdErr.Should().BeEmpty(); } [Fact] @@ -540,7 +542,7 @@ public void ItPrefersTheValueOfAppUrlFromEnvVarOverTheProp() cmd.Should().Pass() .And.HaveStdOutContaining("http://localhost:12345/"); - cmd.StdErr.Should().BeEmpty(); + cmd.StdErr.Should().Contain("Using launch settings from"); } [Fact] @@ -559,7 +561,7 @@ public void ItUsesTheValueOfAppUrlIfTheEnvVarIsNotSet() cmd.Should().Pass() .And.HaveStdOutContaining("http://localhost:54321/"); - cmd.StdErr.Should().BeEmpty(); + cmd.StdErr.Should().Contain("Using launch settings from"); } [Fact] diff --git a/test/dotnet.Tests/CommandTests/Run/GivenDotnetRunBuildsVbProj.cs b/test/dotnet.Tests/CommandTests/Run/GivenDotnetRunBuildsVbProj.cs index a73c7efa3f94..68e87ade0cc3 100644 --- a/test/dotnet.Tests/CommandTests/Run/GivenDotnetRunBuildsVbProj.cs +++ b/test/dotnet.Tests/CommandTests/Run/GivenDotnetRunBuildsVbProj.cs @@ -88,7 +88,7 @@ public void ItUsesLaunchProfileOfTheSpecifiedName(string launchProfileName) .And .HaveStdOutContaining("Second") .And - .NotHaveStdErr(); + .HaveStdErrContaining("Using launch settings from"); } [Fact] @@ -99,17 +99,17 @@ public void ItDefaultsToTheFirstUsableLaunchProfile() .WithSource(); var testProjectDirectory = testInstance.Path; - var launchSettingsPath = Path.Combine(testProjectDirectory, "Properties", "launchSettings.json"); + var launchSettingsPath = Path.Combine(testProjectDirectory, "My Project", "launchSettings.json"); var cmd = new DotnetCommand(Log, "run") .WithWorkingDirectory(testProjectDirectory) .Execute(); cmd.Should().Pass() - .And.NotHaveStdOutContaining(string.Format(CliCommandStrings.UsingLaunchSettingsFromMessage, launchSettingsPath)) .And.HaveStdOutContaining("First"); - cmd.StdErr.Should().BeEmpty(); + cmd.StdErr.Should().Contain( + string.Format(CliCommandStrings.UsingLaunchSettingsFromMessage, launchSettingsPath)); } [Fact] @@ -126,10 +126,8 @@ public void ItPrintsUsingLaunchSettingsMessageWhenNotQuiet() .Execute("-v:m"); cmd.Should().Pass() - .And.HaveStdOutContaining(string.Format(CliCommandStrings.UsingLaunchSettingsFromMessage, launchSettingsPath)) + .And.HaveStdErrContaining(string.Format(CliCommandStrings.UsingLaunchSettingsFromMessage, launchSettingsPath)) .And.HaveStdOutContaining("First"); - - cmd.StdErr.Should().BeEmpty(); } [Fact] diff --git a/test/dotnet.Tests/CommandTests/Run/RunCommandTests.cs b/test/dotnet.Tests/CommandTests/Run/RunCommandTests.cs index e67a05e4678b..79ff2d0e5cbf 100644 --- a/test/dotnet.Tests/CommandTests/Run/RunCommandTests.cs +++ b/test/dotnet.Tests/CommandTests/Run/RunCommandTests.cs @@ -68,7 +68,7 @@ public void EnvironmentVariableExpansion_Project() .And.HaveStdOutContaining("TEST_VAR1=<<>>") .And.HaveStdOutContaining("ARGS=arg1,arg2,arg3"); - cmd.StdErr.Should().BeEmpty(); + cmd.StdErr.Should().Contain("Using launch settings from"); } [Fact] From a2a01660cad071645b1b18ddf910ef3d105b0e4e Mon Sep 17 00:00:00 2001 From: Marc Paine Date: Wed, 15 Apr 2026 16:56:04 -0700 Subject: [PATCH 2/3] Fix PostAction path resolution, CscOnly test, add regression test - Fix DotnetAddPostActionProcessor.ProcessInternal: when targetFiles is specified and GetConfiguredFiles returns empty, try FindExistingTargetFiles before TryFindProjects to avoid resolving stale parent .csproj files - Remove -bl flag from CscOnly_CompilationDiagnostics test that caused false failures on Linux CI when NuGet cache files are missing - Add regression test for PostAction fix with conflicting parent project Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../DotnetAddPostActionProcessor.cs | 21 +++++--- .../New/DotnetAddPostActionTests.cs | 53 +++++++++++++++++++ .../Run/RunFileTests_CscOnlyAndApi.cs | 6 +-- 3 files changed, 69 insertions(+), 11 deletions(-) diff --git a/src/Cli/dotnet/Commands/New/PostActions/DotnetAddPostActionProcessor.cs b/src/Cli/dotnet/Commands/New/PostActions/DotnetAddPostActionProcessor.cs index bcb0cba14855..b28c6f9c541c 100644 --- a/src/Cli/dotnet/Commands/New/PostActions/DotnetAddPostActionProcessor.cs +++ b/src/Cli/dotnet/Commands/New/PostActions/DotnetAddPostActionProcessor.cs @@ -34,11 +34,23 @@ internal static IReadOnlyList FindProjFileAtOrAbovePath(IPhysicalFileSys protected override bool ProcessInternal(IEngineEnvironmentSettings environment, IPostAction action, ICreationEffects creationEffects, ICreationResult templateCreationResult, string outputBasePath) { + bool hasConfiguredTargetFiles = + action.Args.TryGetValue("targetFiles", out string? targetFilesArg) && + !string.IsNullOrWhiteSpace(targetFilesArg); + IReadOnlyList? projectsToProcess = GetConfiguredFiles(action.Args, creationEffects, "targetFiles", outputBasePath); - if (!projectsToProcess.Any()) + if (!projectsToProcess.Any() && hasConfiguredTargetFiles) + { + // targetFiles was specified but none matched in creationEffects (e.g., pre-existing project files). + // Try to resolve them directly from disk before falling back to directory search. + projectsToProcess = FindExistingTargetFiles(environment.Host.FileSystem, action.Args, outputBasePath); + } + + if (!projectsToProcess.Any() && !hasConfiguredTargetFiles) { - //If the author didn't opt in to the new behavior by specifying "targetFiles", search for project file in current output directory or above. + // The author didn't opt in to the new behavior by specifying "targetFiles", + // search for project file in current output directory or above. HashSet extensionLimiters = new(StringComparer.Ordinal); if (action.Args.TryGetValue("projectFileExtensions", out string? projectFileExtensions)) { @@ -65,11 +77,6 @@ protected override bool ProcessInternal(IEngineEnvironmentSettings environment, } } - if (!projectsToProcess.Any()) - { - projectsToProcess = FindExistingTargetFiles(environment.Host.FileSystem, action.Args, outputBasePath); - } - if (!projectsToProcess.Any()) { // no projects found. Error. diff --git a/test/dotnet.Tests/CommandTests/New/DotnetAddPostActionTests.cs b/test/dotnet.Tests/CommandTests/New/DotnetAddPostActionTests.cs index 907da662a0f6..05d53ebca2f9 100644 --- a/test/dotnet.Tests/CommandTests/New/DotnetAddPostActionTests.cs +++ b/test/dotnet.Tests/CommandTests/New/DotnetAddPostActionTests.cs @@ -247,6 +247,59 @@ public void AddRefCanHandleExistingProjectFiles() Assert.Equal(referencedProjectFileFullPath, callback.Reference); } + [Fact(DisplayName = nameof(AddRefWithTargetFilesIgnoresParentDirectoryProjects))] + public void AddRefWithTargetFilesIgnoresParentDirectoryProjects() + { + // Regression test: when targetFiles is specified pointing to a pre-existing project, + // the processor should find it on disk and NOT walk up the directory tree to find + // a different .csproj in a parent directory. + var callback = new MockAddProjectReferenceCallback(); + DotnetAddPostActionProcessor actionProcessor = new(callback.AddPackageReference, callback.AddProjectReference); + + string targetBasePath = _engineEnvironmentSettings.GetTempVirtualizedPath(); + _engineEnvironmentSettings.Host.VirtualizeDirectory(targetBasePath); + + // Create existing target project in a subdirectory (the correct target) + const string existingProjectFolder = "ExistingProject"; + string existingProjectPath = Path.Combine(targetBasePath, existingProjectFolder); + const string existingProjectFile = "ExistingProject.csproj"; + string existingProjectFileFullPath = Path.Combine(existingProjectPath, existingProjectFile); + _engineEnvironmentSettings.Host.FileSystem.WriteAllText(existingProjectFileFullPath, TestCsprojFile); + + // Create a conflicting .csproj in the output directory itself (simulates stale file pollution) + string conflictingProjFile = Path.Combine(targetBasePath, "Conflicting.csproj"); + _engineEnvironmentSettings.Host.FileSystem.WriteAllText(conflictingProjFile, TestCsprojFile); + + string referencedProjectFileFullPath = Path.Combine(targetBasePath, "Reference.csproj"); + + var args = new Dictionary() + { + { "targetFiles", $"[\"{existingProjectFolder}/{existingProjectFile}\"]" }, + { "referenceType", "project" }, + { "reference", "Reference.csproj" } + }; + var postAction = + new MockPostAction(default, default, default, default, default!) + { + ActionId = DotnetAddPostActionProcessor.ActionProcessorId, Args = args + }; + + // Empty creation effects — the existing project was NOT created by the template + MockCreationEffects creationEffects = new MockCreationEffects(); + + actionProcessor.Process( + _engineEnvironmentSettings, + postAction, + creationEffects, + new MockCreationResult(), + targetBasePath); + + // The callback should receive the explicitly configured target file, + // NOT the conflicting .csproj from the parent/output directory + Assert.Equal(existingProjectFileFullPath, callback.Target); + Assert.Equal(referencedProjectFileFullPath, callback.Reference); + } + [Fact(DisplayName = nameof(AddRefCanTargetASingleProjectWithAJsonArray))] public void AddRefCanTargetASingleProjectWithAJsonArray() { diff --git a/test/dotnet.Tests/CommandTests/Run/RunFileTests_CscOnlyAndApi.cs b/test/dotnet.Tests/CommandTests/Run/RunFileTests_CscOnlyAndApi.cs index ce7558ef2ef0..fa90d64a4063 100644 --- a/test/dotnet.Tests/CommandTests/Run/RunFileTests_CscOnlyAndApi.cs +++ b/test/dotnet.Tests/CommandTests/Run/RunFileTests_CscOnlyAndApi.cs @@ -402,11 +402,10 @@ public void CscOnly_CompilationDiagnostics() Console.WriteLine("ran" + x); """); - new DotnetCommand(Log, "run", "Program.cs", "-bl") + new DotnetCommand(Log, "run", "Program.cs") .WithWorkingDirectory(testInstance.Path) .Execute() .Should().Pass() - .And.HaveStdOutContaining(CliCommandStrings.NoBinaryLogBecauseRunningJustCsc) // warning CS8600: Converting null literal or possible null value to non-nullable type. .And.HaveStdOutContaining("warning CS8600") .And.HaveStdOutContaining("ran"); @@ -415,11 +414,10 @@ public void CscOnly_CompilationDiagnostics() Console.Write """); - new DotnetCommand(Log, "run", "Program.cs", "-bl") + new DotnetCommand(Log, "run", "Program.cs") .WithWorkingDirectory(testInstance.Path) .Execute() .Should().Fail() - .And.HaveStdOutContaining(CliCommandStrings.NoBinaryLogBecauseRunningJustCsc) // error CS1002: ; expected .And.HaveStdOutContaining("error CS1002") .And.HaveStdErrContaining(CliCommandStrings.RunCommandException); From 47f5808284b17c3c401bbbd989806df22b3219ae Mon Sep 17 00:00:00 2001 From: Marc Paine Date: Fri, 17 Apr 2026 13:14:48 -0700 Subject: [PATCH 3/3] Revert container changes from branch (merged separately in #53933) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- containers.slnf | 12 +- .../AmazonECRMessageHandler.cs | 2 +- .../AuthHandshakeMessageHandler.cs | 371 +++--------- .../AuthHeaderCache.cs | 60 ++ .../BaseImageNotFoundException.cs | 8 +- .../BuiltImage.cs | 34 +- .../Constants.cs | 3 +- .../ContainerBuilder.cs | 255 ++------ .../ContainerHelpers.cs | 123 ++-- .../ContainerHttpException.cs | 17 + .../ContentStore.cs | 33 +- .../CredentialRetrievalException.cs | 6 +- .../Descriptor.cs | 4 +- .../DiagnosticMessage.cs | 50 ++ .../DigestUtils.cs | 13 +- .../Microsoft.NET.Build.Containers/Globals.cs | 2 + .../ImageBuilder.cs | 260 +-------- .../ImageConfig.cs | 24 +- .../ImageReference.cs | 23 + .../KnownAppCommandInstructions.cs | 2 +- .../KnownStrings.cs | 24 - .../Microsoft.NET.Build.Containers/Layer.cs | 63 +- .../LocalDaemons/DockerCli.cs | 546 +++--------------- .../LocalDaemons/ILocalRegistry.cs | 10 +- .../LocalDaemons/KnownLocalRegistryTypes.cs | 13 +- .../Logging/MSBuildLogger.cs | 62 ++ .../Logging/MSBuildLoggerProvider.cs | 29 + .../ManifestListV2.cs | 3 - .../ManifestV2.cs | 11 +- .../Microsoft.NET.Build.Containers.csproj | 73 +-- .../PublicAPI/net472/PublicAPI.Unshipped.txt | 70 +-- .../PublicAPI/net8.0/PublicAPI.Shipped.txt | 1 + .../PublicAPI/net8.0/PublicAPI.Unshipped.txt | 195 +++++++ .../ReferenceParser.cs | 1 + .../Registry/DefaultBlobOperations.cs | 34 +- .../Registry/DefaultBlobUploadOperations.cs | 11 +- .../Registry/DefaultManifestOperations.cs | 37 +- .../Registry/DefaultRegistryAPI.cs | 78 +-- .../Registry/FinalizeUploadInformation.cs | 5 +- .../Registry/HttpExtensions.cs | 44 +- .../Registry/IBlobOperations.cs | 5 +- .../Registry/IBlobUploadOperations.cs | 5 +- .../Registry/IManifestOperations.cs | 7 +- .../Registry/IRegistryAPI.cs | 5 +- .../Registry/NextChunkUploadInformation.cs | 5 +- .../Registry/Registry.cs | 424 +++----------- .../Registry/RegistrySettings.cs | 69 +-- .../Registry/SchemaTypes.cs | 11 +- .../Registry/StartUploadInformation.cs | 5 +- .../Resources/Resource.cs | 2 +- .../Resources/Strings.Designer.cs | 526 +++++------------ .../Resources/Strings.resx | 310 ++++------ .../Resources/xlf/Strings.cs.xlf | 280 +++------ .../Resources/xlf/Strings.de.xlf | 280 +++------ .../Resources/xlf/Strings.es.xlf | 280 +++------ .../Resources/xlf/Strings.fr.xlf | 280 +++------ .../Resources/xlf/Strings.it.xlf | 280 +++------ .../Resources/xlf/Strings.ja.xlf | 280 +++------ .../Resources/xlf/Strings.ko.xlf | 280 +++------ .../Resources/xlf/Strings.pl.xlf | 280 +++------ .../Resources/xlf/Strings.pt-BR.xlf | 280 +++------ .../Resources/xlf/Strings.ru.xlf | 280 +++------ .../Resources/xlf/Strings.tr.xlf | 280 +++------ .../Resources/xlf/Strings.zh-Hans.xlf | 280 +++------ .../Resources/xlf/Strings.zh-Hant.xlf | 280 +++------ .../Tasks/ComputeDotnetBaseImageTag.cs | 113 ++++ .../Tasks/CreateNewImage.Interface.cs | 65 +-- .../Tasks/CreateNewImage.cs | 341 ++++++----- .../Tasks/CreateNewImageToolTask.cs | 85 +-- .../Tasks/ParseContainerProperties.cs | 56 +- .../VSHostObject.cs | 114 +--- .../net472Definitions.cs | 28 + .../containerize/ContainerizeCommand.cs | 165 ++---- src/Containers/containerize/Program.cs | 3 + .../containerize/containerize.csproj | 22 +- src/Containers/docs/ReleaseNotes/v7.0.400.md | 6 +- .../Microsoft.NET.Build.Containers.props | 7 +- .../Microsoft.NET.Build.Containers.targets | 326 ++--------- src/Containers/packaging/package.csproj | 60 +- .../CapturingLogger.cs | 35 ++ .../ContainerCli.cs | 57 ++ .../CreateNewImageTests.cs | 308 ++++++++++ .../CurrentFile.cs | 15 + .../DockerRegistryManager.cs | 66 +++ .../DockerRegistryTests.cs | 40 ++ .../DockerSupportsArchInlineData.cs | 104 ++++ .../DockerTestsCollection.cs | 16 + .../DockerTestsFixture.cs | 38 ++ .../EndToEndTests.cs | 459 +++++++++++++++ .../CreateNewImageToolTaskTests.cs | 473 +++++++++++++++ .../LayerEndToEndTests.cs | 128 ++++ ...T.Build.Containers.IntegrationTests.csproj | 48 ++ .../PackageTests.cs | 168 ++++++ .../ParseContainerPropertiesTests.cs | 103 ++++ .../ProjectInitializer.cs | 75 +++ .../RegistryTests.cs | 59 ++ .../TargetsTests.cs | 229 ++++++++ .../TestSettings.cs | 39 ++ .../ToolsetUtils.cs | 26 + .../TransientTestFolder.cs | 25 + .../AuthHeaderCacheTests.cs | 24 + .../ContainerHelpersTests.cs | 122 ++++ .../CreateNewImageTests.cs | 99 ++++ .../DescriptorTests.cs | 29 + .../DiagnosticMessageTests.cs | 43 ++ .../DockerAvailableUtils.cs | 54 ++ .../DockerDaemonTests.cs | 52 ++ .../ImageBuilderTests.cs | 378 ++++++++++++ .../ImageConfigTests.cs | 58 ++ ...soft.NET.Build.Containers.UnitTests.csproj | 19 + .../RegistryTests.cs | 397 +++++++++++++ .../Resources/ResourceTests.cs | 53 ++ 112 files changed, 6631 insertions(+), 6225 deletions(-) create mode 100644 src/Containers/Microsoft.NET.Build.Containers/AuthHeaderCache.cs create mode 100644 src/Containers/Microsoft.NET.Build.Containers/ContainerHttpException.cs create mode 100644 src/Containers/Microsoft.NET.Build.Containers/DiagnosticMessage.cs create mode 100644 src/Containers/Microsoft.NET.Build.Containers/ImageReference.cs create mode 100644 src/Containers/Microsoft.NET.Build.Containers/Logging/MSBuildLogger.cs create mode 100644 src/Containers/Microsoft.NET.Build.Containers/Logging/MSBuildLoggerProvider.cs create mode 100644 src/Containers/Microsoft.NET.Build.Containers/PublicAPI/net8.0/PublicAPI.Shipped.txt create mode 100644 src/Containers/Microsoft.NET.Build.Containers/PublicAPI/net8.0/PublicAPI.Unshipped.txt create mode 100644 src/Containers/Microsoft.NET.Build.Containers/Tasks/ComputeDotnetBaseImageTag.cs create mode 100644 src/Containers/Microsoft.NET.Build.Containers/net472Definitions.cs create mode 100644 src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/CapturingLogger.cs create mode 100644 src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/ContainerCli.cs create mode 100644 src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/CreateNewImageTests.cs create mode 100644 src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/CurrentFile.cs create mode 100644 src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/DockerRegistryManager.cs create mode 100644 src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/DockerRegistryTests.cs create mode 100644 src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/DockerSupportsArchInlineData.cs create mode 100644 src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/DockerTestsCollection.cs create mode 100644 src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/DockerTestsFixture.cs create mode 100644 src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/EndToEndTests.cs create mode 100644 src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/FullFramework/CreateNewImageToolTaskTests.cs create mode 100644 src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/LayerEndToEndTests.cs create mode 100644 src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/Microsoft.NET.Build.Containers.IntegrationTests.csproj create mode 100644 src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/PackageTests.cs create mode 100644 src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/ParseContainerPropertiesTests.cs create mode 100644 src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/ProjectInitializer.cs create mode 100644 src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/RegistryTests.cs create mode 100644 src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/TargetsTests.cs create mode 100644 src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/TestSettings.cs create mode 100644 src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/ToolsetUtils.cs create mode 100644 src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/TransientTestFolder.cs create mode 100644 src/Tests/Microsoft.NET.Build.Containers.UnitTests/AuthHeaderCacheTests.cs create mode 100644 src/Tests/Microsoft.NET.Build.Containers.UnitTests/ContainerHelpersTests.cs create mode 100644 src/Tests/Microsoft.NET.Build.Containers.UnitTests/CreateNewImageTests.cs create mode 100644 src/Tests/Microsoft.NET.Build.Containers.UnitTests/DescriptorTests.cs create mode 100644 src/Tests/Microsoft.NET.Build.Containers.UnitTests/DiagnosticMessageTests.cs create mode 100644 src/Tests/Microsoft.NET.Build.Containers.UnitTests/DockerAvailableUtils.cs create mode 100644 src/Tests/Microsoft.NET.Build.Containers.UnitTests/DockerDaemonTests.cs create mode 100644 src/Tests/Microsoft.NET.Build.Containers.UnitTests/ImageBuilderTests.cs create mode 100644 src/Tests/Microsoft.NET.Build.Containers.UnitTests/ImageConfigTests.cs create mode 100644 src/Tests/Microsoft.NET.Build.Containers.UnitTests/Microsoft.NET.Build.Containers.UnitTests.csproj create mode 100644 src/Tests/Microsoft.NET.Build.Containers.UnitTests/RegistryTests.cs create mode 100644 src/Tests/Microsoft.NET.Build.Containers.UnitTests/Resources/ResourceTests.cs diff --git a/containers.slnf b/containers.slnf index 00a95069dd9f..a11c0fd22a92 100644 --- a/containers.slnf +++ b/containers.slnf @@ -1,16 +1,16 @@ { "solution": { - "path": "sdk.slnx", + "path": "sdk.sln", "projects": [ "src\\Cli\\Microsoft.DotNet.Cli.Utils\\Microsoft.DotNet.Cli.Utils.csproj", "src\\Cli\\Microsoft.DotNet.InternalAbstractions\\Microsoft.DotNet.InternalAbstractions.csproj", "src\\Containers\\Microsoft.NET.Build.Containers\\Microsoft.NET.Build.Containers.csproj", "src\\Containers\\containerize\\containerize.csproj", "src\\Containers\\packaging\\package.csproj", - "test\\Microsoft.NET.Build.Containers.IntegrationTests\\Microsoft.NET.Build.Containers.IntegrationTests.csproj", - "test\\Microsoft.NET.Build.Containers.UnitTests\\Microsoft.NET.Build.Containers.UnitTests.csproj", - "test\\Microsoft.NET.TestFramework\\Microsoft.NET.TestFramework.csproj", - "test\\containerize.UnitTests\\containerize.UnitTests.csproj" + "src\\Tests\\Microsoft.NET.Build.Containers.IntegrationTests\\Microsoft.NET.Build.Containers.IntegrationTests.csproj", + "src\\Tests\\Microsoft.NET.Build.Containers.UnitTests\\Microsoft.NET.Build.Containers.UnitTests.csproj", + "src\\Tests\\Microsoft.NET.TestFramework\\Microsoft.NET.TestFramework.csproj", + "src\\Tests\\containerize.UnitTests\\containerize.UnitTests.csproj" ] } -} +} \ No newline at end of file diff --git a/src/Containers/Microsoft.NET.Build.Containers/AmazonECRMessageHandler.cs b/src/Containers/Microsoft.NET.Build.Containers/AmazonECRMessageHandler.cs index a5bbe038684f..8f15f1851a01 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/AmazonECRMessageHandler.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/AmazonECRMessageHandler.cs @@ -26,7 +26,7 @@ protected override async Task SendAsync(HttpRequestMessage catch (HttpRequestException e) when (e.InnerException is IOException ioe && ioe.Message.Equals("The response ended prematurely.", StringComparison.OrdinalIgnoreCase)) { string message = Resource.GetString(nameof(Strings.AmazonRegistryFailed)); - throw new ContainerHttpException(message, request.RequestUri?.ToString(), e.StatusCode); + throw new ContainerHttpException(message, request.RequestUri?.ToString(), null); } catch { diff --git a/src/Containers/Microsoft.NET.Build.Containers/AuthHandshakeMessageHandler.cs b/src/Containers/Microsoft.NET.Build.Containers/AuthHandshakeMessageHandler.cs index 46761e94df7c..de3a195fbf1b 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/AuthHandshakeMessageHandler.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/AuthHandshakeMessageHandler.cs @@ -1,20 +1,18 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Collections.Concurrent; -using System.ComponentModel; -using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Net; using System.Net.Http.Headers; -using System.Net.Sockets; using System.Text; using System.Text.Json; using System.Text.RegularExpressions; -using Microsoft.Extensions.Logging; + +using Valleysoft.DockerCredsProvider; + using Microsoft.NET.Build.Containers.Credentials; +using System.Net.Sockets; using Microsoft.NET.Build.Containers.Resources; -using Valleysoft.DockerCredsProvider; namespace Microsoft.NET.Build.Containers; @@ -25,39 +23,17 @@ internal sealed partial class AuthHandshakeMessageHandler : DelegatingHandler { private const int MaxRequestRetries = 5; // Arbitrary but seems to work ok for chunked uploads to ghcr.io - /// - /// Unique identifier that is used to tag requests from this library to external registries. - /// - /// - /// Valid characters for this clientID are in the unicode range 20-7E - /// - private const string ClientID = "netsdkcontainers"; - private const string BasicAuthScheme = "Basic"; - private const string BearerAuthScheme = "Bearer"; - - private sealed record AuthInfo(string Realm, string? Service, string? Scope); - - private readonly string _registryName; - private readonly ILogger _logger; - private readonly RegistryMode _registryMode; - private static ConcurrentDictionary _authenticationHeaders = new(); - - public AuthHandshakeMessageHandler(string registryName, HttpMessageHandler innerHandler, ILogger logger, RegistryMode mode) : base(innerHandler) - { - _registryName = registryName; - _logger = logger; - _registryMode = mode; - } + private sealed record AuthInfo(Uri Realm, string Service, string? Scope); /// /// the www-authenticate header must have realm, service, and scope information, so this method parses it into that shape if present /// /// - /// + /// /// - private static bool TryParseAuthenticationInfo(HttpResponseMessage msg, [NotNullWhen(true)] out string? scheme, out AuthInfo? bearerAuthInfo) + private static bool TryParseAuthenticationInfo(HttpResponseMessage msg, [NotNullWhen(true)] out string? scheme, [NotNullWhen(true)] out AuthInfo? authInfo) { - bearerAuthInfo = null; + authInfo = null; scheme = null; var authenticateHeader = msg.Headers.WwwAuthenticate; @@ -67,65 +43,29 @@ private static bool TryParseAuthenticationInfo(HttpResponseMessage msg, [NotNull } AuthenticationHeaderValue header = authenticateHeader.First(); - - if (header.Scheme is not null) + if (header is { Scheme: "Bearer" or "Basic", Parameter: string bearerArgs }) { scheme = header.Scheme; - - if (header.Scheme.Equals(BasicAuthScheme, StringComparison.OrdinalIgnoreCase)) - { - bearerAuthInfo = null; - return true; - } - else if (header.Scheme.Equals(BearerAuthScheme, StringComparison.OrdinalIgnoreCase)) - { - var keyValues = ParseBearerArgs(header.Parameter); - if (keyValues is null) - { - return false; - } - return TryParseBearerAuthInfo(keyValues, out bearerAuthInfo); - } - else + Dictionary keyValues = new(); + foreach (Match match in BearerParameterSplitter().Matches(bearerArgs)) { - return false; + keyValues.Add(match.Groups["key"].Value, match.Groups["value"].Value); } - } - return false; - static bool TryParseBearerAuthInfo(Dictionary authValues, [NotNullWhen(true)] out AuthInfo? authInfo) - { - if (authValues.TryGetValue("realm", out string? realm)) + if (keyValues.TryGetValue("realm", out string? realm) && keyValues.TryGetValue("service", out string? service)) { - string? service = null; - authValues.TryGetValue("service", out service); string? scope = null; - authValues.TryGetValue("scope", out scope); - authInfo = new AuthInfo(realm, service, scope); + keyValues.TryGetValue("scope", out scope); + authInfo = new AuthInfo(new Uri(realm), service, scope); return true; } - else - { - authInfo = null; - return false; - } } - static Dictionary? ParseBearerArgs(string? bearerHeaderArgs) - { - if (bearerHeaderArgs is null) - { - return null; - } - Dictionary keyValues = new(); - foreach (Match match in BearerParameterSplitter().Matches(bearerHeaderArgs)) - { - keyValues.Add(match.Groups["key"].Value, match.Groups["value"].Value); - } - return keyValues; - } + return false; } + public AuthHandshakeMessageHandler(HttpMessageHandler innerHandler) : base(innerHandler) { } + /// /// Response to a request to get a token using some auth. /// @@ -135,241 +75,81 @@ static bool TryParseBearerAuthInfo(Dictionary authValues, [NotNu private sealed record TokenResponse(string? token, string? access_token, int? expires_in, DateTimeOffset? issued_at) { public string ResolvedToken => token ?? access_token ?? throw new ArgumentException(Resource.GetString(nameof(Strings.InvalidTokenResponse))); - public DateTimeOffset ResolvedExpiration - { - get - { - var issueTime = this.issued_at ?? DateTimeOffset.UtcNow; // per spec, if no issued_at use the current time - var validityDuration = this.expires_in ?? 60; // per spec, if no expires_in use 60 seconds - var expirationTime = issueTime.AddSeconds(validityDuration); - return expirationTime; - } - } } /// /// Uses the authentication information from a 401 response to perform the authentication dance for a given registry. /// Credentials for the request are retrieved from the credential provider, then used to acquire a token. - /// That token is cached for some duration determined by the authentication mechanism on a per-host basis. + /// That token is cached for some duration on a per-host basis. /// - private async Task<(AuthenticationHeaderValue, DateTimeOffset)?> GetAuthenticationAsync(string registry, string scheme, AuthInfo? bearerAuthInfo, CancellationToken cancellationToken) + /// + /// + /// + /// + /// + private async Task GetAuthenticationAsync(string registry, string scheme, Uri realm, string service, string? scope, CancellationToken cancellationToken) { - DockerCredentials? privateRepoCreds; // Allow overrides for auth via environment variables - if (GetDockerCredentialsFromEnvironment(_registryMode) is (string credU, string credP)) - { - privateRepoCreds = new DockerCredentials(credU, credP); - } - else - { - privateRepoCreds = await GetLoginCredentials(registry).ConfigureAwait(false); - } - - if (scheme.Equals(BasicAuthScheme, StringComparison.OrdinalIgnoreCase)) - { - var authValue = new AuthenticationHeaderValue(BasicAuthScheme, Convert.ToBase64String(Encoding.ASCII.GetBytes($"{privateRepoCreds.Username}:{privateRepoCreds.Password}"))); - return new(authValue, DateTimeOffset.MaxValue); - } - else if (scheme.Equals(BearerAuthScheme, StringComparison.OrdinalIgnoreCase)) - { - Debug.Assert(bearerAuthInfo is not null); - - // Obtain a Bearer token, when the credentials are: - // - an identity token: use it for OAuth - // - a username/password: use them for Basic auth, and fall back to OAuth - - if (string.IsNullOrWhiteSpace(privateRepoCreds.IdentityToken)) - { - var authenticationValueAndDuration = await TryTokenGetAsync(privateRepoCreds, bearerAuthInfo, cancellationToken).ConfigureAwait(false); - if (authenticationValueAndDuration is not null) - { - return authenticationValueAndDuration; - } - } + string? credU = Environment.GetEnvironmentVariable(ContainerHelpers.HostObjectUser); + string? credP = Environment.GetEnvironmentVariable(ContainerHelpers.HostObjectPass); - return await TryOAuthPostAsync(privateRepoCreds, bearerAuthInfo, cancellationToken).ConfigureAwait(false); - } - else - { - return null; - } - } + // fetch creds for the host + DockerCredentials? privateRepoCreds; - internal static (string credU, string credP)? TryGetCredentialsFromEnvVars(string unameVar, string passwordVar) - { - var credU = Environment.GetEnvironmentVariable(unameVar); - var credP = Environment.GetEnvironmentVariable(passwordVar); if (!string.IsNullOrEmpty(credU) && !string.IsNullOrEmpty(credP)) { - return (credU, credP); + privateRepoCreds = new DockerCredentials(credU, credP); } else { - return null; - } - } - - /// - /// Gets docker credentials from the environment variables based on registry mode. - /// - internal static (string credU, string credP)? GetDockerCredentialsFromEnvironment(RegistryMode mode) - { - if (mode == RegistryMode.Push) - { - if (TryGetCredentialsFromEnvVars(ContainerHelpers.PushHostObjectUser, ContainerHelpers.PushHostObjectPass) is (string, string) pushCreds) + try { - return pushCreds; + privateRepoCreds = await CredsProvider.GetCredentialsAsync(registry).ConfigureAwait(false); } - - if (TryGetCredentialsFromEnvVars(ContainerHelpers.HostObjectUser, ContainerHelpers.HostObjectPass) is (string, string) genericCreds) + catch (Exception e) { - return genericCreds; + throw new CredentialRetrievalException(registry, e); } - - return TryGetCredentialsFromEnvVars(ContainerHelpers.HostObjectUserLegacy, ContainerHelpers.HostObjectPassLegacy); } - else if (mode == RegistryMode.Pull) + + if (scheme is "Basic") { - return TryGetCredentialsFromEnvVars(ContainerHelpers.PullHostObjectUser, ContainerHelpers.PullHostObjectPass); + var basicAuth = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes($"{privateRepoCreds.Username}:{privateRepoCreds.Password}"))); + return AuthHeaderCache.AddOrUpdate(realm, basicAuth); } - else if (mode == RegistryMode.PullFromOutput) + else if (scheme is "Bearer") { - if (TryGetCredentialsFromEnvVars(ContainerHelpers.PullHostObjectUser, ContainerHelpers.PullHostObjectPass) is (string, string) pullCreds) - { - return pullCreds; - } - - if (TryGetCredentialsFromEnvVars(ContainerHelpers.HostObjectUser, ContainerHelpers.HostObjectPass) is (string, string) genericCreds) + // use those creds when calling the token provider + var header = privateRepoCreds.Username == "" + ? new AuthenticationHeaderValue("Bearer", privateRepoCreds.Password) + : new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes($"{privateRepoCreds.Username}:{privateRepoCreds.Password}"))); + var builder = new UriBuilder(realm); + var queryDict = System.Web.HttpUtility.ParseQueryString(""); + queryDict["service"] = service; + if (scope is string s) { - return genericCreds; + queryDict["scope"] = s; } + builder.Query = queryDict.ToString(); + var message = new HttpRequestMessage(HttpMethod.Get, builder.ToString()); + message.Headers.Authorization = header; - return TryGetCredentialsFromEnvVars(ContainerHelpers.HostObjectUserLegacy, ContainerHelpers.HostObjectPassLegacy); - } - else - { - throw new InvalidEnumArgumentException(nameof(mode), (int)mode, typeof(RegistryMode)); - } - } - - /// - /// Implements the Docker OAuth2 Authentication flow as documented at . - /// TryOAuthPostAsync(DockerCredentials privateRepoCreds, AuthInfo bearerAuthInfo, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - Uri uri = new(bearerAuthInfo.Realm); - - _logger.LogTrace("Attempting to authenticate on {uri} using POST.", uri); - Dictionary parameters = new() - { - ["client_id"] = ClientID, - }; - if (!string.IsNullOrWhiteSpace(privateRepoCreds.IdentityToken)) - { - parameters["grant_type"] = "refresh_token"; - parameters["refresh_token"] = privateRepoCreds.IdentityToken; - } - else - { - parameters["grant_type"] = "password"; - parameters["username"] = privateRepoCreds.Username; - parameters["password"] = privateRepoCreds.Password; - } - if (bearerAuthInfo.Service is not null) - { - parameters["service"] = bearerAuthInfo.Service; - } - if (bearerAuthInfo.Scope is not null) - { - parameters["scope"] = bearerAuthInfo.Scope; - }; - HttpRequestMessage postMessage = new(HttpMethod.Post, uri) - { - Content = new FormUrlEncodedContent(parameters) - }; + var tokenResponse = await base.SendAsync(message, cancellationToken).ConfigureAwait(false); + tokenResponse.EnsureSuccessStatusCode(); - using HttpResponseMessage postResponse = await base.SendAsync(postMessage, cancellationToken).ConfigureAwait(false); - if (!postResponse.IsSuccessStatusCode) - { - await postResponse.LogHttpResponseAsync(_logger, cancellationToken).ConfigureAwait(false); - return null; // try next method - } - _logger.LogTrace("Received '{statuscode}'.", postResponse.StatusCode); - TokenResponse? tokenResponse = JsonSerializer.Deserialize(postResponse.Content.ReadAsStream(cancellationToken)); - if (tokenResponse is { } tokenEnvelope) - { - var authValue = new AuthenticationHeaderValue(BearerAuthScheme, tokenResponse.ResolvedToken); - return (authValue, tokenResponse.ResolvedExpiration); - } - else - { - _logger.LogTrace(Resource.GetString(nameof(Strings.CouldntDeserializeJsonToken))); - return null; // try next method - } - } - - /// - /// Implements the Docker Token Authentication flow as documented at - /// - private async Task<(AuthenticationHeaderValue, DateTimeOffset)?> TryTokenGetAsync(DockerCredentials privateRepoCreds, AuthInfo bearerAuthInfo, CancellationToken cancellationToken) - { - // this doesn't seem to be called out in the spec, but actual username/password auth information should be converted into Basic auth here, - // even though the overall Scheme we're authenticating for is Bearer - var header = new AuthenticationHeaderValue(BasicAuthScheme, Convert.ToBase64String(Encoding.ASCII.GetBytes($"{privateRepoCreds.Username}:{privateRepoCreds.Password}"))); - var builder = new UriBuilder(new Uri(bearerAuthInfo.Realm)); - - _logger.LogTrace("Attempting to authenticate on {uri} using GET.", bearerAuthInfo.Realm); - var queryDict = System.Web.HttpUtility.ParseQueryString(""); - if (bearerAuthInfo.Service is string svc) - { - queryDict["service"] = svc; - } - if (bearerAuthInfo.Scope is string s) - { - queryDict["scope"] = s; - } - builder.Query = queryDict.ToString(); - var message = new HttpRequestMessage(HttpMethod.Get, builder.ToString()); - message.Headers.Authorization = header; - - using var tokenResponse = await base.SendAsync(message, cancellationToken).ConfigureAwait(false); - if (!tokenResponse.IsSuccessStatusCode) - { - await tokenResponse.LogHttpResponseAsync(_logger, cancellationToken).ConfigureAwait(false); - return null; // try next method - } - - TokenResponse? token = JsonSerializer.Deserialize(tokenResponse.Content.ReadAsStream(cancellationToken)); - if (token is null) - { - throw new ArgumentException(Resource.GetString(nameof(Strings.CouldntDeserializeJsonToken))); - } - return (new AuthenticationHeaderValue(BearerAuthScheme, token.ResolvedToken), token.ResolvedExpiration); - } - - private static async Task GetLoginCredentials(string registry) - { - // For authentication with Docker Hub, 'docker login' uses 'https://index.docker.io/v1/' as the registry key. - // And 'podman login docker.io' uses 'docker.io'. - // Try the key used by 'docker' first, and then fall back to the regular case for 'podman'. - if (registry == ContainerHelpers.DockerRegistryAlias) - { - try + TokenResponse? token = JsonSerializer.Deserialize(tokenResponse.Content.ReadAsStream(cancellationToken)); + if (token is null) { - return await CredsProvider.GetCredentialsAsync("https://index.docker.io/v1/").ConfigureAwait(false); + throw new ArgumentException(Resource.GetString(nameof(Strings.CouldntDeserializeJsonToken))); } - catch - { } - } - try - { - return await CredsProvider.GetCredentialsAsync(registry).ConfigureAwait(false); + // save the retrieved token in the cache + var bearerAuth = new AuthenticationHeaderValue("Bearer", token.ResolvedToken); + return AuthHeaderCache.AddOrUpdate(realm, bearerAuth); } - catch (Exception e) + else { - throw new CredentialRetrievalException(registry, e); + return null; } } @@ -380,13 +160,13 @@ protected override async Task SendAsync(HttpRequestMessage throw new ArgumentException(Resource.GetString(nameof(Strings.NoRequestUriSpecified)), nameof(request)); } - if (_authenticationHeaders.TryGetValue(_registryName, out AuthenticationHeaderValue? header)) + // attempt to use cached token for the request if available + if (AuthHeaderCache.TryGet(request.RequestUri, out AuthenticationHeaderValue? cachedAuthentication)) { - request.Headers.Authorization = header; + request.Headers.Authorization = cachedAuthentication; } int retryCount = 0; - List? requestExceptions = null; while (retryCount < MaxRequestRetries) { @@ -399,18 +179,12 @@ protected override async Task SendAsync(HttpRequestMessage } else if (response is { StatusCode: HttpStatusCode.Unauthorized } && TryParseAuthenticationInfo(response, out string? scheme, out AuthInfo? authInfo)) { - // Load the reply so the HTTP connection becomes available to send the authentication request. - // Ideally we'd call LoadIntoBufferAsync, but it has no overload that accepts a CancellationToken so we call ReadAsByteArrayAsync instead. - _ = await response.Content.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false); - - if (await GetAuthenticationAsync(_registryName, scheme, authInfo, cancellationToken).ConfigureAwait(false) is (AuthenticationHeaderValue authHeader, DateTimeOffset expirationTime)) + if (await GetAuthenticationAsync(request.RequestUri.Host, scheme, authInfo.Realm, authInfo.Service, authInfo.Scope, cancellationToken).ConfigureAwait(false) is AuthenticationHeaderValue authentication) { - _authenticationHeaders[_registryName] = authHeader; - request.Headers.Authorization = authHeader; + request.Headers.Authorization = AuthHeaderCache.AddOrUpdate(request.RequestUri, authentication); return await base.SendAsync(request, cancellationToken).ConfigureAwait(false); } - - throw new UnableToAccessRepositoryException(_registryName); + return response; } else { @@ -419,12 +193,11 @@ protected override async Task SendAsync(HttpRequestMessage } catch (HttpRequestException e) when (e.InnerException is IOException ioe && ioe.InnerException is SocketException se) { - requestExceptions ??= new(); - requestExceptions.Add(e); - retryCount += 1; - _logger.LogInformation("Encountered a HttpRequestException {error} with message \"{message}\". Pausing before retry.", e.HttpRequestError, se.Message); - _logger.LogTrace("Exception details: {ex}", se); + + // TODO: log in a way that is MSBuild-friendly + Console.WriteLine($"Encountered a SocketException with message \"{se.Message}\". Pausing before retry."); + await Task.Delay(TimeSpan.FromSeconds(1.0 * Math.Pow(2, retryCount)), cancellationToken).ConfigureAwait(false); // retry @@ -432,7 +205,7 @@ protected override async Task SendAsync(HttpRequestMessage } } - throw new ApplicationException(Resource.GetString(nameof(Strings.TooManyRetries)), new AggregateException(requestExceptions!)); + throw new ApplicationException(Resource.GetString(nameof(Strings.TooManyRetries))); } [GeneratedRegex("(?\\w+)=\"(?[^\"]*)\"(?:,|$)")] diff --git a/src/Containers/Microsoft.NET.Build.Containers/AuthHeaderCache.cs b/src/Containers/Microsoft.NET.Build.Containers/AuthHeaderCache.cs new file mode 100644 index 000000000000..274f6a14e85b --- /dev/null +++ b/src/Containers/Microsoft.NET.Build.Containers/AuthHeaderCache.cs @@ -0,0 +1,60 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; +using System.Net.Http.Headers; + +namespace Microsoft.NET.Build.Containers; + +internal static class AuthHeaderCache +{ + private static readonly ConcurrentDictionary s_hostAuthenticationCache = new(); + + public static bool TryGet(Uri uri, [NotNullWhen(true)] out AuthenticationHeaderValue? header) + { + return s_hostAuthenticationCache.TryGetValue(GetCacheKey(uri), out header); ; + } + + public static AuthenticationHeaderValue AddOrUpdate(Uri uri, AuthenticationHeaderValue header) + { + return s_hostAuthenticationCache.AddOrUpdate(GetCacheKey(uri), header, (_, _) => header); + } + + internal static string GetCacheKey(Uri uri) + { + string finalUri = uri.Host + uri.AbsolutePath; + + //trim uri parameters + //cases: + //push: + //POST /v2//blobs/uploads/ + //HEAD /v2//blobs/ + //GET /v2//blobs/uploads/ + //PUT /v2//blobs/uploads/?digest= + //PATCH /v2//blobs/uploads/ + //PUT /v2//manifests/ + + //pull: + //GET /v2//manifests/ + //HEAD /v2//manifests/ + //GET /v2//blobs/ + + IReadOnlyList possibleUris = new[] { "blobs/uploads/", "blobs/", "manifests/" }; + + foreach (string end in possibleUris) + { + int index = finalUri.IndexOf(end); + if (index == -1) + { + continue; + } + else + { + finalUri = finalUri.Substring(0, index + end.Length); + break; + } + } + return finalUri; + } +} diff --git a/src/Containers/Microsoft.NET.Build.Containers/BaseImageNotFoundException.cs b/src/Containers/Microsoft.NET.Build.Containers/BaseImageNotFoundException.cs index db4238d3d7c8..6c717b158029 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/BaseImageNotFoundException.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/BaseImageNotFoundException.cs @@ -6,11 +6,5 @@ namespace Microsoft.NET.Build.Containers; public sealed class BaseImageNotFoundException : Exception { internal BaseImageNotFoundException(string specifiedRuntimeIdentifier, string repositoryName, string reference, IEnumerable supportedRuntimeIdentifiers) - : base($"The RuntimeIdentifier '{specifiedRuntimeIdentifier}' is not supported by {repositoryName}:{reference}. The supported RuntimeIdentifiers are {String.Join(",", supportedRuntimeIdentifiers)}") - { - RequestedRuntimeIdentifier = specifiedRuntimeIdentifier; - AvailableRuntimeIdentifiers = supportedRuntimeIdentifiers; - } - internal string RequestedRuntimeIdentifier { get; } - internal IEnumerable AvailableRuntimeIdentifiers { get; } + : base($"The RuntimeIdentifier '{specifiedRuntimeIdentifier}' is not supported by {repositoryName}:{reference}. The supported RuntimeIdentifiers are {String.Join(",", supportedRuntimeIdentifiers)}") {} } diff --git a/src/Containers/Microsoft.NET.Build.Containers/BuiltImage.cs b/src/Containers/Microsoft.NET.Build.Containers/BuiltImage.cs index 91f47193ce66..b3bb668fca28 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/BuiltImage.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/BuiltImage.cs @@ -16,42 +16,22 @@ internal readonly struct BuiltImage /// /// Gets image digest. /// - internal string? ImageDigest { get; init; } + internal required string ImageDigest { get; init; } /// /// Gets image SHA. /// - internal string? ImageSha { get; init; } + internal required string ImageSha { get; init; } /// - /// Gets image manifest. - /// - internal required string Manifest { get; init; } - - /// - /// Gets manifest digest. - /// - internal required string ManifestDigest { get; init; } - - /// - /// Gets manifest mediaType. + /// Gets image size. /// - internal required string ManifestMediaType { get; init; } + internal required long ImageSize { get; init; } /// - /// Gets image layers. - /// - internal List? Layers { get; init; } - - /// - /// Gets image OS. - /// - internal string? OS { get; init; } - - /// - /// Gets image architecture. + /// Gets image manifest. /// - internal string? Architecture { get; init; } + internal required ManifestV2 Manifest { get; init; } /// /// Gets layers descriptors. @@ -60,7 +40,7 @@ internal IEnumerable LayerDescriptors { get { - List layersNode = Layers ?? throw new NotImplementedException("Tried to get layer information but there is no layer node?"); + List layersNode = Manifest.Layers ?? throw new NotImplementedException("Tried to get layer information but there is no layer node?"); foreach (ManifestLayer layer in layersNode) { yield return new(layer.mediaType, layer.digest, layer.size); diff --git a/src/Containers/Microsoft.NET.Build.Containers/Constants.cs b/src/Containers/Microsoft.NET.Build.Containers/Constants.cs index f408aef43165..a3a35bdfc226 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Constants.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/Constants.cs @@ -5,8 +5,7 @@ namespace Microsoft.NET.Build.Containers; -public static class Constants -{ +public static class Constants { public static readonly string Version = typeof(Registry).Assembly.GetCustomAttribute()?.InformationalVersion ?? ""; } diff --git a/src/Containers/Microsoft.NET.Build.Containers/ContainerBuilder.cs b/src/Containers/Microsoft.NET.Build.Containers/ContainerBuilder.cs index e8153bb32314..7a15ee444e1d 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/ContainerBuilder.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/ContainerBuilder.cs @@ -6,27 +6,16 @@ namespace Microsoft.NET.Build.Containers; -internal enum KnownImageFormats +public static class ContainerBuilder { - OCI, - Docker -} - -internal static class ContainerBuilder -{ - internal static async Task ContainerizeAsync( + public static async Task ContainerizeAsync( DirectoryInfo publishDirectory, string workingDir, string baseRegistry, string baseImageName, string baseImageTag, - string? baseImageDigest, string[] entrypoint, - string[] entrypointArgs, - string[] defaultArgs, - string[] appCommand, - string[] appCommandArgs, - string appCommandInstruction, + string[]? cmd, string imageName, string[] imageTags, string? outputRegistry, @@ -37,10 +26,6 @@ internal static async Task ContainerizeAsync( string ridGraphPath, string localRegistry, string? containerUser, - string? archiveOutputPath, - bool generateLabels, - bool generateDigestLabel, - KnownImageFormats? imageFormat, ILoggerFactory loggerFactory, CancellationToken cancellationToken) { @@ -53,46 +38,22 @@ internal static async Task ContainerizeAsync( logger.LogTrace("Trace logging: enabled."); bool isLocalPull = string.IsNullOrEmpty(baseRegistry); - RegistryMode sourceRegistryMode = baseRegistry.Equals(outputRegistry, StringComparison.InvariantCultureIgnoreCase) ? RegistryMode.PullFromOutput : RegistryMode.Pull; - Registry? sourceRegistry = isLocalPull ? null : new Registry(baseRegistry, logger, sourceRegistryMode); - SourceImageReference sourceImageReference = new(sourceRegistry, baseImageName, baseImageTag, baseImageDigest); + Registry? sourceRegistry = isLocalPull ? null : new Registry(ContainerHelpers.TryExpandRegistryToUri(baseRegistry), logger); + ImageReference sourceImageReference = new(sourceRegistry, baseImageName, baseImageTag); - DestinationImageReference destinationImageReference = DestinationImageReference.CreateFromSettings( - imageName, - imageTags, - loggerFactory, - archiveOutputPath, - outputRegistry, - localRegistry); + bool isLocalPush = string.IsNullOrEmpty(outputRegistry); + Registry? destinationRegistry = isLocalPush ? null : new Registry(ContainerHelpers.TryExpandRegistryToUri(outputRegistry!), logger); + IEnumerable destinationImageReferences = imageTags.Select(t => new ImageReference(destinationRegistry, imageName, t)); ImageBuilder? imageBuilder; if (sourceRegistry is { } registry) { - try - { - var ridGraphPicker = new RidGraphManifestPicker(ridGraphPath); - imageBuilder = await registry.GetImageManifestAsync( - baseImageName, - sourceImageReference.Reference, - containerRuntimeIdentifier, - ridGraphPicker, - cancellationToken).ConfigureAwait(false); - } - catch (RepositoryNotFoundException) - { - logger.LogError(Resource.FormatString(nameof(Strings.RepositoryNotFound), baseImageName, baseImageTag, baseImageDigest, registry.RegistryName)); - return 1; - } - catch (UnableToAccessRepositoryException) - { - logger.LogError(Resource.FormatString(nameof(Strings.UnableToAccessRepository), baseImageName, registry.RegistryName)); - return 1; - } - catch (ContainerHttpException e) - { - logger.LogError(e.Message); - return 1; - } + imageBuilder = await registry.GetImageManifestAsync( + baseImageName, + baseImageTag, + containerRuntimeIdentifier, + ridGraphPath, + cancellationToken).ConfigureAwait(false); } else { @@ -106,58 +67,15 @@ internal static async Task ContainerizeAsync( logger.LogInformation(Strings.ContainerBuilder_StartBuildingImage, imageName, string.Join(",", imageName), sourceImageReference); cancellationToken.ThrowIfCancellationRequested(); - // forcibly change the media type if required - imageBuilder.ManifestMediaType = imageFormat switch - { - null => imageBuilder.ManifestMediaType, - KnownImageFormats.Docker => SchemaTypes.DockerManifestV2, - KnownImageFormats.OCI => SchemaTypes.OciManifestV1, - _ => imageBuilder.ManifestMediaType // should be impossible unless we add to the enum - }; - var userId = imageBuilder.IsWindows ? null : TryParseUserId(containerUser); - Layer newLayer = Layer.FromDirectory(publishDirectory.FullName, workingDir, imageBuilder.IsWindows, imageBuilder.ManifestMediaType, userId); + Layer newLayer = Layer.FromDirectory(publishDirectory.FullName, workingDir, imageBuilder.IsWindows); imageBuilder.AddLayer(newLayer); imageBuilder.SetWorkingDirectory(workingDir); - - bool hasErrors = false; - (string[] imageEntrypoint, string[] imageCmd) = ImageBuilder.DetermineEntrypointAndCmd(entrypoint, entrypointArgs, defaultArgs, appCommand, appCommandArgs, appCommandInstruction, - baseImageEntrypoint: imageBuilder.BaseImageConfig.GetEntrypoint(), - logWarning: s => - { - logger.LogWarning(Resource.GetString(nameof(s))); - }, - logError: (s, a) => - { - hasErrors = true; - if (a is null) - { - logger.LogError(Resource.GetString(nameof(s))); - } - else - { - logger.LogError(Resource.FormatString(nameof(s), a)); - } - }); - if (hasErrors) + imageBuilder.SetEntrypointAndCmd(entrypoint, cmd ?? Array.Empty()); + foreach (KeyValuePair label in labels) { - return 1; + // labels are validated by System.CommandLine API + imageBuilder.AddLabel(label.Key, label.Value); } - imageBuilder.SetEntrypointAndCmd(imageEntrypoint, imageCmd); - - if (generateLabels) - { - foreach (KeyValuePair label in labels) - { - // labels are validated by System.CommandLine API - imageBuilder.AddLabel(label.Key, label.Value); - } - - if (generateDigestLabel) - { - imageBuilder.AddBaseImageDigestLabel(); - } - } - foreach (KeyValuePair envVar in envVars) { imageBuilder.AddEnvironmentVariable(envVar.Key, envVar.Value); @@ -174,104 +92,49 @@ internal static async Task ContainerizeAsync( BuiltImage builtImage = imageBuilder.Build(); cancellationToken.ThrowIfCancellationRequested(); - int exitCode; - switch (destinationImageReference.Kind) - { - case DestinationImageReferenceKind.LocalRegistry: - exitCode = await PushToLocalRegistryAsync( - logger, - builtImage, - sourceImageReference, - destinationImageReference, - cancellationToken).ConfigureAwait(false); - break; - case DestinationImageReferenceKind.RemoteRegistry: - exitCode = await PushToRemoteRegistryAsync( - logger, - builtImage, - sourceImageReference, - destinationImageReference, - cancellationToken).ConfigureAwait(false); - break; - default: - throw new ArgumentOutOfRangeException(); - } - - return exitCode; - } - - public static int? TryParseUserId(string? containerUser) - { - if (containerUser is null) - { - return null; - } - if (int.TryParse(containerUser, out int userId)) - { - return userId; - } - if (containerUser.Equals("root", StringComparison.OrdinalIgnoreCase)) - { - return 0; // root user - } - // TODO: on Linux we could _potentially_ try to map the user name to a UID - return null; - } - - private static async Task PushToLocalRegistryAsync(ILogger logger, BuiltImage builtImage, SourceImageReference sourceImageReference, - DestinationImageReference destinationImageReference, - CancellationToken cancellationToken) - { - ILocalRegistry containerRegistry = destinationImageReference.LocalRegistry!; - if (!(await containerRegistry.IsAvailableAsync(cancellationToken).ConfigureAwait(false))) + foreach (ImageReference destinationImageReference in destinationImageReferences) { - logger.LogError(Resource.FormatString(nameof(Strings.LocalRegistryNotAvailable))); - return 7; - } - - try - { - await containerRegistry.LoadAsync(builtImage, sourceImageReference, destinationImageReference, cancellationToken).ConfigureAwait(false); - logger.LogInformation(Strings.ContainerBuilder_ImageUploadedToLocalDaemon, destinationImageReference, containerRegistry); - } - catch (UnableToDownloadFromRepositoryException) - { - logger.LogError(Resource.FormatString(nameof(Strings.UnableToDownloadFromRepository)), sourceImageReference); - return 1; - } - catch (Exception ex) - { - logger.LogError(Resource.FormatString(nameof(Strings.RegistryOutputPushFailed), ex.Message)); - return 1; - } - - return 0; - } + if (isLocalPush) + { + ILocalRegistry containerRegistry = KnownLocalRegistryTypes.CreateLocalRegistry(localRegistry, loggerFactory); + if (!(await containerRegistry.IsAvailableAsync(cancellationToken).ConfigureAwait(false))) + { + Console.WriteLine(DiagnosticMessage.ErrorFromResourceWithCode(nameof(Strings.LocalRegistryNotAvailable))); + return 7; + } - private static async Task PushToRemoteRegistryAsync(ILogger logger, BuiltImage builtImage, SourceImageReference sourceImageReference, - DestinationImageReference destinationImageReference, - CancellationToken cancellationToken) - { - try - { - await (destinationImageReference.RemoteRegistry!.PushAsync( - builtImage, - sourceImageReference, - destinationImageReference, - cancellationToken)).ConfigureAwait(false); - logger.LogInformation(Strings.ContainerBuilder_ImageUploadedToRegistry, destinationImageReference, destinationImageReference.RemoteRegistry.RegistryName); - } - catch (UnableToDownloadFromRepositoryException) - { - logger.LogError(Resource.FormatString(nameof(Strings.UnableToDownloadFromRepository)), sourceImageReference); - return 1; - } - catch (Exception e) - { - logger.LogError(Resource.FormatString(nameof(Strings.RegistryOutputPushFailed), e.Message)); - return 1; + try + { + await containerRegistry.LoadAsync(builtImage, sourceImageReference, destinationImageReference, cancellationToken).ConfigureAwait(false); + logger.LogInformation(Strings.ContainerBuilder_ImageUploadedToLocalDaemon, destinationImageReference.RepositoryAndTag); + } + catch (Exception ex) + { + Console.WriteLine(DiagnosticMessage.ErrorFromResourceWithCode(nameof(Strings.RegistryOutputPushFailed), ex.Message)); + return 1; + } + } + else + { + try + { + if (destinationImageReference.Registry is not null) + { + await (destinationImageReference.Registry.PushAsync( + builtImage, + sourceImageReference, + destinationImageReference, + cancellationToken)).ConfigureAwait(false); + logger.LogInformation(Strings.ContainerBuilder_ImageUploadedToRegistry, destinationImageReference.RepositoryAndTag, destinationImageReference.Registry.RegistryName); + } + } + catch (Exception e) + { + Console.WriteLine(DiagnosticMessage.ErrorFromResourceWithCode(nameof(Strings.RegistryOutputPushFailed), e.Message)); + return 1; + } + } } - return 0; } } diff --git a/src/Containers/Microsoft.NET.Build.Containers/ContainerHelpers.cs b/src/Containers/Microsoft.NET.Build.Containers/ContainerHelpers.cs index 7f114caaba21..6677ca3fd8a9 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/ContainerHelpers.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/ContainerHelpers.cs @@ -3,37 +3,33 @@ #if NETFRAMEWORK using System; -using System.Linq; #endif #if NET using System.Diagnostics.CodeAnalysis; #endif -using System.Text; -using System.Text.Json.Nodes; using System.Text.RegularExpressions; using Microsoft.NET.Build.Containers.Resources; namespace Microsoft.NET.Build.Containers; public static class ContainerHelpers { - internal const string HostObjectUser = "DOTNET_CONTAINER_REGISTRY_UNAME"; - internal const string HostObjectUserLegacy = "SDK_CONTAINER_REGISTRY_UNAME"; + internal const string HostObjectUser = "SDK_CONTAINER_REGISTRY_UNAME"; - internal const string HostObjectPass = "DOTNET_CONTAINER_REGISTRY_PWORD"; - internal const string HostObjectPassLegacy = "SDK_CONTAINER_REGISTRY_PWORD"; - - internal const string PushHostObjectUser = "DOTNET_CONTAINER_PUSH_REGISTRY_UNAME"; - internal const string PushHostObjectPass = "DOTNET_CONTAINER_PUSH_REGISTRY_PWORD"; - - internal const string PullHostObjectUser = "DOTNET_CONTAINER_PULL_REGISTRY_UNAME"; - internal const string PullHostObjectPass = "DOTNET_CONTAINER_PULL_REGISTRY_PWORD"; + internal const string HostObjectPass = "SDK_CONTAINER_REGISTRY_PWORD"; internal const string DockerRegistryAlias = "docker.io"; - + /// /// Matches an environment variable name - must start with a letter or underscore, and can only contain letters, numbers, and underscores. /// - private static Regex envVarRegex = new(@"^[a-zA-Z_]{1,}[a-zA-Z0-9_]*$"); + private static Regex envVarRegex = new Regex(@"^[a-zA-Z_]{1,}[a-zA-Z0-9_]*$"); + + /// + /// Matches if the string is not lowercase or numeric, or ., _, or -. + /// + /// Technically the period should be allowed as well, but due to inconsistent support between cloud providers we're removing it. + private static Regex imageNameCharacters = new Regex(@"[^a-z0-9_\-/]"); + /// /// The enum contains possible error reasons during port parsing using or . @@ -41,10 +37,10 @@ public static class ContainerHelpers [Flags] public enum ParsePortError { - MissingPortNumber = 1, - InvalidPortNumber = 2, - InvalidPortType = 4, - UnknownPortFormat = 8 + MissingPortNumber, + InvalidPortNumber, + InvalidPortType, + UnknownPortFormat } /// @@ -59,7 +55,7 @@ public static bool TryParsePort(string? portNumber, string? portType, [NotNullWh { var portNo = 0; error = null; - if (string.IsNullOrEmpty(portNumber)) + if (String.IsNullOrEmpty(portNumber)) { error = ParsePortError.MissingPortNumber; } @@ -68,9 +64,9 @@ public static bool TryParsePort(string? portNumber, string? portType, [NotNullWh error = ParsePortError.InvalidPortNumber; } - if (!Enum.TryParse(portType, out PortType t)) + if (!Enum.TryParse(portType, out PortType t)) { - if (!string.IsNullOrEmpty(portType)) + if (portType is not null) { error = (error ?? ParsePortError.InvalidPortType) | ParsePortError.InvalidPortType; } @@ -152,6 +148,16 @@ internal static bool IsValidImageTag(string imageTag) return ReferenceParser.anchoredTagRegexp.IsMatch(imageTag); } + /// + /// Given an already-validated registry domain, this is our hueristic to determine what HTTP protocol should be used to interact with it. + /// This is primarily for testing - in the real world almost all usage should be through HTTPS! + /// + internal static Uri TryExpandRegistryToUri(string alreadyValidatedDomain) + { + var prefix = alreadyValidatedDomain.StartsWith("localhost", StringComparison.Ordinal) ? "http" : "https"; + return new Uri($"{prefix}://{alreadyValidatedDomain}"); + } + /// /// Ensures a given environment variable is valid. /// @@ -253,81 +259,26 @@ out bool isRegistrySpecified return true; } - - /// /// Checks if a given container image name adheres to the image name spec. If not, and recoverable, then normalizes invalid characters. /// - internal static (string? normalizedImageName, (string, object[])? normalizationWarning, (string, object[])? normalizationError) NormalizeRepository(string containerRepository) + internal static bool NormalizeRepository(string containerRepository, + [NotNullWhen(false)] out string? normalizedImageName) { if (IsValidImageName(containerRepository)) { - return (containerRepository, null, null); + normalizedImageName = null; + return true; } else { - // check for leading alphanumeric character - char firstChar = containerRepository[0]; - if (!IsAlpha(firstChar) && !IsNumeric(firstChar)) + if (!Char.IsLetterOrDigit(containerRepository, 0)) { - // The name did not start with an alphanumeric character, so we can't normalize it. - var error = (nameof(Strings.InvalidImageName_NonAlphanumericStartCharacter), new[] { containerRepository }); - return (null, null, error); - } - - - // normalize the name. a little more complex, but this does all of our checks in a single pass and doesn't require coming back - // after the normalization to check if our invariants hold - var invalidChars = 0; - var normalizationOccurred = false; - var builder = new StringBuilder(containerRepository); - for (int i = 0; i < containerRepository.Length; i++) - { - var current = containerRepository[i]; - if (IsLowerAlpha(current) || IsNumeric(current) || IsAllowedPunctuation(current)) - { - // no need to set the builder's char here, since we preloaded - } - else if (IsUpperAlpha(current)) - { - builder[i] = char.ToLowerInvariant(current); - normalizationOccurred = true; - } - else - { - builder[i] = '-'; - normalizationOccurred = true; - invalidChars++; - } - } - var normalizedImageName = builder.ToString(); - - // check for normalization to useless name - if (invalidChars == builder.Length) - { - // The name was normalized to all dashes, so there was nothing recoverable. We should throw. - var error = (nameof(Strings.InvalidImageName_EntireNameIsInvalidCharacters), new string[] { containerRepository }); - return (null, null, error); - } - - // check for warning/notification that we did indeed perform normalization - if (normalizationOccurred) - { - var warning = (nameof(Strings.NormalizedContainerName), new string[] { containerRepository, normalizedImageName }); - return (normalizedImageName, warning, null); - } - - // user value was already normalized, so we don't need to do anything - else - { - return (containerRepository, null, null); + throw new ArgumentException(Resources.Resource.GetString(nameof(Strings.InvalidImageName))); } + var loweredImageName = containerRepository.ToLowerInvariant(); + normalizedImageName = imageNameCharacters.Replace(loweredImageName, "-"); + return false; } - - static bool IsUpperAlpha(char c) => c >= 'A' && c <= 'Z'; - static bool IsLowerAlpha(char c) => c >= 'a' && c <= 'z'; - static bool IsAlpha(char c) => IsLowerAlpha(c) || IsUpperAlpha(c); - static bool IsNumeric(char c) => c >= '0' && c <= '9'; - static bool IsAllowedPunctuation(char c) => (c == '_') || (c == '-') || (c == '/'); } } diff --git a/src/Containers/Microsoft.NET.Build.Containers/ContainerHttpException.cs b/src/Containers/Microsoft.NET.Build.Containers/ContainerHttpException.cs new file mode 100644 index 000000000000..4bcb5a9f02ad --- /dev/null +++ b/src/Containers/Microsoft.NET.Build.Containers/ContainerHttpException.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.NET.Build.Containers; + +internal sealed class ContainerHttpException : Exception +{ + private const string ErrorPrefix = "Containerize: error CONTAINER004:"; + string? jsonResponse; + string? uri; + public ContainerHttpException(string message, string? targetUri, string? jsonResp) + : base($"{ErrorPrefix} {message}\nURI: {targetUri ?? "Unknown"}\nJson Response: {jsonResp ?? "None."}") + { + jsonResponse = jsonResp; + uri = targetUri; + } +} diff --git a/src/Containers/Microsoft.NET.Build.Containers/ContentStore.cs b/src/Containers/Microsoft.NET.Build.Containers/ContentStore.cs index ebd1e3984d8b..f7590668b87a 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/ContentStore.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/ContentStore.cs @@ -1,7 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Text.RegularExpressions; +using System.Diagnostics; using Microsoft.NET.Build.Containers.Resources; namespace Microsoft.NET.Build.Containers; @@ -11,14 +11,7 @@ internal static class ContentStore public static string ArtifactRoot { get; set; } = Path.Combine(Path.GetTempPath(), "Containers"); public static string ContentRoot { - get - { - string contentPath = Path.Join(ArtifactRoot, "Content"); - - Directory.CreateDirectory(contentPath); - - return contentPath; - } + get => Path.Combine(ArtifactRoot, "Content"); } public static string TempPath @@ -33,16 +26,13 @@ public static string TempPath } } - private static readonly Regex s_sha256DigestRegex = new(@"^sha256:[0-9A-Fa-f]{64}$", RegexOptions.Compiled); - public static string PathForDescriptor(Descriptor descriptor) { - string digestString = descriptor.Digest; - if (!s_sha256DigestRegex.IsMatch(digestString)) - { - throw new ArgumentException($"Invalid digest: {digestString}", nameof(descriptor.Digest)); - } - string digestValue = digestString.Substring("sha256:".Length); + string digest = descriptor.Digest; + + Debug.Assert(digest.StartsWith("sha256:", StringComparison.Ordinal)); + + string contentHash = digest.Substring("sha256:".Length); string extension = descriptor.MediaType switch { @@ -56,8 +46,13 @@ public static string PathForDescriptor(Descriptor descriptor) _ => throw new ArgumentException(Resource.FormatString(nameof(Strings.UnrecognizedMediaType), descriptor.MediaType)) }; - string descriptorPath = Path.Combine(ContentRoot, digestValue) + extension; - return descriptorPath; + return GetPathForHash(contentHash) + extension; + } + + + public static string GetPathForHash(string contentHash) + { + return Path.Combine(ContentRoot, contentHash); } public static string GetTempFile() diff --git a/src/Containers/Microsoft.NET.Build.Containers/Credentials/CredentialRetrievalException.cs b/src/Containers/Microsoft.NET.Build.Containers/Credentials/CredentialRetrievalException.cs index ff157eb72b23..e4370f849a35 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Credentials/CredentialRetrievalException.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/Credentials/CredentialRetrievalException.cs @@ -7,9 +7,9 @@ namespace Microsoft.NET.Build.Containers.Credentials; internal sealed class CredentialRetrievalException : Exception { - public CredentialRetrievalException(string registry, Exception innerException) - : base( + public CredentialRetrievalException(string registry, Exception innerException) + : base( Resource.FormatString(nameof(Strings.FailedRetrievingCredentials), registry, innerException.Message), innerException) - { } + { } } diff --git a/src/Containers/Microsoft.NET.Build.Containers/Descriptor.cs b/src/Containers/Microsoft.NET.Build.Containers/Descriptor.cs index 63a4588c8280..567f29fab42d 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Descriptor.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/Descriptor.cs @@ -59,7 +59,7 @@ public readonly record struct Descriptor /// /// /// - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public Dictionary? Annotations { get; init; } = null; /// @@ -68,7 +68,7 @@ public readonly record struct Descriptor /// /// /// - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Data { get; init; } = null; public Descriptor(string mediaType, string digest, long size) diff --git a/src/Containers/Microsoft.NET.Build.Containers/DiagnosticMessage.cs b/src/Containers/Microsoft.NET.Build.Containers/DiagnosticMessage.cs new file mode 100644 index 000000000000..055867c707b9 --- /dev/null +++ b/src/Containers/Microsoft.NET.Build.Containers/DiagnosticMessage.cs @@ -0,0 +1,50 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; +using Microsoft.NET.Build.Containers.Resources; + +namespace Microsoft.NET.Build.Containers; + +/// +/// Represents a diagnostic message that could be parsed by VS. +/// https://learn.microsoft.com/en-us/visualstudio/msbuild/msbuild-diagnostic-format-for-tasks?view=vs-2022 +/// +internal static class DiagnosticMessage +{ + public static string Warning(string code, string text) => Create("warning", code, text); + + public static string Error(string code, string text) => Create("error", code, text); + + public static string WarningFromResourceWithCode(string resourceName, params object?[] args) => CreateFromResourceWithCode("warning", resourceName, args); + + public static string ErrorFromResourceWithCode(string resourceName, params object?[] args) => CreateFromResourceWithCode("error", resourceName, args); + + private static string Create(string category, string code, string text) + { + StringBuilder builder = new(); + + builder.Append("Containerize : "); // tool name as the origin + builder.Append(category); + builder.Append(' '); + builder.Append(code); + builder.Append(" : "); + builder.Append(text); + + return builder.ToString(); + } + + private static string CreateFromResourceWithCode(string category, string resourceName, params object?[] args) + { + string textWithCode = Resource.FormatString(resourceName, args); + + StringBuilder builder = new(); + + builder.Append("Containerize : "); // tool name as the origin + builder.Append(category); + builder.Append(' '); + builder.Append(textWithCode); + + return builder.ToString(); + } +} diff --git a/src/Containers/Microsoft.NET.Build.Containers/DigestUtils.cs b/src/Containers/Microsoft.NET.Build.Containers/DigestUtils.cs index c0c7e16e6793..a41b9a6faa44 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/DigestUtils.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/DigestUtils.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Security.Cryptography; +using System.Text; namespace Microsoft.NET.Build.Containers; @@ -17,16 +18,6 @@ internal sealed class DigestUtils /// internal static string GetDigestFromSha(string sha) => $"sha256:{sha}"; - internal static string GetShaFromDigest(string digest) - { - if (!digest.StartsWith("sha256:", StringComparison.OrdinalIgnoreCase)) - { - throw new ArgumentException($"Invalid digest '{digest}'. Digest must start with 'sha256:'."); - } - - return digest.Substring("sha256:".Length); - } - /// /// Gets the SHA of . /// @@ -35,6 +26,6 @@ internal static string GetSha(string str) Span hash = stackalloc byte[SHA256.HashSizeInBytes]; SHA256.HashData(Encoding.UTF8.GetBytes(str), hash); - return Convert.ToHexStringLower(hash); + return Convert.ToHexString(hash).ToLowerInvariant(); } } diff --git a/src/Containers/Microsoft.NET.Build.Containers/Globals.cs b/src/Containers/Microsoft.NET.Build.Containers/Globals.cs index 468ce9978790..4469e747683a 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Globals.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/Globals.cs @@ -3,6 +3,8 @@ using System.Runtime.CompilerServices; +using System.Resources; + [assembly: InternalsVisibleTo("containerize, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")] [assembly: InternalsVisibleTo("Microsoft.NET.Build.Containers.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")] [assembly: InternalsVisibleTo("Microsoft.NET.Build.Containers.IntegrationTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")] diff --git a/src/Containers/Microsoft.NET.Build.Containers/ImageBuilder.cs b/src/Containers/Microsoft.NET.Build.Containers/ImageBuilder.cs index e97a8151a897..e6e24be685b6 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/ImageBuilder.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/ImageBuilder.cs @@ -2,10 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Text; -using System.Text.RegularExpressions; -using System.Text.Json; -using Microsoft.Extensions.Logging; -using Microsoft.NET.Build.Containers.Resources; namespace Microsoft.NET.Build.Containers; @@ -14,34 +10,15 @@ namespace Microsoft.NET.Build.Containers; /// internal sealed class ImageBuilder { - // a snapshot of the manifest that this builder is based on - private readonly ManifestV2 _baseImageManifest; - - // the mutable internal manifest that we're building by modifying the base and applying customizations private readonly ManifestV2 _manifest; private readonly ImageConfig _baseImageConfig; - private readonly ILogger _logger; - - /// - /// This is a parser for ASPNETCORE_URLS based on https://github.com/dotnet/aspnetcore/blob/main/src/Http/Http/src/BindingAddress.cs - /// We can cut corners a bit here because we really only care about ports, if they exist. - /// - internal static Regex aspnetPortRegex = new(@"(?\w+)://(?([*+]|).+):(?\d+)"); public ImageConfig BaseImageConfig => _baseImageConfig; - /// - /// MediaType of the output manifest. By default, this will be the same as the base image manifest. - /// - public string ManifestMediaType { get; set; } - - internal ImageBuilder(ManifestV2 manifest, string manifestMediaType, ImageConfig baseImageConfig, ILogger logger) + internal ImageBuilder(ManifestV2 manifest, ImageConfig baseImageConfig) { - _baseImageManifest = manifest; - _manifest = new ManifestV2() { SchemaVersion = manifest.SchemaVersion, Config = manifest.Config, Layers = new(manifest.Layers), MediaType = manifest.MediaType }; - ManifestMediaType = manifestMediaType; + _manifest = manifest; _baseImageConfig = baseImageConfig; - _logger = logger; } /// @@ -49,18 +26,11 @@ internal ImageBuilder(ManifestV2 manifest, string manifestMediaType, ImageConfig /// public bool IsWindows => _baseImageConfig.IsWindows; - // For tests - internal string ManifestConfigDigest => _manifest.Config.digest; - /// /// Builds the image configuration ready for further processing. /// internal BuiltImage Build() { - // before we build, we need to make sure that any image customizations occur - AssignUserFromEnvironment(); - AssignPortsFromEnvironment(); - string imageJsonStr = _baseImageConfig.BuildConfig(); string imageSha = DigestUtils.GetSha(imageJsonStr); string imageDigest = DigestUtils.GetDigestFromSha(imageSha); @@ -69,21 +39,12 @@ internal BuiltImage Build() ManifestConfig newManifestConfig = _manifest.Config with { digest = imageDigest, - size = imageSize, - mediaType = ManifestMediaType switch - { - SchemaTypes.OciManifestV1 => SchemaTypes.OciImageConfigV1, - SchemaTypes.DockerManifestV2 => SchemaTypes.DockerContainerV1, - _ => SchemaTypes.OciImageConfigV1 // opinion - defaulting to modern here, but really this should never happen - } + size = imageSize }; - ManifestV2 newManifest = new ManifestV2() + ManifestV2 newManifest = _manifest with { - Config = newManifestConfig, - SchemaVersion = _manifest.SchemaVersion, - MediaType = ManifestMediaType, - Layers = _manifest.Layers + Config = newManifestConfig }; return new BuiltImage() @@ -91,10 +52,8 @@ internal BuiltImage Build() Config = imageJsonStr, ImageDigest = imageDigest, ImageSha = imageSha, - Manifest = JsonSerializer.SerializeToNode(newManifest)?.ToJsonString() ?? "", - ManifestDigest = newManifest.GetDigest(), - ManifestMediaType = ManifestMediaType, - Layers = _manifest.Layers + ImageSize = imageSize, + Manifest = newManifest, }; } @@ -107,13 +66,6 @@ internal void AddLayer(Layer l) _baseImageConfig.AddLayer(l); } - internal (string name, string value) AddBaseImageDigestLabel() - { - var label = ("org.opencontainers.image.base.digest", _baseImageManifest.GetDigest()); - AddLabel(label.Item1, label.Item2); - return label; - } - /// /// Adds a label to a base image. /// @@ -142,201 +94,5 @@ internal void AddLayer(Layer l) /// /// Sets the USER for the image. /// - internal void SetUser(string user, bool isExplicitUserInteraction = true) => _baseImageConfig.SetUser(user, isExplicitUserInteraction); - - internal static (string[] entrypoint, string[] cmd) DetermineEntrypointAndCmd( - string[] entrypoint, - string[] entrypointArgs, - string[] cmd, - string[] appCommand, - string[] appCommandArgs, - string appCommandInstruction, - string[]? baseImageEntrypoint, - Action logWarning, - Action logError) - { - bool setsEntrypoint = entrypoint.Length > 0 || entrypointArgs.Length > 0; - bool setsCmd = cmd.Length > 0; - - baseImageEntrypoint ??= Array.Empty(); - // Some (Microsoft) base images set 'dotnet' as the ENTRYPOINT. We mustn't use it. - if (baseImageEntrypoint.Length == 1 && (baseImageEntrypoint[0] == "dotnet" || baseImageEntrypoint[0] == "/usr/bin/dotnet")) - { - baseImageEntrypoint = Array.Empty(); - } - - if (string.IsNullOrEmpty(appCommandInstruction)) - { - if (setsEntrypoint) - { - // Backwards-compatibility: before 'AppCommand'/'Cmd' was added, only 'Entrypoint' was available. - if (!setsCmd && appCommandArgs.Length == 0 && entrypoint.Length == 0) - { - // Copy over the values for starting the application from AppCommand. - entrypoint = appCommand; - appCommand = Array.Empty(); - - // Use EntrypointArgs as cmd. - cmd = entrypointArgs; - entrypointArgs = Array.Empty(); - - if (entrypointArgs.Length > 0) - { - // Log warning: Instead of ContainerEntrypointArgs, use ContainerAppCommandArgs for arguments that must always be set, or ContainerDefaultArgs for default arguments that the user override when creating the container. - logWarning(nameof(Strings.EntrypointArgsSetPreferAppCommandArgs)); - } - - appCommandInstruction = KnownAppCommandInstructions.None; - } - else - { - // There's an Entrypoint. Use DefaultArgs for the AppCommand. - appCommandInstruction = KnownAppCommandInstructions.DefaultArgs; - } - } - else - { - // Default to use an Entrypoint. - // If the base image defines an ENTRYPOINT, print a warning. - if (baseImageEntrypoint.Length > 0) - { - logWarning(nameof(Strings.BaseEntrypointOverwritten)); - } - appCommandInstruction = KnownAppCommandInstructions.Entrypoint; - } - } - - if (entrypointArgs.Length > 0 && entrypoint.Length == 0) - { - logError(nameof(Strings.EntrypointArgsSetNoEntrypoint), null); - return (Array.Empty(), Array.Empty()); - } - - if (appCommandArgs.Length > 0 && appCommand.Length == 0) - { - logError(nameof(Strings.AppCommandArgsSetNoAppCommand), null); - return (Array.Empty(), Array.Empty()); - } - - switch (appCommandInstruction) - { - case KnownAppCommandInstructions.None: - if (appCommand.Length > 0 || appCommandArgs.Length > 0) - { - logError(nameof(Strings.AppCommandSetNotUsed), appCommandInstruction); - return (Array.Empty(), Array.Empty()); - } - break; - case KnownAppCommandInstructions.DefaultArgs: - cmd = appCommand.Concat(appCommandArgs).Concat(cmd).ToArray(); - break; - case KnownAppCommandInstructions.Entrypoint: - if (setsEntrypoint) - { - logError(nameof(Strings.EntrypointConflictAppCommand), appCommandInstruction); - return (Array.Empty(), Array.Empty()); - } - entrypoint = appCommand; - entrypointArgs = appCommandArgs; - break; - default: - throw new NotSupportedException( - Resource.FormatString( - nameof(Strings.UnknownAppCommandInstruction), - appCommandInstruction, - string.Join(",", KnownAppCommandInstructions.SupportedAppCommandInstructions))); - } - - return (entrypoint.Length > 0 ? entrypoint.Concat(entrypointArgs).ToArray() : baseImageEntrypoint, cmd); - } - - /// - /// The APP_UID environment variable is a convention used to set the user in a data-driven manner. we should respect it if it's present. - /// - internal void AssignUserFromEnvironment() - { - // it's a common convention to apply custom users with the APP_UID convention - we check and apply that here - if (_baseImageConfig.EnvironmentVariables.TryGetValue(EnvironmentVariables.APP_UID, out string? appUid)) - { - _logger.LogTrace("Setting user from APP_UID environment variable"); - SetUser(appUid, isExplicitUserInteraction: false); - } - } - - /// - /// ASP.NET can have urls/ports set via three environment variables - if we see any of them we should create ExposedPorts for them - /// to ensure tooling can automatically create port mappings. - /// - internal void AssignPortsFromEnvironment() - { - // asp.net images control port bindings via three environment variables. we should check for those variables and ensure that ports are created for them. - // precendence is captured at https://github.com/dotnet/aspnetcore/blob/f49c1c7f7467c184ffb630086afac447772096c6/src/Hosting/Hosting/src/GenericHost/GenericWebHostService.cs#L68-L119 - // ASPNETCORE_URLS is the most specific and is the only one used if present, followed by ASPNETCORE_HTTPS_PORT and ASPNETCORE_HTTP_PORT together - - // https://learn.microsoft.com//aspnet/core/fundamentals/host/web-host?view=aspnetcore-8.0#server-urls - the format of ASPNETCORE_URLS has been stable for many years now - if (_baseImageConfig.EnvironmentVariables.TryGetValue(EnvironmentVariables.ASPNETCORE_URLS, out string? urls)) - { - foreach (var url in Split(urls)) - { - _logger.LogTrace("Setting ports from ASPNETCORE_URLS environment variable"); - var match = aspnetPortRegex.Match(url); - if (match.Success && int.TryParse(match.Groups["port"].Value, out int port)) - { - _logger.LogTrace("Added port {port}", port); - ExposePort(port, PortType.tcp); - } - } - return; // we're done here - ASPNETCORE_URLS is the most specific and overrides the other two - } - - // port-specific - // https://learn.microsoft.com/aspnet/core/fundamentals/servers/kestrel/endpoints?view=aspnetcore-8.0#specify-ports-only - new for .NET 8 - allows just changing port(s) easily - if (_baseImageConfig.EnvironmentVariables.TryGetValue(EnvironmentVariables.ASPNETCORE_HTTP_PORTS, out string? httpPorts)) - { - _logger.LogTrace("Setting ports from ASPNETCORE_HTTP_PORTS environment variable"); - foreach (var port in Split(httpPorts)) - { - if (int.TryParse(port, out int parsedPort)) - { - _logger.LogTrace("Added port {port}", parsedPort); - ExposePort(parsedPort, PortType.tcp); - } - else - { - _logger.LogTrace("Skipped port {port} because it could not be parsed as an integer", port); - } - } - } - - if (_baseImageConfig.EnvironmentVariables.TryGetValue(EnvironmentVariables.ASPNETCORE_HTTPS_PORTS, out string? httpsPorts)) - { - _logger.LogTrace("Setting ports from ASPNETCORE_HTTPS_PORTS environment variable"); - foreach (var port in Split(httpsPorts)) - { - if (int.TryParse(port, out int parsedPort)) - { - _logger.LogTrace("Added port {port}", parsedPort); - ExposePort(parsedPort, PortType.tcp); - } - else - { - _logger.LogTrace("Skipped port {port} because it could not be parsed as an integer", port); - } - } - } - - static string[] Split(string input) - { - return input.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); - } - } - - internal static class EnvironmentVariables - { - public static readonly string APP_UID = nameof(APP_UID); - public static readonly string ASPNETCORE_URLS = nameof(ASPNETCORE_URLS); - public static readonly string ASPNETCORE_HTTP_PORTS = nameof(ASPNETCORE_HTTP_PORTS); - public static readonly string ASPNETCORE_HTTPS_PORTS = nameof(ASPNETCORE_HTTPS_PORTS); - } - + internal void SetUser(string user) => _baseImageConfig.SetUser(user); } diff --git a/src/Containers/Microsoft.NET.Build.Containers/ImageConfig.cs b/src/Containers/Microsoft.NET.Build.Containers/ImageConfig.cs index d16563c052db..530bcfe1392c 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/ImageConfig.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/ImageConfig.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Collections.ObjectModel; using System.Text.Json; using System.Text.Json.Nodes; @@ -20,7 +19,6 @@ internal sealed class ImageConfig private string[]? _newEntrypoint; private string[]? _newCmd; private string? _user; - private bool _userHasBeenExplicitlySet; /// /// Models the file system of the image. Typically has a key 'type' with value 'layers' and a key 'diff_ids' with a list of layer digests. @@ -35,9 +33,6 @@ internal sealed class ImageConfig /// public bool IsWindows => "windows".Equals(_os, StringComparison.OrdinalIgnoreCase); - public ReadOnlyDictionary EnvironmentVariables => _environmentVariables.AsReadOnly(); - public HashSet Ports => _exposedPorts; - internal ImageConfig(string imageConfigJson) : this(JsonNode.Parse(imageConfigJson)!) { } @@ -85,7 +80,7 @@ internal string BuildConfig() { newConfig["Labels"] = CreateLabelMap(); } - if (_environmentVariables.Count != 0) + if (_environmentVariables.Any()) { newConfig["Env"] = CreateEnvironmentVariablesMapping(); } @@ -112,7 +107,7 @@ internal string BuildConfig() // These fields aren't (yet) supported by the task layer, but we should // preserve them if they're already set in the base image. - foreach (string propertyName in new[] { "Volumes", "StopSignal" }) + foreach (string propertyName in new [] { "Volumes", "StopSignal" }) { if (_config["config"]?[propertyName] is JsonNode propertyValue) { @@ -127,7 +122,7 @@ internal string BuildConfig() // The number of (non empty) history items must match the number of layers in the image. // Some registries like JFrog Artifactory have there a strict validation rule (see sdk-container-builds#382). int numberOfLayers = _rootFsLayers.Count; - int numberOfNonEmptyLayerHistoryEntries = _history.Count(h => h.empty_layer is null or false); + int numberOfNonEmptyLayerHistoryEntries = _history.Count(h =>h.empty_layer is null or false); int missingHistoryEntries = numberOfLayers - numberOfNonEmptyLayerHistoryEntries; HistoryEntry customHistoryEntry = new(created: DateTime.UtcNow, author: ".NET SDK", created_by: $".NET SDK Container Tooling, version {Constants.Version}"); @@ -168,7 +163,7 @@ private JsonObject CreateHistory(HistoryEntry h) { history["comment"] = h.comment; } - if (h.created is { } date) + if (h.created is {} date) { history["created"] = RFC3339Format(date); } @@ -217,16 +212,7 @@ internal void AddLayer(Layer l) _rootFsLayers.Add(l.Descriptor.UncompressedDigest!); } - internal void SetUser(string user, bool isUserInteraction = false) { - // we don't let automatic/inferred user settings overwrite an explicit user request - if (_userHasBeenExplicitlySet && !isUserInteraction) - { - return; - } - - _user = user; - _userHasBeenExplicitlySet = isUserInteraction; - } + internal void SetUser(string user) => _user = user; private HashSet GetExposedPorts() { diff --git a/src/Containers/Microsoft.NET.Build.Containers/ImageReference.cs b/src/Containers/Microsoft.NET.Build.Containers/ImageReference.cs new file mode 100644 index 000000000000..4989625b4ecb --- /dev/null +++ b/src/Containers/Microsoft.NET.Build.Containers/ImageReference.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.NET.Build.Containers; + +/// +/// Represents a reference to a Docker image. A reference is made of a registry, a repository (aka the image name) and a tag. +/// +internal readonly record struct ImageReference(Registry? Registry, string Repository, string Tag) { + public override string ToString() + { + if (Registry is {} reg) { + return $"{reg.RegistryName}/{Repository}:{Tag}"; + } else { + return RepositoryAndTag; + } + } + + /// + /// Returns the repository and tag as a formatted string. Used in cases + /// + public readonly string RepositoryAndTag => $"{Repository}:{Tag}"; +} diff --git a/src/Containers/Microsoft.NET.Build.Containers/KnownAppCommandInstructions.cs b/src/Containers/Microsoft.NET.Build.Containers/KnownAppCommandInstructions.cs index 6ade2a65ff9a..b9420e8e447d 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/KnownAppCommandInstructions.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/KnownAppCommandInstructions.cs @@ -9,5 +9,5 @@ internal static class KnownAppCommandInstructions public const string Entrypoint = nameof(Entrypoint); public const string None = nameof(None); - public static readonly string[] SupportedAppCommandInstructions = new[] { Entrypoint, DefaultArgs, None }; + public static readonly string[] SupportedAppCommandInstructions = new [] { Entrypoint, DefaultArgs, None }; } diff --git a/src/Containers/Microsoft.NET.Build.Containers/KnownStrings.cs b/src/Containers/Microsoft.NET.Build.Containers/KnownStrings.cs index f806d0a53598..bc68b412d7e4 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/KnownStrings.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/KnownStrings.cs @@ -8,8 +8,6 @@ internal static class KnownStrings public static class Properties { public static readonly string ContainerBaseImage = nameof(ContainerBaseImage); - public static readonly string ContainerFamily = nameof(ContainerFamily); - public static readonly string _ContainerBaseImageTag = nameof(_ContainerBaseImageTag); public static readonly string ContainerRegistry = nameof(ContainerRegistry); /// Note that this is deprecated in favor of public static readonly string ContainerImageName = nameof(ContainerImageName); @@ -18,53 +16,31 @@ public static class Properties public static readonly string ContainerImageTags = nameof(ContainerImageTags); public static readonly string ContainerWorkingDirectory = nameof(ContainerWorkingDirectory); public static readonly string ContainerEntrypoint = nameof(ContainerEntrypoint); - public static readonly string ContainerAppCommand = nameof(ContainerAppCommand); public static readonly string UseAppHost = nameof(UseAppHost); public static readonly string ContainerLabel = nameof(ContainerLabel); public static readonly string SelfContained = nameof(SelfContained); public static readonly string ContainerPort = nameof(ContainerPort); public static readonly string ContainerEnvironmentVariable = nameof(ContainerEnvironmentVariable); - public static readonly string ComputeContainerBaseImage = nameof(ComputeContainerBaseImage); public static readonly string ComputeContainerConfig = nameof(ComputeContainerConfig); - public static readonly string _ComputeContainerExecutionArgs = nameof(_ComputeContainerExecutionArgs); public static readonly string AssemblyName = nameof(AssemblyName); public static readonly string ContainerBaseRegistry = nameof(ContainerBaseRegistry); public static readonly string ContainerBaseName = nameof(ContainerBaseName); public static readonly string ContainerBaseTag = nameof(ContainerBaseTag); - public static readonly string ContainerBaseDigest = nameof(ContainerBaseDigest); public static readonly string ContainerGenerateLabels = nameof(ContainerGenerateLabels); public static readonly string ContainerRuntimeIdentifier = nameof(ContainerRuntimeIdentifier); - public static readonly string RuntimeIdentifier = nameof(RuntimeIdentifier); - public static readonly string PublishAot = nameof(PublishAot); - public static readonly string PublishTrimmed = nameof(PublishTrimmed); - public static readonly string PublishSelfContained = nameof(PublishSelfContained); - public static readonly string InvariantGlobalization = nameof(InvariantGlobalization); - public static readonly string InvariantTimezone = nameof(InvariantTimezone); - } - - public static class Items - { - public static readonly string FrameworkReference = nameof(FrameworkReference); } public static class ErrorCodes { - // current version doesn't support containerization public static readonly string CONTAINER002 = nameof(CONTAINER002); - // containerimagename rename public static readonly string CONTAINER003 = nameof(CONTAINER003); - // generic http error - public static readonly string CONTAINER004 = nameof(CONTAINER004); - // don't use the containers package - public static readonly string CONTAINER005 = nameof(CONTAINER005); public static readonly string CONTAINER1011 = nameof(CONTAINER1011); public static readonly string CONTAINER1012 = nameof(CONTAINER1012); public static readonly string CONTAINER1013 = nameof(CONTAINER1013); - public static readonly string CONTAINER2005 = nameof(CONTAINER2005); public static readonly string CONTAINER2007 = nameof(CONTAINER2007); public static readonly string CONTAINER2008 = nameof(CONTAINER2008); public static readonly string CONTAINER2009 = nameof(CONTAINER2009); diff --git a/src/Containers/Microsoft.NET.Build.Containers/Layer.cs b/src/Containers/Microsoft.NET.Build.Containers/Layer.cs index 1cb57628f390..4a39106e37fc 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Layer.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/Layer.cs @@ -3,18 +3,17 @@ using System.Diagnostics; using System.Formats.Tar; +using System.Globalization; using System.IO.Compression; -using System.IO.Enumeration; using System.Security.Cryptography; -using Microsoft.NET.Build.Containers.Resources; +using System.Text; +using System.IO.Enumeration; 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, - // so that we can author Windows layers successfully on non-Windows hosts. - // + // 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( @@ -52,7 +51,7 @@ public static Layer FromDescriptor(Descriptor descriptor) return new(ContentStore.PathForDescriptor(descriptor), descriptor); } - public static Layer FromDirectory(string directory, string containerPath, bool isWindowsLayer, string manifestMediaType, int? userId = null) + public static Layer FromDirectory(string directory, string containerPath, bool isWindowsLayer) { long fileSize; Span hash = stackalloc byte[SHA256.HashSizeInBytes]; @@ -103,7 +102,7 @@ public static Layer FromDirectory(string directory, string containerPath, bool i } // Write an entry for the application directory. - WriteTarEntryForFile(writer, new DirectoryInfo(directory), containerPath, entryAttributes, isWindowsLayer ? null : userId); + WriteTarEntryForFile(writer, new DirectoryInfo(directory), containerPath, entryAttributes); // Write entries for the application directory contents. var fileList = new FileSystemEnumerable<(FileSystemInfo file, string containerPath)>( @@ -121,12 +120,11 @@ public static Layer FromDirectory(string directory, string containerPath, bool i }, options: new EnumerationOptions() { - AttributesToSkip = FileAttributes.System, // Include hidden files RecurseSubdirectories = true }); foreach (var item in fileList) { - WriteTarEntryForFile(writer, item.file, item.containerPath, entryAttributes, isWindowsLayer ? null : userId); + WriteTarEntryForFile(writer, item.file, item.containerPath, entryAttributes); } // Windows layers need a Hives folder, we do not need to create any Registry Hive deltas inside @@ -150,36 +148,27 @@ public static Layer FromDirectory(string directory, string containerPath, bool i 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> entryAttributes, int? userId) + static void WriteTarEntryForFile(TarWriter writer, FileSystemInfo file, string containerPath, IEnumerable> entryAttributes) { UnixFileMode mode = DetermineFileMode(file); - PaxTarEntry entry; if (file is FileInfo) { - var fileStream = File.OpenRead(file.FullName); - entry = new(TarEntryType.RegularFile, containerPath, entryAttributes) + using var fileStream = File.OpenRead(file.FullName); + PaxTarEntry entry = new(TarEntryType.RegularFile, containerPath, entryAttributes) { - DataStream = fileStream, + Mode = mode, + DataStream = fileStream }; + writer.WriteEntry(entry); } else { - entry = new(TarEntryType.Directory, containerPath, entryAttributes); - } - - entry.Mode = mode; - if (userId is int uid) - { - entry.Uid = uid; - } - - writer.WriteEntry(entry); - - if (entry.DataStream is not null) - { - // no longer relying on the `using` of the FileStream, so need to do it manually - entry.DataStream.Dispose(); + PaxTarEntry entry = new(TarEntryType.Directory, containerPath, entryAttributes) + { + Mode = mode + }; + writer.WriteEntry(entry); } static UnixFileMode DetermineFileMode(FileSystemInfo file) @@ -196,20 +185,12 @@ static UnixFileMode DetermineFileMode(FileSystemInfo file) } } - 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)) - }; + string contentHash = Convert.ToHexString(hash).ToLowerInvariant(); + string uncompressedContentHash = Convert.ToHexString(uncompressedHash).ToLowerInvariant(); Descriptor descriptor = new() { - MediaType = layerMediaType, + MediaType = "application/vnd.docker.image.rootfs.diff.tar.gzip", // TODO: configurable? gzip always? Size = fileSize, Digest = $"sha256:{contentHash}", UncompressedDigest = $"sha256:{uncompressedContentHash}", @@ -226,7 +207,7 @@ static UnixFileMode DetermineFileMode(FileSystemInfo file) internal virtual Stream OpenBackingFile() => File.OpenRead(BackingFile); - private static readonly char[] PathSeparators = new char[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }; + private readonly static char[] PathSeparators = new char[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }; /// /// A stream capable of computing the hash digest of raw uncompressed data while also compressing it. diff --git a/src/Containers/Microsoft.NET.Build.Containers/LocalDaemons/DockerCli.cs b/src/Containers/Microsoft.NET.Build.Containers/LocalDaemons/DockerCli.cs index ebc8064f8e41..3d96341c8868 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/LocalDaemons/DockerCli.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/LocalDaemons/DockerCli.cs @@ -1,25 +1,20 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.Diagnostics; -#if NET using System.Formats.Tar; -#endif +using System.Text; using System.Text.Json; using System.Text.Json.Nodes; -#if NET using Microsoft.DotNet.Cli.Utils; -#endif using Microsoft.Extensions.Logging; using Microsoft.NET.Build.Containers.Resources; namespace Microsoft.NET.Build.Containers; // Wraps the 'docker'/'podman' cli. -internal sealed class DockerCli -#if NET -: ILocalRegistry -#endif +internal sealed class DockerCli : ILocalRegistry { public const string DockerCommand = "docker"; public const string PodmanCommand = "podman"; @@ -27,15 +22,9 @@ internal sealed class DockerCli private const string Commands = $"{DockerCommand}/{PodmanCommand}"; private readonly ILogger _logger; - private string? _command; + private string? _commandPath; -#if NET - private string? _fullCommandPath; -#endif - - private const string _blobsPath = "blobs/sha256"; - - public DockerCli(string? command, ILoggerFactory loggerFactory) + public DockerCli(string? command, ILoggerFactory logger) { if (!(command == null || command == PodmanCommand || @@ -44,77 +33,39 @@ public DockerCli(string? command, ILoggerFactory loggerFactory) throw new ArgumentException($"{command} is an unknown command."); } - _command = command; - _logger = loggerFactory.CreateLogger(); + this._commandPath = command; + this._logger = logger.CreateLogger(); } public DockerCli(ILoggerFactory loggerFactory) : this(null, loggerFactory) { } - private static string FindFullPathFromPath(string command) + public async Task LoadAsync(BuiltImage image, ImageReference sourceReference, ImageReference destinationReference, CancellationToken cancellationToken) { - foreach (string directory in (Environment.GetEnvironmentVariable("PATH") ?? string.Empty).Split(Path.PathSeparator)) - { - string fullPath = Path.Combine(directory, RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? $"{command}.exe" : command); - if (File.Exists(fullPath)) - { - return fullPath; - } - } - - return command; - } - -#if NET - private async ValueTask FindFullCommandPath(CancellationToken cancellationToken) - { - if (_fullCommandPath != null) - { - return _fullCommandPath; - } + cancellationToken.ThrowIfCancellationRequested(); - string? command = await GetCommandAsync(cancellationToken); - if (command is null) + string? commandPath = await GetCommandPathAsync(cancellationToken); + if (commandPath is null) { - throw new NotImplementedException(Resource.FormatString(Strings.ContainerRuntimeProcessCreationFailed, Commands)); + throw new NotImplementedException(Resource.FormatString(Strings.DockerProcessCreationFailed, Commands)); } - _fullCommandPath = FindFullPathFromPath(command); - - return _fullCommandPath; - } + // call `docker load` and get it ready to receive input + ProcessStartInfo loadInfo = new(commandPath, $"load"); + loadInfo.RedirectStandardInput = true; + loadInfo.RedirectStandardOutput = true; + loadInfo.RedirectStandardError = true; - private async Task LoadAsync( - T image, - SourceImageReference sourceReference, - DestinationImageReference destinationReference, - Func writeStreamFunc, - CancellationToken cancellationToken, - bool checkContainerdStore = false) - { - cancellationToken.ThrowIfCancellationRequested(); + using Process? loadProcess = Process.Start(loadInfo); - if (checkContainerdStore && !IsContainerdStoreEnabledForDocker()) + if (loadProcess is null) { - throw new DockerLoadException(Strings.ImageLoadFailed_ContainerdStoreDisabled); + throw new NotImplementedException(Resource.FormatString(Strings.DockerProcessCreationFailed, commandPath)); } - string commandPath = await FindFullCommandPath(cancellationToken); - - // call `docker load` and get it ready to receive input - ProcessStartInfo loadInfo = new(commandPath, $"load") - { - RedirectStandardInput = true, - RedirectStandardOutput = true, - RedirectStandardError = true - }; + // Create new stream tarball - using Process? loadProcess = Process.Start(loadInfo) ?? - throw new NotImplementedException(Resource.FormatString(Strings.ContainerRuntimeProcessCreationFailed, commandPath)); - - // Call the delegate to write the image to the stream - await writeStreamFunc(image, sourceReference, destinationReference, loadProcess.StandardInput.BaseStream, cancellationToken) - .ConfigureAwait(false); + await WriteImageToStreamAsync(image, sourceReference, destinationReference, loadProcess.StandardInput.BaseStream, cancellationToken).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); @@ -130,18 +81,11 @@ await writeStreamFunc(image, sourceReference, destinationReference, loadProcess. } } - public async Task LoadAsync(BuiltImage image, SourceImageReference sourceReference, DestinationImageReference destinationReference, CancellationToken cancellationToken) - // For loading to the local registry, we use the Docker format. Two reasons: one - compatibility with previous behavior before oci formatted publishing was available, two - Podman cannot load multi tag oci image tarball. - => await LoadAsync(image, sourceReference, destinationReference, WriteDockerImageToStreamAsync, cancellationToken); - - public async Task LoadAsync(MultiArchImage multiArchImage, SourceImageReference sourceReference, DestinationImageReference destinationReference, CancellationToken cancellationToken) - => await LoadAsync(multiArchImage, sourceReference, destinationReference, WriteMultiArchOciImageToStreamAsync, cancellationToken, checkContainerdStore: true); - public async Task IsAvailableAsync(CancellationToken cancellationToken) { - bool commandPathWasUnknown = _command is null; // avoid running the version command twice. - string? command = await GetCommandAsync(cancellationToken); - if (command is null) + bool commandPathWasUnknown = this._commandPath is null; // avoid running the version command twice. + string? commandPath = await GetCommandPathAsync(cancellationToken); + if (commandPath is null) { _logger.LogError($"Cannot find {Commands} executable."); return false; @@ -149,32 +93,32 @@ public async Task IsAvailableAsync(CancellationToken cancellationToken) try { - switch (command) + switch (commandPath) { case DockerCommand: + { + JsonDocument config = GetConfig(); + + if (!config.RootElement.TryGetProperty("ServerErrors", out JsonElement errorProperty)) + { + return true; + } + else if (errorProperty.ValueKind == JsonValueKind.Array && errorProperty.GetArrayLength() == 0) + { + return true; + } + else { - JsonDocument config = GetDockerConfig(); - - if (!config.RootElement.TryGetProperty("ServerErrors", out JsonElement errorProperty)) - { - return true; - } - else if (errorProperty.ValueKind == JsonValueKind.Array && errorProperty.GetArrayLength() == 0) - { - return true; - } - else - { - // we have errors, turn them into a string and log them - string messages = string.Join(Environment.NewLine, errorProperty.EnumerateArray()); - _logger.LogError($"The daemon server reported errors: {messages}"); - return false; - } + // we have errors, turn them into a string and log them + string messages = string.Join(Environment.NewLine, errorProperty.EnumerateArray()); + _logger.LogError($"The daemon server reported errors: {messages}"); + return false; } + } case PodmanCommand: return commandPathWasUnknown || await TryRunVersionCommandAsync(PodmanCommand, cancellationToken); default: - throw new NotImplementedException($"{command} is an unknown command."); + throw new NotImplementedException($"{commandPath} is an unknown command."); } } catch (Exception ex) @@ -190,19 +134,18 @@ public bool IsAvailable() => IsAvailableAsync(default).GetAwaiter().GetResult(); public string? GetCommand() - => GetCommandAsync(default).GetAwaiter().GetResult(); + => GetCommandPathAsync(default).GetAwaiter().GetResult(); /// /// Gets docker configuration. /// /// when , the method is executed synchronously. /// when failed to retrieve docker configuration. - internal static JsonDocument GetDockerConfig() + internal static JsonDocument GetConfig() { - string dockerPath = FindFullPathFromPath("docker"); Process proc = new() { - StartInfo = new ProcessStartInfo(dockerPath, "info --format=\"{{json .}}\"") + StartInfo = new ProcessStartInfo("docker", "info --format=\"{{json .}}\"") }; try @@ -212,6 +155,7 @@ internal static JsonDocument GetDockerConfig() dockerCommand.CaptureStdErr(); CommandResult dockerCommandResult = dockerCommand.Execute(); + if (dockerCommandResult.ExitCode != 0) { throw new DockerLoadException(Resource.FormatString( @@ -221,132 +165,39 @@ internal static JsonDocument GetDockerConfig() dockerCommandResult.StdErr)); } - return JsonDocument.Parse(dockerCommandResult.StdOut ?? string.Empty); - } - catch (Exception e) when (e is not DockerLoadException) - { - throw new DockerLoadException(Resource.FormatString(nameof(Strings.DockerInfoFailed_Ex), e.Message)); - } - } - /// - /// Checks if the registry is marked as insecure in the docker/podman config. - /// - /// - /// - public static bool IsInsecureRegistry(string registryDomain) - { - try - { - //check the docker config to see if the registry is marked as insecure - var rootElement = GetDockerConfig().RootElement; + return JsonDocument.Parse(dockerCommandResult.StdOut); - //for docker - if (rootElement.TryGetProperty("RegistryConfig", out var registryConfig) && registryConfig.ValueKind == JsonValueKind.Object) - { - if (registryConfig.TryGetProperty("IndexConfigs", out var indexConfigs) && indexConfigs.ValueKind == JsonValueKind.Object) - { - foreach (var property in indexConfigs.EnumerateObject()) - { - if (property.Value.ValueKind == JsonValueKind.Object - && property.Value.TryGetProperty("Secure", out var secure) - && !secure.GetBoolean() - && property.Name.Equals(registryDomain, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } - } - } - //for podman - if (rootElement.TryGetProperty("registries", out var registries) && registries.ValueKind == JsonValueKind.Object) - { - foreach (var property in registries.EnumerateObject()) - { - if (property.Value.ValueKind == JsonValueKind.Object - && property.Value.TryGetProperty("Insecure", out var insecure) - && insecure.GetBoolean() - && property.Name.Equals(registryDomain, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } - } - return false; } - catch (DockerLoadException) + catch (Exception e) when (e is not DockerLoadException) { - //if docker load fails, we can't check the config so we assume the registry is secure - return false; + throw new DockerLoadException(Resource.FormatString(nameof(Strings.DockerInfoFailed_Ex), e.Message)); } } -#endif private static void Proc_OutputDataReceived(object sender, DataReceivedEventArgs e) => throw new NotImplementedException(); -#if NET - public static async Task WriteImageToStreamAsync(BuiltImage image, SourceImageReference sourceReference, DestinationImageReference destinationReference, Stream imageStream, CancellationToken cancellationToken) - { - if (image.ManifestMediaType == SchemaTypes.DockerManifestV2) - { - await WriteDockerImageToStreamAsync(image, sourceReference, destinationReference, imageStream, cancellationToken); - } - else if (image.ManifestMediaType == SchemaTypes.OciManifestV1) - { - await WriteOciImageToStreamAsync(image, sourceReference, destinationReference, imageStream, cancellationToken); - } - else - { - throw new ArgumentException(Resource.FormatString(nameof(Strings.UnsupportedMediaTypeForTarball), image.ManifestMediaType)); - } - } - - private static async Task WriteDockerImageToStreamAsync( - BuiltImage image, - SourceImageReference sourceReference, - DestinationImageReference destinationReference, - Stream imageStream, - CancellationToken cancellationToken) + private static async Task WriteImageToStreamAsync(BuiltImage image, ImageReference sourceReference, ImageReference destinationReference, Stream imageStream, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); using TarWriter writer = new(imageStream, TarEntryFormat.Pax, leaveOpen: true); - // Feed each layer tarball into the stream - JsonArray layerTarballPaths = new(); - await WriteImageLayers(writer, image, sourceReference, d => $"{d.Substring("sha256:".Length)}/layer.tar", cancellationToken, layerTarballPaths) - .ConfigureAwait(false); - - string configTarballPath = $"{image.ImageSha!}.json"; - await WriteImageConfig(writer, image, configTarballPath, cancellationToken) - .ConfigureAwait(false); - // Add manifest - await WriteManifestForDockerImage(writer, destinationReference, configTarballPath, layerTarballPaths, cancellationToken) - .ConfigureAwait(false); - } - - private static async Task WriteImageLayers( - TarWriter writer, - BuiltImage image, - SourceImageReference sourceReference, - Func layerPathFunc, - CancellationToken cancellationToken, - JsonArray? layerTarballPaths = null) - { - cancellationToken.ThrowIfCancellationRequested(); + // Feed each layer tarball into the stream + JsonArray layerTarballPaths = new JsonArray(); foreach (var d in image.LayerDescriptors) { if (sourceReference.Registry is { } registry) { cancellationToken.ThrowIfCancellationRequested(); - string localPath = await registry.DownloadBlobAsync(sourceReference.Repository, d, cancellationToken).ConfigureAwait(false); + string localPath = await registry.DownloadBlobAsync(sourceReference.Repository, d, cancellationToken).ConfigureAwait(false); ; // Stuff that (uncompressed) tarball into the image tar stream // TODO uncompress!! - string layerTarballPath = layerPathFunc(d.Digest); + string layerTarballPath = $"{d.Digest.Substring("sha256:".Length)}/layer.tar"; await writer.WriteEntryAsync(localPath, layerTarballPath, cancellationToken).ConfigureAwait(false); - layerTarballPaths?.Add(layerTarballPath); + layerTarballPaths.Add(layerTarballPath); } else { @@ -356,37 +207,25 @@ private static async Task WriteImageLayers( sourceReference.Registry?.ToString() ?? "")); } } - } - private static async Task WriteImageConfig( - TarWriter writer, - BuiltImage image, - string configPath, - CancellationToken cancellationToken) - { + // add config + string configTarballPath = $"{image.ImageSha}.json"; cancellationToken.ThrowIfCancellationRequested(); - using (MemoryStream configStream = new(Encoding.UTF8.GetBytes(image.Config))) + using (MemoryStream configStream = new MemoryStream(Encoding.UTF8.GetBytes(image.Config))) { - PaxTarEntry configEntry = new(TarEntryType.RegularFile, configPath) + PaxTarEntry configEntry = new(TarEntryType.RegularFile, configTarballPath) { DataStream = configStream }; + await writer.WriteEntryAsync(configEntry, cancellationToken).ConfigureAwait(false); } - } - private static async Task WriteManifestForDockerImage( - TarWriter writer, - DestinationImageReference destinationReference, - string configTarballPath, - JsonArray layerTarballPaths, - CancellationToken cancellationToken) - { - JsonArray tagsNode = new(); - foreach (string tag in destinationReference.Tags) + // Add manifest + JsonArray tagsNode = new() { - tagsNode.Add($"{destinationReference.Repository}:{tag}"); - } + destinationReference.RepositoryAndTag + }; JsonNode manifestNode = new JsonArray(new JsonObject { @@ -396,7 +235,7 @@ private static async Task WriteManifestForDockerImage( }); cancellationToken.ThrowIfCancellationRequested(); - using (MemoryStream manifestStream = new(Encoding.UTF8.GetBytes(manifestNode.ToJsonString()))) + using (MemoryStream manifestStream = new MemoryStream(Encoding.UTF8.GetBytes(manifestNode.ToJsonString()))) { PaxTarEntry manifestEntry = new(TarEntryType.RegularFile, "manifest.json") { @@ -407,252 +246,26 @@ private static async Task WriteManifestForDockerImage( } } - private static async Task WriteOciImageToStreamAsync( - BuiltImage image, - SourceImageReference sourceReference, - DestinationImageReference destinationReference, - Stream imageStream, - CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - - using TarWriter writer = new(imageStream, TarEntryFormat.Pax, leaveOpen: true); - - await WriteOciImageToBlobs(writer, image, sourceReference, cancellationToken) - .ConfigureAwait(false); - - await WriteIndexJsonForOciImage(writer, image, destinationReference, cancellationToken) - .ConfigureAwait(false); - - await WriteOciLayout(writer, cancellationToken) - .ConfigureAwait(false); - } - - private static async Task WriteOciLayout(TarWriter writer, CancellationToken cancellationToken) + private async ValueTask GetCommandPathAsync(CancellationToken cancellationToken) { - cancellationToken.ThrowIfCancellationRequested(); - - string ociLayoutPath = "oci-layout"; - var ociLayoutContent = "{\"imageLayoutVersion\": \"1.0.0\"}"; - using (MemoryStream ociLayoutStream = new MemoryStream(Encoding.UTF8.GetBytes(ociLayoutContent))) + if (_commandPath != null) { - PaxTarEntry layoutEntry = new(TarEntryType.RegularFile, ociLayoutPath) - { - DataStream = ociLayoutStream - }; - await writer.WriteEntryAsync(layoutEntry, cancellationToken).ConfigureAwait(false); - } - } - - private static async Task WriteManifestForOciImage( - TarWriter writer, - BuiltImage image, - CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - - string manifestPath = $"{_blobsPath}/{image.ManifestDigest.Substring("sha256:".Length)}"; - using (MemoryStream manifestStream = new MemoryStream(Encoding.UTF8.GetBytes(image.Manifest))) - { - PaxTarEntry manifestEntry = new(TarEntryType.RegularFile, manifestPath) - { - DataStream = manifestStream - }; - await writer.WriteEntryAsync(manifestEntry, cancellationToken).ConfigureAwait(false); - } - } - - private static async Task WriteIndexJsonForOciImage( - TarWriter writer, - BuiltImage image, - DestinationImageReference destinationReference, - CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - - string indexJson = ImageIndexGenerator.GenerateImageIndexWithAnnotations( - SchemaTypes.OciManifestV1, - image.ManifestDigest, - image.Manifest.Length, - destinationReference.Repository, - destinationReference.Tags); - - using (MemoryStream indexStream = new(Encoding.UTF8.GetBytes(indexJson))) - { - PaxTarEntry indexEntry = new(TarEntryType.RegularFile, "index.json") - { - DataStream = indexStream - }; - await writer.WriteEntryAsync(indexEntry, cancellationToken).ConfigureAwait(false); - } - } - - private static async Task WriteOciImageToBlobs( - TarWriter writer, - BuiltImage image, - SourceImageReference sourceReference, - CancellationToken cancellationToken) - { - await WriteImageLayers(writer, image, sourceReference, d => $"{_blobsPath}/{d.Substring("sha256:".Length)}", cancellationToken) - .ConfigureAwait(false); - - await WriteImageConfig(writer, image, $"{_blobsPath}/{image.ImageSha!}", cancellationToken) - .ConfigureAwait(false); - - await WriteManifestForOciImage(writer, image, cancellationToken) - .ConfigureAwait(false); - } - - public static async Task WriteMultiArchOciImageToStreamAsync( - MultiArchImage multiArchImage, - SourceImageReference sourceReference, - DestinationImageReference destinationReference, - Stream imageStream, - CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - - using TarWriter writer = new(imageStream, TarEntryFormat.Pax, leaveOpen: true); - - foreach (var image in multiArchImage.Images!) - { - await WriteOciImageToBlobs(writer, image, sourceReference, cancellationToken) - .ConfigureAwait(false); - } - - await WriteIndexJsonForMultiArchOciImage(writer, multiArchImage, destinationReference, cancellationToken) - .ConfigureAwait(false); - - await WriteOciLayout(writer, cancellationToken) - .ConfigureAwait(false); - } - - private static async Task WriteIndexJsonForMultiArchOciImage( - TarWriter writer, - MultiArchImage multiArchImage, - DestinationImageReference destinationReference, - CancellationToken cancellationToken) - { - // 1. create manifest list for the blobs - cancellationToken.ThrowIfCancellationRequested(); - - var manifestListDigest = DigestUtils.GetDigest(multiArchImage.ImageIndex); - var manifestListSha = DigestUtils.GetShaFromDigest(manifestListDigest); - var manifestListPath = $"{_blobsPath}/{manifestListSha}"; - - using (MemoryStream indexStream = new(Encoding.UTF8.GetBytes(multiArchImage.ImageIndex))) - { - PaxTarEntry indexEntry = new(TarEntryType.RegularFile, manifestListPath) - { - DataStream = indexStream - }; - await writer.WriteEntryAsync(indexEntry, cancellationToken).ConfigureAwait(false); - } - - // 2. create index.json that points to manifest list in the blobs - cancellationToken.ThrowIfCancellationRequested(); - - string indexJson = ImageIndexGenerator.GenerateImageIndexWithAnnotations( - multiArchImage.ImageIndexMediaType, - manifestListDigest, - multiArchImage.ImageIndex.Length, - destinationReference.Repository, - destinationReference.Tags); - - using (MemoryStream indexStream = new(Encoding.UTF8.GetBytes(indexJson))) - { - PaxTarEntry indexEntry = new(TarEntryType.RegularFile, "index.json") - { - DataStream = indexStream - }; - await writer.WriteEntryAsync(indexEntry, cancellationToken).ConfigureAwait(false); - } - } - - private async ValueTask GetCommandAsync(CancellationToken cancellationToken) - { - if (_command != null) - { - return _command; + return _commandPath; } // Try to find the docker or podman cli. // On systems with podman it's not uncommon for docker to be an alias to podman. - // We have to attempt to locate both binaries and inspect the output of the 'docker' binary if present to determine - // if it is actually podman. - var podmanCommand = TryRunVersionCommandAsync(PodmanCommand, cancellationToken); - var dockerCommand = TryRunVersionCommandAsync(DockerCommand, cancellationToken); - - await Task.WhenAll( - podmanCommand, - dockerCommand - ).ConfigureAwait(false); - - // be explicit with this check so that we don't do the link target check unless it might actually be a solution. - if (dockerCommand.Result && podmanCommand.Result && IsPodmanAlias()) - { - _command = PodmanCommand; - } - else if (dockerCommand.Result) - { - _command = DockerCommand; - } - else if (podmanCommand.Result) - { - _command = PodmanCommand; - } - - return _command; - } - - private static bool IsPodmanAlias() - { - // If both exist we need to check and see if the docker command is actually docker, - // or if it is a podman script in a trenchcoat. - try - { - var dockerinfo = GetDockerConfig().RootElement; - // Docker's info output has a 'DockerRootDir' top-level property string that is a good marker, - // while Podman has a 'host' top-level property object with a 'buildahVersion' subproperty - var hasdockerProperty = - dockerinfo.TryGetProperty("DockerRootDir", out var dockerRootDir) && dockerRootDir.GetString() is not null; - var hasPodmanProperty = dockerinfo.TryGetProperty("host", out var host) && host.TryGetProperty("buildahVersion", out var buildahVersion) && buildahVersion.GetString() is not null; - return !hasdockerProperty && hasPodmanProperty; - } - catch - { - return false; - } - } - - internal static bool IsContainerdStoreEnabledForDocker() - { - try + // We try to find podman first so we can identify those systems to be using podman. + foreach (var command in new[] { PodmanCommand, DockerCommand }) { - // We don't need to check if this is docker, because there is no "DriverStatus" for podman - if (!GetDockerConfig().RootElement.TryGetProperty("DriverStatus", out var driverStatus) || driverStatus.ValueKind != JsonValueKind.Array) - { - return false; - } - - foreach (var item in driverStatus.EnumerateArray()) + if (await TryRunVersionCommandAsync(command, cancellationToken)) { - if (item.ValueKind != JsonValueKind.Array || item.GetArrayLength() != 2) continue; - - var array = item.EnumerateArray().ToArray(); - // The usual output is [driver-type io.containerd.snapshotter.v1] - if (array[0].GetString() == "driver-type" && array[1].GetString()!.StartsWith("io.containerd.snapshotter")) - { - return true; - } + _commandPath = command; + break; } - - return false; - } - catch - { - return false; } + + return _commandPath; } private async Task TryRunVersionCommandAsync(string command, CancellationToken cancellationToken) @@ -677,10 +290,5 @@ private async Task TryRunVersionCommandAsync(string command, CancellationT return false; } } -#endif - public override string ToString() - { - return string.Format(Strings.DockerCli_PushInfo, _command); - } } diff --git a/src/Containers/Microsoft.NET.Build.Containers/LocalDaemons/ILocalRegistry.cs b/src/Containers/Microsoft.NET.Build.Containers/LocalDaemons/ILocalRegistry.cs index 602a9f71845a..21b89f59a84d 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/LocalDaemons/ILocalRegistry.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/LocalDaemons/ILocalRegistry.cs @@ -6,18 +6,12 @@ namespace Microsoft.NET.Build.Containers; /// /// Abstracts over the concept of a local registry storeof some kind. /// -internal interface ILocalRegistry -{ +internal interface ILocalRegistry { /// /// Loads an image (presumably from a tarball) into the local registry. /// - public Task LoadAsync(BuiltImage image, SourceImageReference sourceReference, DestinationImageReference destinationReference, CancellationToken cancellationToken); - - /// - /// Loads a multi-arch image (presumably from a tarball) into the local registry. - /// - public Task LoadAsync(MultiArchImage multiArchImage, SourceImageReference sourceReference, DestinationImageReference destinationReference, CancellationToken cancellationToken); + public Task LoadAsync(BuiltImage image, ImageReference sourceReference, ImageReference destinationReference, CancellationToken cancellationToken); /// /// Checks to see if the local registry is available. This is used to give nice errors to the user. diff --git a/src/Containers/Microsoft.NET.Build.Containers/LocalDaemons/KnownLocalRegistryTypes.cs b/src/Containers/Microsoft.NET.Build.Containers/LocalDaemons/KnownLocalRegistryTypes.cs index bb50404e2956..447280b1d6c0 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/LocalDaemons/KnownLocalRegistryTypes.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/LocalDaemons/KnownLocalRegistryTypes.cs @@ -1,8 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.Extensions.Logging; using Microsoft.NET.Build.Containers.Resources; +using Microsoft.Extensions.Logging; namespace Microsoft.NET.Build.Containers; @@ -11,7 +11,7 @@ public static class KnownLocalRegistryTypes public const string Docker = nameof(Docker); public const string Podman = nameof(Podman); - public static readonly string[] SupportedLocalRegistryTypes = new[] { Docker, Podman }; + public static readonly string[] SupportedLocalRegistryTypes = new [] { Docker, Podman }; internal static ILocalRegistry CreateLocalRegistry(string? type, ILoggerFactory loggerFactory) { @@ -20,15 +20,14 @@ internal static ILocalRegistry CreateLocalRegistry(string? type, ILoggerFactory return new DockerCli(null, loggerFactory); } - return type switch - { - Podman => new DockerCli(DockerCli.PodmanCommand, loggerFactory), - Docker => new DockerCli(DockerCli.DockerCommand, loggerFactory), + return type switch { + KnownLocalRegistryTypes.Podman => new DockerCli(DockerCli.PodmanCommand, loggerFactory), + KnownLocalRegistryTypes.Docker => new DockerCli(DockerCli.DockerCommand, loggerFactory), _ => throw new NotSupportedException( Resource.FormatString( nameof(Strings.UnknownLocalRegistryType), type, - string.Join(",", SupportedLocalRegistryTypes))) + string.Join(",", KnownLocalRegistryTypes.SupportedLocalRegistryTypes))) }; } } diff --git a/src/Containers/Microsoft.NET.Build.Containers/Logging/MSBuildLogger.cs b/src/Containers/Microsoft.NET.Build.Containers/Logging/MSBuildLogger.cs new file mode 100644 index 000000000000..e6b96093f53b --- /dev/null +++ b/src/Containers/Microsoft.NET.Build.Containers/Logging/MSBuildLogger.cs @@ -0,0 +1,62 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Extensions.Logging; +using ILogger = Microsoft.Extensions.Logging.ILogger; + +namespace Microsoft.NET.Build.Containers.Logging; + +/// +/// Implements an ILogger that passes the logs to the wrapped TaskLoggingHelper. +/// +internal sealed class MSBuildLogger : ILogger +{ + private static readonly IDisposable Scope = new DummyDisposable(); + + private readonly TaskLoggingHelper _loggingHelper; + + public MSBuildLogger(string category, TaskLoggingHelper loggingHelperToWrap) + { + _loggingHelper = loggingHelperToWrap; + } + + IDisposable ILogger.BeginScope(TState state) => Scope; + + public bool IsEnabled(LogLevel logLevel) => true; + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + switch (logLevel) + { + case LogLevel.Trace: + _loggingHelper.LogMessage(MessageImportance.Low, formatter(state, exception)); + break; + case LogLevel.Debug: + case LogLevel.Information: + _loggingHelper.LogMessage(MessageImportance.High, formatter(state, exception)); + break; + case LogLevel.Warning: + _loggingHelper.LogWarning(formatter(state, exception)); + break; + case LogLevel.Error: + case LogLevel.Critical: + _loggingHelper.LogError(formatter(state, exception)); + break; + case LogLevel.None: + break; + default: + break; + } + } + + /// + /// A simple disposable to describe scopes with . + /// + private sealed class DummyDisposable : IDisposable + { + public void Dispose() { } + } +} diff --git a/src/Containers/Microsoft.NET.Build.Containers/Logging/MSBuildLoggerProvider.cs b/src/Containers/Microsoft.NET.Build.Containers/Logging/MSBuildLoggerProvider.cs new file mode 100644 index 000000000000..8db4a7b2ae51 --- /dev/null +++ b/src/Containers/Microsoft.NET.Build.Containers/Logging/MSBuildLoggerProvider.cs @@ -0,0 +1,29 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.Build.Utilities; +using Microsoft.Extensions.Logging; + +namespace Microsoft.NET.Build.Containers.Logging; + +/// +/// An that creates s which passes +/// all the logs to MSBuild's . +/// +internal class MSBuildLoggerProvider : ILoggerProvider +{ + private readonly TaskLoggingHelper _loggingHelper; + + public MSBuildLoggerProvider(TaskLoggingHelper loggingHelperToWrap) + { + _loggingHelper = loggingHelperToWrap; + } + + public ILogger CreateLogger(string categoryName) + { + return new MSBuildLogger(categoryName, _loggingHelper); + } + + public void Dispose() { } +} diff --git a/src/Containers/Microsoft.NET.Build.Containers/ManifestListV2.cs b/src/Containers/Microsoft.NET.Build.Containers/ManifestListV2.cs index 471f450f42d6..44a294d65473 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/ManifestListV2.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/ManifestListV2.cs @@ -10,6 +10,3 @@ public record struct ManifestListV2(int schemaVersion, string mediaType, Platfor public record struct PlatformInformation(string architecture, string os, string? variant, string[] features, [property: JsonPropertyName("os.version")][field: JsonPropertyName("os.version")] string? version); public record struct PlatformSpecificManifest(string mediaType, long size, string digest, PlatformInformation platform); -public record struct ImageIndexV1(int schemaVersion, string mediaType, PlatformSpecificOciManifest[] manifests); - -public record struct PlatformSpecificOciManifest(string mediaType, long size, string digest, PlatformInformation platform, Dictionary annotations); diff --git a/src/Containers/Microsoft.NET.Build.Containers/ManifestV2.cs b/src/Containers/Microsoft.NET.Build.Containers/ManifestV2.cs index b4334c5e26db..eb00143665c6 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/ManifestV2.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/ManifestV2.cs @@ -12,11 +12,8 @@ namespace Microsoft.NET.Build.Containers; /// /// https://github.com/opencontainers/image-spec/blob/main/manifest.md /// -public class ManifestV2 +public readonly record struct ManifestV2 { - [JsonIgnore] - public string? KnownDigest { get; set; } - /// /// This REQUIRED property specifies the image manifest schema version. /// For this version of the specification, this MUST be 2 to ensure backward compatibility with older versions of Docker. @@ -30,7 +27,7 @@ public class ManifestV2 /// When used, this field MUST contain the media type application/vnd.oci.image.manifest.v1+json. This field usage differs from the descriptor use of mediaType. /// [JsonPropertyName("mediaType")] - public string? MediaType { get; init; } + public required string MediaType { get; init; } /// /// This REQUIRED property references a configuration object for a container, by digest. @@ -50,9 +47,9 @@ public class ManifestV2 /// /// Gets the digest for this manifest. /// - public string GetDigest() => KnownDigest ??= DigestUtils.GetDigest(JsonSerializer.SerializeToNode(this)?.ToJsonString() ?? string.Empty); + public string GetDigest() => DigestUtils.GetDigest(JsonSerializer.SerializeToNode(this)?.ToJsonString() ?? string.Empty); } public record struct ManifestConfig(string mediaType, long size, string digest); -public record struct ManifestLayer(string mediaType, long size, string digest, [property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)][field: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] string[]? urls); +public record struct ManifestLayer(string mediaType, long size, string digest, string[]? urls); diff --git a/src/Containers/Microsoft.NET.Build.Containers/Microsoft.NET.Build.Containers.csproj b/src/Containers/Microsoft.NET.Build.Containers/Microsoft.NET.Build.Containers.csproj index 0ac53676b8c4..d891c2f619b3 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Microsoft.NET.Build.Containers.csproj +++ b/src/Containers/Microsoft.NET.Build.Containers/Microsoft.NET.Build.Containers.csproj @@ -1,92 +1,69 @@  - $(SdkTargetFramework);$(NetFrameworkToolCurrent) - enable + $(SdkTargetFramework);net472 + $(SdkTargetFramework) + enable $(TargetsForTfmSpecificBuildOutput);CopyProjectReferencesToPackage - - true + + true . embedded - $(NoWarn);CS8002 + enable + ($NoWarn);CS8002 MicrosoftShared true - - - false - - - build - false - - + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - runtime - - - - $(SystemTextJsonToolsetPackageVersion) - + + + + - - - - - - - - - - + + - - - + - + + - - + + - + + - True @@ -114,7 +91,7 @@ - + @@ -122,7 +99,7 @@ - + diff --git a/src/Containers/Microsoft.NET.Build.Containers/PublicAPI/net472/PublicAPI.Unshipped.txt b/src/Containers/Microsoft.NET.Build.Containers/PublicAPI/net472/PublicAPI.Unshipped.txt index 607f906e2676..f8b14db27b28 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/PublicAPI/net472/PublicAPI.Unshipped.txt +++ b/src/Containers/Microsoft.NET.Build.Containers/PublicAPI/net472/PublicAPI.Unshipped.txt @@ -1,36 +1,31 @@ Microsoft.NET.Build.Containers.ContainerHelpers Microsoft.NET.Build.Containers.ContainerHelpers.ParsePortError -Microsoft.NET.Build.Containers.ContainerHelpers.ParsePortError.InvalidPortNumber = 2 -> Microsoft.NET.Build.Containers.ContainerHelpers.ParsePortError -Microsoft.NET.Build.Containers.ContainerHelpers.ParsePortError.InvalidPortType = 4 -> Microsoft.NET.Build.Containers.ContainerHelpers.ParsePortError -Microsoft.NET.Build.Containers.ContainerHelpers.ParsePortError.MissingPortNumber = 1 -> Microsoft.NET.Build.Containers.ContainerHelpers.ParsePortError -Microsoft.NET.Build.Containers.ContainerHelpers.ParsePortError.UnknownPortFormat = 8 -> Microsoft.NET.Build.Containers.ContainerHelpers.ParsePortError +Microsoft.NET.Build.Containers.ContainerHelpers.ParsePortError.InvalidPortNumber = 1 -> Microsoft.NET.Build.Containers.ContainerHelpers.ParsePortError +Microsoft.NET.Build.Containers.ContainerHelpers.ParsePortError.InvalidPortType = 2 -> Microsoft.NET.Build.Containers.ContainerHelpers.ParsePortError +Microsoft.NET.Build.Containers.ContainerHelpers.ParsePortError.MissingPortNumber = 0 -> Microsoft.NET.Build.Containers.ContainerHelpers.ParsePortError +Microsoft.NET.Build.Containers.ContainerHelpers.ParsePortError.UnknownPortFormat = Microsoft.NET.Build.Containers.ContainerHelpers.ParsePortError.InvalidPortNumber | Microsoft.NET.Build.Containers.ContainerHelpers.ParsePortError.InvalidPortType -> Microsoft.NET.Build.Containers.ContainerHelpers.ParsePortError Microsoft.NET.Build.Containers.Port -Microsoft.NET.Build.Containers.Port.Deconstruct(out int Number, out Microsoft.NET.Build.Containers.PortType Type) -> void -Microsoft.NET.Build.Containers.Port.Equals(Microsoft.NET.Build.Containers.Port other) -> bool Microsoft.NET.Build.Containers.Port.Number.get -> int Microsoft.NET.Build.Containers.Port.Number.set -> void Microsoft.NET.Build.Containers.Port.Port() -> void Microsoft.NET.Build.Containers.Port.Port(int Number, Microsoft.NET.Build.Containers.PortType Type) -> void Microsoft.NET.Build.Containers.Port.Type.get -> Microsoft.NET.Build.Containers.PortType Microsoft.NET.Build.Containers.Port.Type.set -> void -Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.ContainerFamily.get -> string? -Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.UserBaseImage.get -> string? -Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.UserBaseImage.set -> void -~override Microsoft.NET.Build.Containers.Port.ToString() -> string -static Microsoft.NET.Build.Containers.Port.operator !=(Microsoft.NET.Build.Containers.Port left, Microsoft.NET.Build.Containers.Port right) -> bool -static Microsoft.NET.Build.Containers.Port.operator ==(Microsoft.NET.Build.Containers.Port left, Microsoft.NET.Build.Containers.Port right) -> bool -override Microsoft.NET.Build.Containers.Port.GetHashCode() -> int -~override Microsoft.NET.Build.Containers.Port.Equals(object obj) -> bool Microsoft.NET.Build.Containers.PortType Microsoft.NET.Build.Containers.PortType.tcp = 0 -> Microsoft.NET.Build.Containers.PortType Microsoft.NET.Build.Containers.PortType.udp = 1 -> Microsoft.NET.Build.Containers.PortType +Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageTag +Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageTag.ComputedBaseImageTag.get -> string? +Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageTag.ComputeDotnetBaseImageTag() -> void +Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageTag.SdkVersion.get -> string! +Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageTag.SdkVersion.set -> void +Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageTag.TargetFrameworkVersion.get -> string! +Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageTag.TargetFrameworkVersion.set -> void Microsoft.NET.Build.Containers.Tasks.CreateNewImage Microsoft.NET.Build.Containers.Tasks.CreateNewImage.BaseImageName.get -> string! Microsoft.NET.Build.Containers.Tasks.CreateNewImage.BaseImageName.set -> void Microsoft.NET.Build.Containers.Tasks.CreateNewImage.BaseImageTag.get -> string! Microsoft.NET.Build.Containers.Tasks.CreateNewImage.BaseImageTag.set -> void -Microsoft.NET.Build.Containers.Tasks.CreateNewImage.BaseImageDigest.get -> string! -Microsoft.NET.Build.Containers.Tasks.CreateNewImage.BaseImageDigest.set -> void Microsoft.NET.Build.Containers.Tasks.CreateNewImage.BaseRegistry.get -> string! Microsoft.NET.Build.Containers.Tasks.CreateNewImage.BaseRegistry.set -> void Microsoft.NET.Build.Containers.Tasks.CreateNewImage.ContainerEnvironmentVariables.get -> Microsoft.Build.Framework.ITaskItem![]! @@ -60,12 +55,6 @@ Microsoft.NET.Build.Containers.Tasks.CreateNewImage.GeneratedContainerConfigurat Microsoft.NET.Build.Containers.Tasks.CreateNewImage.GeneratedContainerConfiguration.set -> void Microsoft.NET.Build.Containers.Tasks.CreateNewImage.GeneratedContainerManifest.get -> string! Microsoft.NET.Build.Containers.Tasks.CreateNewImage.GeneratedContainerManifest.set -> void -Microsoft.NET.Build.Containers.Tasks.CreateNewImage.GeneratedContainerDigest.get -> string! -Microsoft.NET.Build.Containers.Tasks.CreateNewImage.GeneratedContainerDigest.set -> void -Microsoft.NET.Build.Containers.Tasks.CreateNewImage.GeneratedArchiveOutputPath.get -> string! -Microsoft.NET.Build.Containers.Tasks.CreateNewImage.GeneratedArchiveOutputPath.set -> void -Microsoft.NET.Build.Containers.Tasks.CreateNewImage.GeneratedContainerMediaType.get -> string! -Microsoft.NET.Build.Containers.Tasks.CreateNewImage.GeneratedContainerMediaType.set -> void Microsoft.NET.Build.Containers.Tasks.CreateNewImage.Repository.get -> string! Microsoft.NET.Build.Containers.Tasks.CreateNewImage.Repository.set -> void Microsoft.NET.Build.Containers.Tasks.CreateNewImage.ImageTags.get -> string![]! @@ -76,26 +65,13 @@ Microsoft.NET.Build.Containers.Tasks.CreateNewImage.LocalRegistry.get -> string! Microsoft.NET.Build.Containers.Tasks.CreateNewImage.LocalRegistry.set -> void Microsoft.NET.Build.Containers.Tasks.CreateNewImage.OutputRegistry.get -> string! Microsoft.NET.Build.Containers.Tasks.CreateNewImage.OutputRegistry.set -> void -Microsoft.NET.Build.Containers.Tasks.CreateNewImage.ArchiveOutputPath.get -> string! -Microsoft.NET.Build.Containers.Tasks.CreateNewImage.ArchiveOutputPath.set -> void Microsoft.NET.Build.Containers.Tasks.CreateNewImage.PublishDirectory.get -> string! Microsoft.NET.Build.Containers.Tasks.CreateNewImage.PublishDirectory.set -> void Microsoft.NET.Build.Containers.Tasks.CreateNewImage.RuntimeIdentifierGraphPath.get -> string! Microsoft.NET.Build.Containers.Tasks.CreateNewImage.RuntimeIdentifierGraphPath.set -> void Microsoft.NET.Build.Containers.Tasks.CreateNewImage.WorkingDirectory.get -> string! Microsoft.NET.Build.Containers.Tasks.CreateNewImage.WorkingDirectory.set -> void -Microsoft.NET.Build.Containers.Tasks.CreateNewImage.GenerateLabels.get -> bool -Microsoft.NET.Build.Containers.Tasks.CreateNewImage.GenerateLabels.set -> void -Microsoft.NET.Build.Containers.Tasks.CreateNewImage.GenerateDigestLabel.get -> bool -Microsoft.NET.Build.Containers.Tasks.CreateNewImage.GenerateDigestLabel.set -> void -Microsoft.NET.Build.Containers.Tasks.CreateNewImage.SkipPublishing.get -> bool -Microsoft.NET.Build.Containers.Tasks.CreateNewImage.SkipPublishing.set -> void -Microsoft.NET.Build.Containers.Tasks.CreateNewImage.GeneratedContainerNames.get -> Microsoft.Build.Framework.ITaskItem![]! -Microsoft.NET.Build.Containers.Tasks.CreateNewImage.GeneratedContainerNames.set -> void -Microsoft.NET.Build.Containers.Tasks.CreateNewImage.ImageFormat.get -> string? -Microsoft.NET.Build.Containers.Tasks.CreateNewImage.ImageFormat.set -> void -Microsoft.NET.Build.Containers.Tasks.CreateNewImage.GeneratedDigestLabel.get -> Microsoft.Build.Framework.ITaskItem? -Microsoft.NET.Build.Containers.Tasks.CreateNewImage.GeneratedDigestLabel.set -> void +override Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageTag.Execute() -> bool override Microsoft.NET.Build.Containers.Tasks.CreateNewImage.ToolName.get -> string! override Microsoft.NET.Build.Containers.Tasks.CreateNewImage.GenerateCommandLineCommands() -> string! override Microsoft.NET.Build.Containers.Tasks.CreateNewImage.GenerateFullPathToTool() -> string! @@ -121,28 +97,6 @@ Microsoft.NET.Build.Containers.Tasks.ParseContainerProperties.ParseContainerProp Microsoft.NET.Build.Containers.Tasks.ParseContainerProperties.ParsedContainerImage.get -> string! Microsoft.NET.Build.Containers.Tasks.ParseContainerProperties.ParsedContainerRegistry.get -> string! Microsoft.NET.Build.Containers.Tasks.ParseContainerProperties.ParsedContainerTag.get -> string! -Microsoft.NET.Build.Containers.Tasks.ParseContainerProperties.ParsedContainerDigest.get -> string! override Microsoft.NET.Build.Containers.Tasks.ParseContainerProperties.Execute() -> bool static Microsoft.NET.Build.Containers.ContainerHelpers.TryParsePort(string! input, out Microsoft.NET.Build.Containers.Port? port, out Microsoft.NET.Build.Containers.ContainerHelpers.ParsePortError? error) -> bool static Microsoft.NET.Build.Containers.ContainerHelpers.TryParsePort(string? portNumber, string? portType, out Microsoft.NET.Build.Containers.Port? port, out Microsoft.NET.Build.Containers.ContainerHelpers.ParsePortError? error) -> bool -Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.ComputedContainerBaseImage.get -> string? -Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.FrameworkReferences.get -> Microsoft.Build.Framework.ITaskItem![]! -Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.FrameworkReferences.set -> void -Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.IsAotPublished.get -> bool -Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.IsAotPublished.set -> void -Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.IsTrimmed.get -> bool -Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.IsTrimmed.set -> void -Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.IsSelfContained.get -> bool -Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.IsSelfContained.set -> void -Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.TargetRuntimeIdentifiers.get -> string![]! -Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.TargetRuntimeIdentifiers.set -> void -Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.UsesInvariantGlobalization.get -> bool -Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.UsesInvariantGlobalization.set -> void -Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag -Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.ComputeDotnetBaseImageAndTag() -> void -Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.ContainerFamily.set -> void -Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.SdkVersion.get -> string! -Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.SdkVersion.set -> void -Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.TargetFrameworkVersion.get -> string! -Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.TargetFrameworkVersion.set -> void -override Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageAndTag.Execute() -> bool diff --git a/src/Containers/Microsoft.NET.Build.Containers/PublicAPI/net8.0/PublicAPI.Shipped.txt b/src/Containers/Microsoft.NET.Build.Containers/PublicAPI/net8.0/PublicAPI.Shipped.txt new file mode 100644 index 000000000000..ab058de62d44 --- /dev/null +++ b/src/Containers/Microsoft.NET.Build.Containers/PublicAPI/net8.0/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/src/Containers/Microsoft.NET.Build.Containers/PublicAPI/net8.0/PublicAPI.Unshipped.txt b/src/Containers/Microsoft.NET.Build.Containers/PublicAPI/net8.0/PublicAPI.Unshipped.txt new file mode 100644 index 000000000000..c0b58b0697aa --- /dev/null +++ b/src/Containers/Microsoft.NET.Build.Containers/PublicAPI/net8.0/PublicAPI.Unshipped.txt @@ -0,0 +1,195 @@ +const Microsoft.NET.Build.Containers.KnownLocalRegistryTypes.Docker = "Docker" -> string! +const Microsoft.NET.Build.Containers.KnownLocalRegistryTypes.Podman = "Podman" -> string! +Microsoft.NET.Build.Containers.BaseImageNotFoundException +Microsoft.NET.Build.Containers.Constants +Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageTag.ComputeDotnetBaseImageTag() -> void +static Microsoft.NET.Build.Containers.ContainerBuilder.ContainerizeAsync(System.IO.DirectoryInfo! publishDirectory, string! workingDir, string! baseRegistry, string! baseImageName, string! baseImageTag, string![]! entrypoint, string![]? cmd, string! imageName, string![]! imageTags, string? outputRegistry, System.Collections.Generic.Dictionary! labels, Microsoft.NET.Build.Containers.Port[]? exposedPorts, System.Collections.Generic.Dictionary! envVars, string! containerRuntimeIdentifier, string! ridGraphPath, string! localRegistry, string? containerUser, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +static readonly Microsoft.NET.Build.Containers.Constants.Version -> string! +Microsoft.NET.Build.Containers.ContainerBuilder +Microsoft.NET.Build.Containers.ContainerHelpers +Microsoft.NET.Build.Containers.ContainerHelpers.ParsePortError +Microsoft.NET.Build.Containers.ContainerHelpers.ParsePortError.InvalidPortNumber = 1 -> Microsoft.NET.Build.Containers.ContainerHelpers.ParsePortError +Microsoft.NET.Build.Containers.ContainerHelpers.ParsePortError.InvalidPortType = 2 -> Microsoft.NET.Build.Containers.ContainerHelpers.ParsePortError +Microsoft.NET.Build.Containers.ContainerHelpers.ParsePortError.MissingPortNumber = 0 -> Microsoft.NET.Build.Containers.ContainerHelpers.ParsePortError +Microsoft.NET.Build.Containers.ContainerHelpers.ParsePortError.UnknownPortFormat = Microsoft.NET.Build.Containers.ContainerHelpers.ParsePortError.InvalidPortNumber | Microsoft.NET.Build.Containers.ContainerHelpers.ParsePortError.InvalidPortType -> Microsoft.NET.Build.Containers.ContainerHelpers.ParsePortError +Microsoft.NET.Build.Containers.Descriptor +Microsoft.NET.Build.Containers.Descriptor.Annotations.get -> System.Collections.Generic.Dictionary? +Microsoft.NET.Build.Containers.Descriptor.Annotations.init -> void +Microsoft.NET.Build.Containers.Descriptor.Data.get -> string? +Microsoft.NET.Build.Containers.Descriptor.Data.init -> void +Microsoft.NET.Build.Containers.Descriptor.Descriptor() -> void +Microsoft.NET.Build.Containers.Descriptor.Descriptor(string! mediaType, string! digest, long size) -> void +Microsoft.NET.Build.Containers.Descriptor.Digest.get -> string! +Microsoft.NET.Build.Containers.Descriptor.Digest.init -> void +Microsoft.NET.Build.Containers.Descriptor.MediaType.get -> string! +Microsoft.NET.Build.Containers.Descriptor.MediaType.init -> void +Microsoft.NET.Build.Containers.Descriptor.Size.get -> long +Microsoft.NET.Build.Containers.Descriptor.Size.init -> void +Microsoft.NET.Build.Containers.Descriptor.UncompressedDigest.get -> string? +Microsoft.NET.Build.Containers.Descriptor.UncompressedDigest.init -> void +Microsoft.NET.Build.Containers.Descriptor.Urls.get -> string![]? +Microsoft.NET.Build.Containers.Descriptor.Urls.init -> void +Microsoft.NET.Build.Containers.KnownLocalRegistryTypes +Microsoft.NET.Build.Containers.ManifestConfig +Microsoft.NET.Build.Containers.ManifestConfig.digest.get -> string! +Microsoft.NET.Build.Containers.ManifestConfig.digest.set -> void +Microsoft.NET.Build.Containers.ManifestConfig.ManifestConfig() -> void +Microsoft.NET.Build.Containers.ManifestConfig.ManifestConfig(string! mediaType, long size, string! digest) -> void +Microsoft.NET.Build.Containers.ManifestConfig.mediaType.get -> string! +Microsoft.NET.Build.Containers.ManifestConfig.mediaType.set -> void +Microsoft.NET.Build.Containers.ManifestConfig.size.get -> long +Microsoft.NET.Build.Containers.ManifestConfig.size.set -> void +Microsoft.NET.Build.Containers.ManifestLayer +Microsoft.NET.Build.Containers.ManifestLayer.digest.get -> string! +Microsoft.NET.Build.Containers.ManifestLayer.digest.set -> void +Microsoft.NET.Build.Containers.ManifestLayer.ManifestLayer() -> void +Microsoft.NET.Build.Containers.ManifestLayer.ManifestLayer(string! mediaType, long size, string! digest, string![]? urls) -> void +Microsoft.NET.Build.Containers.ManifestLayer.mediaType.get -> string! +Microsoft.NET.Build.Containers.ManifestLayer.mediaType.set -> void +Microsoft.NET.Build.Containers.ManifestLayer.size.get -> long +Microsoft.NET.Build.Containers.ManifestLayer.size.set -> void +Microsoft.NET.Build.Containers.ManifestLayer.urls.get -> string![]? +Microsoft.NET.Build.Containers.ManifestLayer.urls.set -> void +Microsoft.NET.Build.Containers.ManifestListV2 +Microsoft.NET.Build.Containers.ManifestListV2.ManifestListV2() -> void +Microsoft.NET.Build.Containers.ManifestListV2.ManifestListV2(int schemaVersion, string! mediaType, Microsoft.NET.Build.Containers.PlatformSpecificManifest[]! manifests) -> void +Microsoft.NET.Build.Containers.ManifestListV2.manifests.get -> Microsoft.NET.Build.Containers.PlatformSpecificManifest[]! +Microsoft.NET.Build.Containers.ManifestListV2.manifests.set -> void +Microsoft.NET.Build.Containers.ManifestListV2.mediaType.get -> string! +Microsoft.NET.Build.Containers.ManifestListV2.mediaType.set -> void +Microsoft.NET.Build.Containers.ManifestListV2.schemaVersion.get -> int +Microsoft.NET.Build.Containers.ManifestListV2.schemaVersion.set -> void +Microsoft.NET.Build.Containers.ManifestV2 +Microsoft.NET.Build.Containers.ManifestV2.Config.get -> Microsoft.NET.Build.Containers.ManifestConfig +Microsoft.NET.Build.Containers.ManifestV2.Config.init -> void +Microsoft.NET.Build.Containers.ManifestV2.GetDigest() -> string! +Microsoft.NET.Build.Containers.ManifestV2.Layers.get -> System.Collections.Generic.List! +Microsoft.NET.Build.Containers.ManifestV2.Layers.init -> void +Microsoft.NET.Build.Containers.ManifestV2.ManifestV2() -> void +Microsoft.NET.Build.Containers.ManifestV2.MediaType.get -> string! +Microsoft.NET.Build.Containers.ManifestV2.MediaType.init -> void +Microsoft.NET.Build.Containers.ManifestV2.SchemaVersion.get -> int +Microsoft.NET.Build.Containers.ManifestV2.SchemaVersion.init -> void +Microsoft.NET.Build.Containers.PlatformInformation +Microsoft.NET.Build.Containers.PlatformInformation.architecture.get -> string! +Microsoft.NET.Build.Containers.PlatformInformation.architecture.set -> void +Microsoft.NET.Build.Containers.PlatformInformation.features.get -> string![]! +Microsoft.NET.Build.Containers.PlatformInformation.features.set -> void +Microsoft.NET.Build.Containers.PlatformInformation.os.get -> string! +Microsoft.NET.Build.Containers.PlatformInformation.os.set -> void +Microsoft.NET.Build.Containers.PlatformInformation.PlatformInformation() -> void +Microsoft.NET.Build.Containers.PlatformInformation.PlatformInformation(string! architecture, string! os, string? variant, string![]! features, string? version) -> void +Microsoft.NET.Build.Containers.PlatformInformation.variant.get -> string? +Microsoft.NET.Build.Containers.PlatformInformation.variant.set -> void +Microsoft.NET.Build.Containers.PlatformInformation.version.get -> string? +Microsoft.NET.Build.Containers.PlatformInformation.version.set -> void +Microsoft.NET.Build.Containers.PlatformSpecificManifest +Microsoft.NET.Build.Containers.PlatformSpecificManifest.digest.get -> string! +Microsoft.NET.Build.Containers.PlatformSpecificManifest.digest.set -> void +Microsoft.NET.Build.Containers.PlatformSpecificManifest.mediaType.get -> string! +Microsoft.NET.Build.Containers.PlatformSpecificManifest.mediaType.set -> void +Microsoft.NET.Build.Containers.PlatformSpecificManifest.platform.get -> Microsoft.NET.Build.Containers.PlatformInformation +Microsoft.NET.Build.Containers.PlatformSpecificManifest.platform.set -> void +Microsoft.NET.Build.Containers.PlatformSpecificManifest.PlatformSpecificManifest() -> void +Microsoft.NET.Build.Containers.PlatformSpecificManifest.PlatformSpecificManifest(string! mediaType, long size, string! digest, Microsoft.NET.Build.Containers.PlatformInformation platform) -> void +Microsoft.NET.Build.Containers.PlatformSpecificManifest.size.get -> long +Microsoft.NET.Build.Containers.PlatformSpecificManifest.size.set -> void +Microsoft.NET.Build.Containers.Port +Microsoft.NET.Build.Containers.Port.Number.get -> int +Microsoft.NET.Build.Containers.Port.Number.set -> void +Microsoft.NET.Build.Containers.Port.Port() -> void +Microsoft.NET.Build.Containers.Port.Port(int Number, Microsoft.NET.Build.Containers.PortType Type) -> void +Microsoft.NET.Build.Containers.Port.Type.get -> Microsoft.NET.Build.Containers.PortType +Microsoft.NET.Build.Containers.Port.Type.set -> void +Microsoft.NET.Build.Containers.PortType +Microsoft.NET.Build.Containers.PortType.tcp = 0 -> Microsoft.NET.Build.Containers.PortType +Microsoft.NET.Build.Containers.PortType.udp = 1 -> Microsoft.NET.Build.Containers.PortType +Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageTag +Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageTag.SdkVersion.get -> string! +Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageTag.SdkVersion.set -> void +Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageTag.TargetFrameworkVersion.get -> string! +Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageTag.TargetFrameworkVersion.set -> void +Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageTag.ComputedBaseImageTag.get -> string? +Microsoft.NET.Build.Containers.Tasks.CreateNewImage +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.BaseImageName.get -> string! +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.BaseImageName.set -> void +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.BaseImageTag.get -> string! +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.BaseImageTag.set -> void +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.BaseRegistry.get -> string! +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.BaseRegistry.set -> void +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.Cancel() -> void +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.ContainerEnvironmentVariables.get -> Microsoft.Build.Framework.ITaskItem![]! +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.ContainerEnvironmentVariables.set -> void +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.ContainerizeDirectory.get -> string! +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.ContainerizeDirectory.set -> void +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.ContainerRuntimeIdentifier.get -> string! +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.ContainerRuntimeIdentifier.set -> void +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.ContainerUser.get -> string! +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.ContainerUser.set -> void +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.CreateNewImage() -> void +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.Dispose() -> void +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.Entrypoint.get -> Microsoft.Build.Framework.ITaskItem![]! +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.Entrypoint.set -> void +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.EntrypointArgs.get -> Microsoft.Build.Framework.ITaskItem![]! +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.EntrypointArgs.set -> void +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.DefaultArgs.get -> Microsoft.Build.Framework.ITaskItem![]! +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.DefaultArgs.set -> void +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.AppCommand.get -> Microsoft.Build.Framework.ITaskItem![]! +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.AppCommand.set -> void +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.AppCommandArgs.get -> Microsoft.Build.Framework.ITaskItem![]! +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.AppCommandArgs.set -> void +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.AppCommandInstruction.get -> string! +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.AppCommandInstruction.set -> void +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.ExposedPorts.get -> Microsoft.Build.Framework.ITaskItem![]! +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.ExposedPorts.set -> void +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.GeneratedContainerConfiguration.get -> string! +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.GeneratedContainerConfiguration.set -> void +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.GeneratedContainerManifest.get -> string! +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.GeneratedContainerManifest.set -> void +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.Repository.get -> string! +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.Repository.set -> void +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.ImageTags.get -> string![]! +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.ImageTags.set -> void +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.Labels.get -> Microsoft.Build.Framework.ITaskItem![]! +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.Labels.set -> void +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.LocalRegistry.get -> string! +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.LocalRegistry.set -> void +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.OutputRegistry.get -> string! +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.OutputRegistry.set -> void +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.PublishDirectory.get -> string! +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.PublishDirectory.set -> void +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.RuntimeIdentifierGraphPath.get -> string! +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.RuntimeIdentifierGraphPath.set -> void +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.ToolExe.get -> string! +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.ToolExe.set -> void +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.ToolPath.get -> string! +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.ToolPath.set -> void +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.WorkingDirectory.get -> string! +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.WorkingDirectory.set -> void +Microsoft.NET.Build.Containers.Tasks.ParseContainerProperties +Microsoft.NET.Build.Containers.Tasks.ParseContainerProperties.ContainerEnvironmentVariables.get -> Microsoft.Build.Framework.ITaskItem![]! +Microsoft.NET.Build.Containers.Tasks.ParseContainerProperties.ContainerEnvironmentVariables.set -> void +Microsoft.NET.Build.Containers.Tasks.ParseContainerProperties.ContainerRepository.get -> string! +Microsoft.NET.Build.Containers.Tasks.ParseContainerProperties.ContainerRepository.set -> void +Microsoft.NET.Build.Containers.Tasks.ParseContainerProperties.ContainerImageTag.get -> string! +Microsoft.NET.Build.Containers.Tasks.ParseContainerProperties.ContainerImageTag.set -> void +Microsoft.NET.Build.Containers.Tasks.ParseContainerProperties.ContainerImageTags.get -> string![]! +Microsoft.NET.Build.Containers.Tasks.ParseContainerProperties.ContainerImageTags.set -> void +Microsoft.NET.Build.Containers.Tasks.ParseContainerProperties.ContainerRegistry.get -> string! +Microsoft.NET.Build.Containers.Tasks.ParseContainerProperties.ContainerRegistry.set -> void +Microsoft.NET.Build.Containers.Tasks.ParseContainerProperties.FullyQualifiedBaseImageName.get -> string! +Microsoft.NET.Build.Containers.Tasks.ParseContainerProperties.FullyQualifiedBaseImageName.set -> void +Microsoft.NET.Build.Containers.Tasks.ParseContainerProperties.NewContainerEnvironmentVariables.get -> Microsoft.Build.Framework.ITaskItem![]! +Microsoft.NET.Build.Containers.Tasks.ParseContainerProperties.NewContainerRepository.get -> string! +Microsoft.NET.Build.Containers.Tasks.ParseContainerProperties.NewContainerRegistry.get -> string! +Microsoft.NET.Build.Containers.Tasks.ParseContainerProperties.NewContainerTags.get -> string![]! +Microsoft.NET.Build.Containers.Tasks.ParseContainerProperties.ParseContainerProperties() -> void +Microsoft.NET.Build.Containers.Tasks.ParseContainerProperties.ParsedContainerImage.get -> string! +Microsoft.NET.Build.Containers.Tasks.ParseContainerProperties.ParsedContainerRegistry.get -> string! +Microsoft.NET.Build.Containers.Tasks.ParseContainerProperties.ParsedContainerTag.get -> string! +override Microsoft.NET.Build.Containers.Tasks.ComputeDotnetBaseImageTag.Execute() -> bool +override Microsoft.NET.Build.Containers.Tasks.CreateNewImage.Execute() -> bool +override Microsoft.NET.Build.Containers.Tasks.ParseContainerProperties.Execute() -> bool +static Microsoft.NET.Build.Containers.ContainerHelpers.TryParsePort(string! input, out Microsoft.NET.Build.Containers.Port? port, out Microsoft.NET.Build.Containers.ContainerHelpers.ParsePortError? error) -> bool +static Microsoft.NET.Build.Containers.ContainerHelpers.TryParsePort(string? portNumber, string? portType, out Microsoft.NET.Build.Containers.Port? port, out Microsoft.NET.Build.Containers.ContainerHelpers.ParsePortError? error) -> bool +static readonly Microsoft.NET.Build.Containers.KnownLocalRegistryTypes.SupportedLocalRegistryTypes -> string![]! diff --git a/src/Containers/Microsoft.NET.Build.Containers/ReferenceParser.cs b/src/Containers/Microsoft.NET.Build.Containers/ReferenceParser.cs index 03f9e074990e..5884983dbda1 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/ReferenceParser.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/ReferenceParser.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Text; using System.Text.RegularExpressions; namespace Microsoft.NET.Build.Containers; diff --git a/src/Containers/Microsoft.NET.Build.Containers/Registry/DefaultBlobOperations.cs b/src/Containers/Microsoft.NET.Build.Containers/Registry/DefaultBlobOperations.cs index fdbac3852cd3..360b160144c8 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Registry/DefaultBlobOperations.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/Registry/DefaultBlobOperations.cs @@ -1,11 +1,11 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// using System.Diagnostics; using System.Net; using System.Text.Json.Nodes; using Microsoft.Extensions.Logging; -using Microsoft.NET.Build.Containers.Resources; namespace Microsoft.NET.Build.Containers; @@ -14,14 +14,12 @@ internal class DefaultBlobOperations : IBlobOperations private readonly Uri _baseUri; private readonly HttpClient _client; private readonly ILogger _logger; - private readonly string _registryName; - public DefaultBlobOperations(Uri baseUri, string registryName, HttpClient client, ILogger logger) + public DefaultBlobOperations(Uri baseUri, HttpClient client, ILogger logger) { _baseUri = baseUri; _client = client; _logger = logger; - _registryName = registryName; Upload = new DefaultBlobUploadOperations(_baseUri, _client, _logger); } @@ -30,14 +28,8 @@ public DefaultBlobOperations(Uri baseUri, string registryName, HttpClient client public async Task ExistsAsync(string repositoryName, string digest, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - using HttpResponseMessage response = await _client.SendAsync(new HttpRequestMessage(HttpMethod.Head, new Uri(_baseUri, $"/v2/{repositoryName}/blobs/{digest}")), cancellationToken).ConfigureAwait(false); - return response.StatusCode switch - { - HttpStatusCode.OK => true, - HttpStatusCode.NotFound => false, - HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden => throw new UnableToAccessRepositoryException(_registryName, repositoryName), - _ => await LogAndThrowContainerHttpException(response, cancellationToken).ConfigureAwait(false) - }; + HttpResponseMessage response = await _client.SendAsync(new HttpRequestMessage(HttpMethod.Head, new Uri(_baseUri, $"/v2/{repositoryName}/blobs/{digest}")), cancellationToken).ConfigureAwait(false); + return response.StatusCode == HttpStatusCode.OK; } public async Task GetJsonAsync(string repositoryName, string digest, CancellationToken cancellationToken) @@ -64,17 +56,7 @@ private async Task GetAsync(string repositoryName, string d cancellationToken.ThrowIfCancellationRequested(); using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, new Uri(_baseUri, $"/v2/{repositoryName}/blobs/{digest}")).AcceptManifestFormats(); HttpResponseMessage response = await _client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); - return response.StatusCode switch - { - HttpStatusCode.OK => response, - HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden => throw new UnableToAccessRepositoryException(_registryName, repositoryName), - _ => await LogAndThrowContainerHttpException(response, cancellationToken).ConfigureAwait(false) - }; - } - - private async Task LogAndThrowContainerHttpException(HttpResponseMessage response, CancellationToken cancellationToken) - { - await response.LogHttpResponseAsync(_logger, cancellationToken).ConfigureAwait(false); - throw new ContainerHttpException(Resource.GetString(nameof(Strings.RegistryPullFailed)), response.RequestMessage?.RequestUri?.ToString(), response.StatusCode); + response.EnsureSuccessStatusCode(); + return response; } } diff --git a/src/Containers/Microsoft.NET.Build.Containers/Registry/DefaultBlobUploadOperations.cs b/src/Containers/Microsoft.NET.Build.Containers/Registry/DefaultBlobUploadOperations.cs index ea9494f6d79f..cf5b5cd9066e 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Registry/DefaultBlobUploadOperations.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/Registry/DefaultBlobUploadOperations.cs @@ -1,10 +1,11 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// -using System.Net; using System.Net.Http.Headers; -using Microsoft.Extensions.Logging; +using System.Net; using Microsoft.NET.Build.Containers.Resources; +using Microsoft.Extensions.Logging; namespace Microsoft.NET.Build.Containers; @@ -107,7 +108,7 @@ private async Task PatchAsync(Uri uploadUri, HttpContent content, Cancellat _logger.LogTrace("Uploading {0} bytes of content at {1}", content.Headers.ContentLength, uploadUri); HttpRequestMessage patchMessage = GetPatchHttpRequest(uploadUri, content); - using HttpResponseMessage patchResponse = await _client.SendAsync(patchMessage, cancellationToken).ConfigureAwait(false); + HttpResponseMessage patchResponse = await _client.SendAsync(patchMessage, cancellationToken).ConfigureAwait(false); _logger.LogTrace("Received status code '{0}' from upload.", patchResponse.StatusCode); diff --git a/src/Containers/Microsoft.NET.Build.Containers/Registry/DefaultManifestOperations.cs b/src/Containers/Microsoft.NET.Build.Containers/Registry/DefaultManifestOperations.cs index 363df11b416d..ce7325ff4dfb 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Registry/DefaultManifestOperations.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/Registry/DefaultManifestOperations.cs @@ -1,7 +1,7 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// -using System.Net; using System.Net.Http.Headers; using System.Text.Json; using Microsoft.Extensions.Logging; @@ -14,14 +14,12 @@ internal class DefaultManifestOperations : IManifestOperations private readonly Uri _baseUri; private readonly HttpClient _client; private readonly ILogger _logger; - private readonly string _registryName; - internal DefaultManifestOperations(Uri baseUri, string registryName, HttpClient client, ILogger logger) + internal DefaultManifestOperations(Uri baseUri, HttpClient client, ILogger logger) { _baseUri = baseUri; _client = client; _logger = logger; - _registryName = registryName; } public async Task GetAsync(string repositoryName, string reference, CancellationToken cancellationToken) @@ -29,32 +27,21 @@ public async Task GetAsync(string repositoryName, string re cancellationToken.ThrowIfCancellationRequested(); using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, new Uri(_baseUri, $"/v2/{repositoryName}/manifests/{reference}")).AcceptManifestFormats(); HttpResponseMessage response = await _client.SendAsync(request, cancellationToken).ConfigureAwait(false); - return response.StatusCode switch - { - HttpStatusCode.OK => response, - HttpStatusCode.NotFound => throw new RepositoryNotFoundException(_registryName, repositoryName, reference), - HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden => throw new UnableToAccessRepositoryException(_registryName, repositoryName), - _ => await LogAndThrowContainerHttpException(response, cancellationToken).ConfigureAwait(false) - }; + response.EnsureSuccessStatusCode(); + return response; } - public async Task PutAsync(string repositoryName, string reference, string manifestJson, string mediaType, CancellationToken cancellationToken) + public async Task PutAsync(string repositoryName, string reference, ManifestV2 manifest, CancellationToken cancellationToken) { - HttpContent manifestUploadContent = new StringContent(manifestJson); - manifestUploadContent.Headers.ContentType = new MediaTypeHeaderValue(mediaType); + string jsonString = JsonSerializer.SerializeToNode(manifest)?.ToJsonString() ?? ""; + HttpContent manifestUploadContent = new StringContent(jsonString); + manifestUploadContent.Headers.ContentType = new MediaTypeHeaderValue(SchemaTypes.DockerManifestV2); - HttpResponseMessage putResponse = await _client.PutAsync(new Uri(_baseUri, $"/v2/{repositoryName}/manifests/{reference}"), manifestUploadContent, cancellationToken).ConfigureAwait(false); + HttpResponseMessage putResponse = await _client.PutAsync(new Uri(_baseUri, $"/v2/{repositoryName}/manifests/{reference}"), manifestUploadContent, cancellationToken).ConfigureAwait(false); if (!putResponse.IsSuccessStatusCode) { - await putResponse.LogHttpResponseAsync(_logger, cancellationToken).ConfigureAwait(false); - throw new ContainerHttpException(Resource.FormatString(nameof(Strings.RegistryPushFailed), putResponse.StatusCode), putResponse.RequestMessage?.RequestUri?.ToString(), putResponse.StatusCode); + throw new ContainerHttpException(Resource.GetString(nameof(Strings.RegistryPushFailed)), putResponse.RequestMessage?.RequestUri?.ToString(), jsonString); } } - - private async Task LogAndThrowContainerHttpException(HttpResponseMessage response, CancellationToken cancellationToken) - { - await response.LogHttpResponseAsync(_logger, cancellationToken).ConfigureAwait(false); - throw new ContainerHttpException(Resource.GetString(nameof(Strings.RegistryPullFailed)), response.RequestMessage?.RequestUri?.ToString(), response.StatusCode); - } } diff --git a/src/Containers/Microsoft.NET.Build.Containers/Registry/DefaultRegistryAPI.cs b/src/Containers/Microsoft.NET.Build.Containers/Registry/DefaultRegistryAPI.cs index d1285298ab93..b033541e712a 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Registry/DefaultRegistryAPI.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/Registry/DefaultRegistryAPI.cs @@ -1,8 +1,7 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// -using System.Net.Security; -using System.Security.Cryptography.X509Certificates; using Microsoft.Extensions.Logging; namespace Microsoft.NET.Build.Containers; @@ -13,86 +12,33 @@ internal class DefaultRegistryAPI : IRegistryAPI private readonly HttpClient _client; private readonly ILogger _logger; - // Empirical value - Unoptimized .NET application layers can be ~200MB - // * .NET Runtime (~80MB) - // * ASP.NET Runtime (~25MB) - // * application and dependencies - variable, but _probably_ not more than the BCL? - // Given a 200MB target and a 1Mb/s upload speed, we'd expect an upload speed of 27m:57s. - // Making this a round 30 for convenience. - private static TimeSpan LongRequestTimeout = TimeSpan.FromMinutes(30); - - internal DefaultRegistryAPI(string registryName, Uri baseUri, bool isInsecureRegistry, ILogger logger, RegistryMode mode) + internal DefaultRegistryAPI(Uri baseUri, ILogger logger) { + bool isAmazonECRRegistry = baseUri.IsAmazonECRRegistry(); _baseUri = baseUri; _logger = logger; - _client = CreateClient(registryName, baseUri, logger, isInsecureRegistry, mode); - Manifest = new DefaultManifestOperations(_baseUri, registryName, _client, _logger); - Blob = new DefaultBlobOperations(_baseUri, registryName, _client, _logger); + _client = CreateClient(baseUri, isAmazonECRRegistry); + Manifest = new DefaultManifestOperations(_baseUri, _client, _logger); + Blob = new DefaultBlobOperations(_baseUri, _client, _logger); } public IBlobOperations Blob { get; } public IManifestOperations Manifest { get; } - private static HttpClient CreateClient(string registryName, Uri baseUri, ILogger logger, bool isInsecureRegistry, RegistryMode mode) + private static HttpClient CreateClient(Uri baseUri, bool isAmazonECRRegistry = false) { - HttpMessageHandler innerHandler = CreateHttpHandler(registryName, baseUri, isInsecureRegistry, logger); - - HttpMessageHandler clientHandler = new AuthHandshakeMessageHandler(registryName, innerHandler, logger, mode); + HttpMessageHandler clientHandler = new AuthHandshakeMessageHandler(new SocketsHttpHandler() { PooledConnectionLifetime = TimeSpan.FromMilliseconds(10 /* total guess */) }); - if (baseUri.IsAmazonECRRegistry()) + if (isAmazonECRRegistry) { clientHandler = new AmazonECRMessageHandler(clientHandler); } - HttpClient client = new(clientHandler) - { - Timeout = LongRequestTimeout - }; + HttpClient client = new(clientHandler); client.DefaultRequestHeaders.Add("User-Agent", $".NET Container Library v{Constants.Version}"); return client; } - - private static HttpMessageHandler CreateHttpHandler(string registryName, Uri baseUri, bool allowInsecure, ILogger logger) - { - var socketsHttpHandler = new SocketsHttpHandler() - { - UseCookies = false, - // the rest of the HTTP stack has an very long timeout (see below) but we should still have a reasonable timeout for the initial connection - ConnectTimeout = TimeSpan.FromSeconds(30) - }; - - if (!allowInsecure) - { - return socketsHttpHandler; - } - - socketsHttpHandler.SslOptions = new System.Net.Security.SslClientAuthenticationOptions() - { - RemoteCertificateValidationCallback = IgnoreCertificateErrorsForSpecificHost(baseUri.Host) - }; - - return new FallbackToHttpMessageHandler(registryName, baseUri.Host, baseUri.Port, socketsHttpHandler, logger); - } - - private static RemoteCertificateValidationCallback IgnoreCertificateErrorsForSpecificHost(string host) - { - return (object sender, X509Certificate? certificate, X509Chain? chain, SslPolicyErrors sslPolicyErrors) => - { - if (sslPolicyErrors == SslPolicyErrors.None) - { - return true; - } - - // Ignore certificate errors for the hostname. - if ((sender as SslStream)?.TargetHostName == host) - { - return true; - } - - return false; - }; - } } diff --git a/src/Containers/Microsoft.NET.Build.Containers/Registry/FinalizeUploadInformation.cs b/src/Containers/Microsoft.NET.Build.Containers/Registry/FinalizeUploadInformation.cs index 8df28ab9f4a3..09d1b42eadf5 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Registry/FinalizeUploadInformation.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/Registry/FinalizeUploadInformation.cs @@ -1,5 +1,6 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// namespace Microsoft.NET.Build.Containers; diff --git a/src/Containers/Microsoft.NET.Build.Containers/Registry/HttpExtensions.cs b/src/Containers/Microsoft.NET.Build.Containers/Registry/HttpExtensions.cs index 8a3842eba8c9..58934d7f4f18 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Registry/HttpExtensions.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/Registry/HttpExtensions.cs @@ -1,27 +1,22 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// -using System.Net.Http.Headers; +using System.Text; using Microsoft.Extensions.Logging; -using NuGet.Packaging; namespace Microsoft.NET.Build.Containers; internal static class HttpExtensions { - private static readonly MediaTypeWithQualityHeaderValue[] _knownManifestFormats = [ - new("application/json"), - new(SchemaTypes.DockerManifestListV2), - new(SchemaTypes.OciImageIndexV1), - new(SchemaTypes.DockerManifestV2), - new(SchemaTypes.OciManifestV1), - new(SchemaTypes.DockerContainerV1), - ]; - internal static HttpRequestMessage AcceptManifestFormats(this HttpRequestMessage request) { request.Headers.Accept.Clear(); - request.Headers.Accept.AddRange(_knownManifestFormats); + request.Headers.Accept.Add(new("application/json")); + request.Headers.Accept.Add(new(SchemaTypes.DockerManifestListV2)); + request.Headers.Accept.Add(new(SchemaTypes.DockerManifestV2)); + request.Headers.Accept.Add(new(SchemaTypes.OciManifestV1)); + request.Headers.Accept.Add(new(SchemaTypes.DockerContainerV1)); return request; } @@ -45,7 +40,7 @@ public static Uri GetNextLocation(this HttpResponseMessage response) internal static bool IsAmazonECRRegistry(this Uri uri) { // If this the registry is to public ECR the name will contain "public.ecr.aws". - if (uri.Authority.Contains(RegistryConstants.PublicAmazonElasticContainerRegistryDomain)) + if (uri.Authority.Contains("public.ecr.aws")) { return true; } @@ -66,16 +61,13 @@ internal static bool IsAmazonECRRegistry(this Uri uri) /// internal static async Task LogHttpResponseAsync(this HttpResponseMessage response, ILogger logger, CancellationToken cancellationToken) { - if (logger.IsEnabled(LogLevel.Trace)) - { - StringBuilder s = new(); - s.AppendLine($"Request URI: {response.RequestMessage?.Method} {response.RequestMessage?.RequestUri?.ToString()}"); - s.AppendLine($"Status code: {response.StatusCode}"); - s.AppendLine($"Response headers:"); - s.AppendLine(response.Headers.ToString()); - string detail = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); - s.AppendLine($"Response content: {(string.IsNullOrWhiteSpace(detail) ? "" : detail)}"); - logger.LogTrace(s.ToString()); - } + StringBuilder s = new(); + s.AppendLine($"Last request URI: {response.RequestMessage?.RequestUri?.ToString()}"); + s.AppendLine($"Status code: {response.StatusCode}"); + s.AppendLine($"Response headers:"); + s.AppendLine(response.Headers.ToString()); + string detail = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); + s.AppendLine($"Response content: {(string.IsNullOrWhiteSpace(detail) ? "" : detail)}"); + logger.LogTrace(s.ToString()); } } diff --git a/src/Containers/Microsoft.NET.Build.Containers/Registry/IBlobOperations.cs b/src/Containers/Microsoft.NET.Build.Containers/Registry/IBlobOperations.cs index 6da654499d5b..61a579954259 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Registry/IBlobOperations.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/Registry/IBlobOperations.cs @@ -1,5 +1,6 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// using System.Text.Json.Nodes; diff --git a/src/Containers/Microsoft.NET.Build.Containers/Registry/IBlobUploadOperations.cs b/src/Containers/Microsoft.NET.Build.Containers/Registry/IBlobUploadOperations.cs index 4a4f04133ef0..5d2e2165f1b9 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Registry/IBlobUploadOperations.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/Registry/IBlobUploadOperations.cs @@ -1,5 +1,6 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// namespace Microsoft.NET.Build.Containers; diff --git a/src/Containers/Microsoft.NET.Build.Containers/Registry/IManifestOperations.cs b/src/Containers/Microsoft.NET.Build.Containers/Registry/IManifestOperations.cs index 221e2fe594a9..cec13dd68b4d 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Registry/IManifestOperations.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/Registry/IManifestOperations.cs @@ -1,5 +1,6 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// namespace Microsoft.NET.Build.Containers; @@ -13,5 +14,5 @@ internal interface IManifestOperations { public Task GetAsync(string repositoryName, string reference, CancellationToken cancellationToken); - public Task PutAsync(string repositoryName, string reference, string manifestListJson, string mediaType, CancellationToken cancellationToken); + public Task PutAsync(string repositoryName, string reference, ManifestV2 manifest, CancellationToken cancellationToken); } diff --git a/src/Containers/Microsoft.NET.Build.Containers/Registry/IRegistryAPI.cs b/src/Containers/Microsoft.NET.Build.Containers/Registry/IRegistryAPI.cs index aa6399832e64..f6efc479751b 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Registry/IRegistryAPI.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/Registry/IRegistryAPI.cs @@ -1,5 +1,6 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// namespace Microsoft.NET.Build.Containers; diff --git a/src/Containers/Microsoft.NET.Build.Containers/Registry/NextChunkUploadInformation.cs b/src/Containers/Microsoft.NET.Build.Containers/Registry/NextChunkUploadInformation.cs index 4ed499134554..3c95f7c57331 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Registry/NextChunkUploadInformation.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/Registry/NextChunkUploadInformation.cs @@ -1,5 +1,6 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// namespace Microsoft.NET.Build.Containers; diff --git a/src/Containers/Microsoft.NET.Build.Containers/Registry/Registry.cs b/src/Containers/Microsoft.NET.Build.Containers/Registry/Registry.cs index 3472ba1c53b2..21a23a38bdaf 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Registry/Registry.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/Registry/Registry.cs @@ -1,82 +1,22 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using NuGet.Packaging; -using System.Diagnostics; -using System.Net.Http.Json; -using System.Text.Json.Nodes; +using Microsoft.DotNet.Cli.Utils; using Microsoft.Extensions.Logging; using Microsoft.NET.Build.Containers.Resources; using NuGet.RuntimeModel; +using System.Diagnostics; +using System.Net.Http.Json; +using System.Text; +using System.Text.Json.Nodes; namespace Microsoft.NET.Build.Containers; -internal interface IManifestPicker -{ - public PlatformSpecificManifest? PickBestManifestForRid(IReadOnlyDictionary manifestList, string runtimeIdentifier); - public PlatformSpecificOciManifest? PickBestManifestForRid(IReadOnlyDictionary manifestList, string runtimeIdentifier); -} - -internal sealed class RidGraphManifestPicker : IManifestPicker -{ - private readonly RuntimeGraph _runtimeGraph; - - public RidGraphManifestPicker(string runtimeIdentifierGraphPath) - { - _runtimeGraph = GetRuntimeGraphForDotNet(runtimeIdentifierGraphPath); - } - public PlatformSpecificManifest? PickBestManifestForRid(IReadOnlyDictionary ridManifestDict, string runtimeIdentifier) - { - var bestManifestRid = GetBestMatchingRid(_runtimeGraph, runtimeIdentifier, ridManifestDict.Keys); - if (bestManifestRid is null) - { - return null; - } - return ridManifestDict[bestManifestRid]; - } - - public PlatformSpecificOciManifest? PickBestManifestForRid(IReadOnlyDictionary ridManifestDict, string runtimeIdentifier) - { - var bestManifestRid = GetBestMatchingRid(_runtimeGraph, runtimeIdentifier, ridManifestDict.Keys); - if (bestManifestRid is null) - { - return null; - } - return ridManifestDict[bestManifestRid]; - } - - private static string? GetBestMatchingRid(RuntimeGraph runtimeGraph, string runtimeIdentifier, IEnumerable availableRuntimeIdentifiers) - { - HashSet availableRids = new HashSet(availableRuntimeIdentifiers, StringComparer.Ordinal); - foreach (var candidateRuntimeIdentifier in runtimeGraph.ExpandRuntime(runtimeIdentifier)) - { - if (availableRids.Contains(candidateRuntimeIdentifier)) - { - return candidateRuntimeIdentifier; - } - } - - return null; - } - - private static RuntimeGraph GetRuntimeGraphForDotNet(string ridGraphPath) => JsonRuntimeFormat.ReadRuntimeGraph(ridGraphPath); - -} - -internal enum RegistryMode -{ - Push, - Pull, - PullFromOutput -} - internal sealed class Registry { private const string DockerHubRegistry1 = "registry-1.docker.io"; private const string DockerHubRegistry2 = "registry.hub.docker.com"; private static readonly int s_defaultChunkSizeBytes = 1024 * 64; - private const int MaxDownloadRetries = 5; - private readonly Func _retryDelayProvider; private readonly ILogger _logger; private readonly IRegistryAPI _registryAPI; @@ -87,41 +27,22 @@ internal sealed class Registry /// This is used in user-facing error messages, and it should match what the user would manually enter as /// part of Docker commands like `docker login`. /// - public string RegistryName { get; } - - internal Registry(string registryName, ILogger logger, IRegistryAPI registryAPI, RegistrySettings? settings = null, Func? retryDelayProvider = null) : - this(new Uri($"https://{registryName}"), logger, registryAPI, settings) - { } - - internal Registry(string registryName, ILogger logger, RegistryMode mode, RegistrySettings? settings = null) : - this(new Uri($"https://{registryName}"), logger, new RegistryApiFactory(mode), settings) - { } + public string RegistryName { get; init; } + internal Registry(Uri baseUri, ILogger logger) : this(baseUri, logger, new DefaultRegistryAPI(baseUri, logger), new RegistrySettings()) { } - internal Registry(Uri baseUri, ILogger logger, IRegistryAPI registryAPI, RegistrySettings? settings = null, Func? retryDelayProvider = null) : - this(baseUri, logger, new RegistryApiFactory(registryAPI), settings) - { } - - internal Registry(Uri baseUri, ILogger logger, RegistryMode mode, RegistrySettings? settings = null) : - this(baseUri, logger, new RegistryApiFactory(mode), settings) - { } - - private Registry(Uri baseUri, ILogger logger, RegistryApiFactory factory, RegistrySettings? settings = null, Func? retryDelayProvider = null) + internal Registry(Uri baseUri, ILogger logger, IRegistryAPI registryAPI, RegistrySettings settings) { + _logger = logger; + _registryAPI = registryAPI; + _settings = settings; RegistryName = DeriveRegistryName(baseUri); - + BaseUri = baseUri; // "docker.io" is not a real registry. Replace the uri to refer to an actual registry. - if (baseUri.Host == ContainerHelpers.DockerRegistryAlias) + if (BaseUri.Host == ContainerHelpers.DockerRegistryAlias) { - baseUri = new UriBuilder(baseUri.ToString()) { Host = DockerHubRegistry1 }.Uri; + BaseUri = new UriBuilder(BaseUri.ToString()) { Host = DockerHubRegistry1 }.Uri; } - BaseUri = baseUri; - - _logger = logger; - _settings = settings ?? new RegistrySettings(RegistryName); - _registryAPI = factory.Create(RegistryName, BaseUri, logger, _settings.IsInsecure); - - _retryDelayProvider = retryDelayProvider ?? (() => TimeSpan.FromSeconds(1)); } private static string DeriveRegistryName(Uri baseUri) @@ -129,7 +50,7 @@ private static string DeriveRegistryName(Uri baseUri) var port = baseUri.Port == -1 ? string.Empty : $":{baseUri.Port}"; if (baseUri.OriginalString.EndsWith(port, ignoreCase: true, culture: null)) { - // the port was part of the original assignment, so it's ok to consider it part of the 'name' + // the port was part of the original assignment, so it's ok to consider it part of the 'name return baseUri.GetComponents(UriComponents.HostAndPort, UriFormat.Unescaped); } else @@ -154,12 +75,7 @@ private static string DeriveRegistryName(Uri baseUri) /// /// Check to see if the registry is GitHub Packages, which always uses ghcr.io. /// - public bool IsGithubPackageRegistry => RegistryName.StartsWith(RegistryConstants.GitHubPackageRegistryDomain, StringComparison.Ordinal); - - /// - /// Is this registry the public Microsoft Container Registry. - /// - public bool IsMcr => RegistryName.Equals(RegistryConstants.MicrosoftContainerRegistryDomain, StringComparison.Ordinal); + public bool IsGithubPackageRegistry => RegistryName.StartsWith("ghcr.io", StringComparison.Ordinal); /// /// Check to see if the registry is Docker Hub, which uses two well-known domains. @@ -174,45 +90,33 @@ private static string DeriveRegistryName(Uri baseUri) /// /// Google Artifact Registry locations (one for each availability zone) are of the form "ZONE-docker.pkg.dev". /// - public bool IsGoogleArtifactRegistry - { + public bool IsGoogleArtifactRegistry { get => RegistryName.EndsWith("-docker.pkg.dev", StringComparison.Ordinal); } - public bool IsAzureContainerRegistry => RegistryName.EndsWith(".azurecr.io", StringComparison.OrdinalIgnoreCase); - /// /// Pushing to ECR uses a much larger chunk size. To avoid getting too many socket disconnects trying to do too many /// parallel uploads be more conservative and upload one layer at a time. /// private bool SupportsParallelUploads => !IsAmazonECRRegistry && _settings.ParallelUploadEnabled; - public async Task GetImageManifestAsync(string repositoryName, string reference, string runtimeIdentifier, IManifestPicker manifestPicker, CancellationToken cancellationToken) + public async Task GetImageManifestAsync(string repositoryName, string reference, string runtimeIdentifier, string runtimeIdentifierGraphPath, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - using HttpResponseMessage initialManifestResponse = await _registryAPI.Manifest.GetAsync(repositoryName, reference, cancellationToken).ConfigureAwait(false); + HttpResponseMessage initialManifestResponse = await _registryAPI.Manifest.GetAsync(repositoryName, reference, cancellationToken).ConfigureAwait(false); return initialManifestResponse.Content.Headers.ContentType?.MediaType switch { SchemaTypes.DockerManifestV2 or SchemaTypes.OciManifestV1 => await ReadSingleImageAsync( repositoryName, - await ReadManifest().ConfigureAwait(false), - initialManifestResponse.Content.Headers.ContentType.MediaType, + await initialManifestResponse.Content.ReadFromJsonAsync(cancellationToken: cancellationToken).ConfigureAwait(false), cancellationToken).ConfigureAwait(false), SchemaTypes.DockerManifestListV2 => await PickBestImageFromManifestListAsync( repositoryName, reference, await initialManifestResponse.Content.ReadFromJsonAsync(cancellationToken: cancellationToken).ConfigureAwait(false), runtimeIdentifier, - manifestPicker, - cancellationToken).ConfigureAwait(false), - SchemaTypes.OciImageIndexV1 => - await PickBestImageFromImageIndexAsync( - repositoryName, - reference, - await initialManifestResponse.Content.ReadFromJsonAsync(cancellationToken: cancellationToken).ConfigureAwait(false), - runtimeIdentifier, - manifestPicker, + runtimeIdentifierGraphPath, cancellationToken).ConfigureAwait(false), var unknownMediaType => throw new NotImplementedException(Resource.FormatString( nameof(Strings.UnknownMediaType), @@ -221,32 +125,9 @@ await initialManifestResponse.Content.ReadFromJsonAsync(cancellati BaseUri, unknownMediaType)) }; - - async Task ReadManifest() - { - initialManifestResponse.Headers.TryGetValues("Docker-Content-Digest", out var knownDigest); - var manifest = (await initialManifestResponse.Content.ReadFromJsonAsync(cancellationToken: cancellationToken).ConfigureAwait(false))!; - if (knownDigest?.FirstOrDefault() is string knownDigestValue) - { - manifest.KnownDigest = knownDigestValue; - } - return manifest; - } } - internal async Task GetManifestListAsync(string repositoryName, string reference, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - using HttpResponseMessage initialManifestResponse = await _registryAPI.Manifest.GetAsync(repositoryName, reference, cancellationToken).ConfigureAwait(false); - - return initialManifestResponse.Content.Headers.ContentType?.MediaType switch - { - SchemaTypes.DockerManifestListV2 => await initialManifestResponse.Content.ReadFromJsonAsync(cancellationToken: cancellationToken).ConfigureAwait(false), - _ => null - }; - } - - private async Task ReadSingleImageAsync(string repositoryName, ManifestV2 manifest, string manifestMediaType, CancellationToken cancellationToken) + private async Task ReadSingleImageAsync(string repositoryName, ManifestV2 manifest, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); ManifestConfig config = manifest.Config; @@ -255,37 +136,52 @@ private async Task ReadSingleImageAsync(string repositoryName, Man JsonNode configDoc = await _registryAPI.Blob.GetJsonAsync(repositoryName, configSha, cancellationToken).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); - // ManifestV2.MediaType can be null, so we also provide manifest mediaType from http response - return new ImageBuilder(manifest, manifest.MediaType ?? manifestMediaType, new ImageConfig(configDoc), _logger); + return new ImageBuilder(manifest, new ImageConfig(configDoc)); } - - private static IReadOnlyDictionary GetManifestsByRid(PlatformSpecificManifest[] manifestList) + private async Task PickBestImageFromManifestListAsync( + string repositoryName, + string reference, + ManifestListV2 manifestList, + string runtimeIdentifier, + string runtimeIdentifierGraphPath, + CancellationToken cancellationToken) { - var ridDict = new Dictionary(); - foreach (var manifest in manifestList) - { - if (CreateRidForPlatform(manifest.platform) is { } rid) - { - ridDict.TryAdd(rid, manifest); - } + cancellationToken.ThrowIfCancellationRequested(); + var runtimeGraph = GetRuntimeGraphForDotNet(runtimeIdentifierGraphPath); + var (ridDict, graphForManifestList) = ConstructRuntimeGraphForManifestList(manifestList, runtimeGraph); + var bestManifestRid = CheckIfRidExistsInGraph(graphForManifestList, ridDict.Keys, runtimeIdentifier); + if (bestManifestRid is null) { + throw new BaseImageNotFoundException(runtimeIdentifier, repositoryName, reference, graphForManifestList.Runtimes.Keys); } + PlatformSpecificManifest matchingManifest = ridDict[bestManifestRid]; + HttpResponseMessage manifestResponse = await _registryAPI.Manifest.GetAsync(repositoryName, matchingManifest.digest, cancellationToken).ConfigureAwait(false); - return ridDict; + cancellationToken.ThrowIfCancellationRequested(); + + return await ReadSingleImageAsync( + repositoryName, + await manifestResponse.Content.ReadFromJsonAsync(cancellationToken: cancellationToken).ConfigureAwait(false), + cancellationToken).ConfigureAwait(false); } - private static IReadOnlyDictionary GetManifestsByRid(PlatformSpecificOciManifest[] manifestList) + private static string? CheckIfRidExistsInGraph(RuntimeGraph graphForManifestList, IEnumerable leafRids, string userRid) => leafRids.FirstOrDefault(leaf => graphForManifestList.AreCompatible(leaf, userRid)); + + private (IReadOnlyDictionary, RuntimeGraph) ConstructRuntimeGraphForManifestList(ManifestListV2 manifestList, RuntimeGraph dotnetRuntimeGraph) { - var ridDict = new Dictionary(); - foreach (var manifest in manifestList) - { + var ridDict = new Dictionary(); + var runtimeDescriptionSet = new HashSet(); + foreach (var manifest in manifestList.manifests) { if (CreateRidForPlatform(manifest.platform) is { } rid) { - ridDict.TryAdd(rid, manifest); + if (ridDict.TryAdd(rid, manifest)) { + AddRidAndDescendantsToSet(runtimeDescriptionSet, rid, dotnetRuntimeGraph); + } } } - return ridDict; + var graph = new RuntimeGraph(runtimeDescriptionSet); + return (ridDict, graph); } private static string? CreateRidForPlatform(PlatformInformation platform) @@ -301,7 +197,7 @@ private static IReadOnlyDictionary GetManif // TODO: we _may_ need OS-specific version parsing. Need to do more research on what the field looks like across more manifest lists. var versionPart = platform.version?.Split('.') switch { - [var major, ..] => major, + [var major, .. ] => major, _ => null }; var platformPart = platform.architecture switch @@ -312,8 +208,6 @@ private static IReadOnlyDictionary GetManif "arm64" => "arm64", "ppc64le" => "ppc64le", "s390x" => "s390x", - "riscv64" => "riscv64", - "loongarch64" => "loongarch64", _ => null }; @@ -321,81 +215,13 @@ private static IReadOnlyDictionary GetManif return $"{osPart}{versionPart ?? ""}-{platformPart}"; } + private static RuntimeGraph GetRuntimeGraphForDotNet(string ridGraphPath) => JsonRuntimeFormat.ReadRuntimeGraph(ridGraphPath); - private async Task PickBestImageFromManifestListAsync( - string repositoryName, - string reference, - ManifestListV2 manifestList, - string runtimeIdentifier, - IManifestPicker manifestPicker, - CancellationToken cancellationToken) + private void AddRidAndDescendantsToSet(HashSet runtimeDescriptionSet, string rid, RuntimeGraph dotnetRuntimeGraph) { - cancellationToken.ThrowIfCancellationRequested(); - var ridManifestDict = GetManifestsByRid(manifestList.manifests); - if (manifestPicker.PickBestManifestForRid(ridManifestDict, runtimeIdentifier) is PlatformSpecificManifest matchingManifest) - { - return await ReadImageFromManifest( - repositoryName, - reference, - matchingManifest.digest, - matchingManifest.mediaType, - runtimeIdentifier, - ridManifestDict.Keys, - cancellationToken); - } - else - { - throw new BaseImageNotFoundException(runtimeIdentifier, repositoryName, reference, ridManifestDict.Keys); - } - } - - private async Task PickBestImageFromImageIndexAsync( - string repositoryName, - string reference, - ImageIndexV1 index, - string runtimeIdentifier, - IManifestPicker manifestPicker, - CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - var ridManifestDict = GetManifestsByRid(index.manifests); - if (manifestPicker.PickBestManifestForRid(ridManifestDict, runtimeIdentifier) is PlatformSpecificOciManifest matchingManifest) - { - return await ReadImageFromManifest( - repositoryName, - reference, - matchingManifest.digest, - matchingManifest.mediaType, - runtimeIdentifier, - ridManifestDict.Keys, - cancellationToken); - } - else - { - throw new BaseImageNotFoundException(runtimeIdentifier, repositoryName, reference, ridManifestDict.Keys); - } - } - - private async Task ReadImageFromManifest( - string repositoryName, - string reference, - string manifestDigest, - string mediaType, - string runtimeIdentifier, - IEnumerable rids, - CancellationToken cancellationToken) - { - using HttpResponseMessage manifestResponse = await _registryAPI.Manifest.GetAsync(repositoryName, manifestDigest, cancellationToken).ConfigureAwait(false); - - cancellationToken.ThrowIfCancellationRequested(); - var manifest = await manifestResponse.Content.ReadFromJsonAsync(cancellationToken: cancellationToken).ConfigureAwait(false); - if (manifest is null) throw new BaseImageNotFoundException(runtimeIdentifier, repositoryName, reference, rids); - manifest.KnownDigest = manifestDigest; - return await ReadSingleImageAsync( - repositoryName, - manifest, - mediaType, - cancellationToken).ConfigureAwait(false); + var R = dotnetRuntimeGraph.Runtimes[rid]; + runtimeDescriptionSet.Add(R); + foreach (var r in R.InheritedRuntimes) AddRidAndDescendantsToSet(runtimeDescriptionSet, r, dotnetRuntimeGraph); } /// @@ -408,48 +234,26 @@ public async Task DownloadBlobAsync(string repository, Descriptor descri { cancellationToken.ThrowIfCancellationRequested(); string localPath = ContentStore.PathForDescriptor(descriptor); - + if (File.Exists(localPath)) { // Assume file is up to date and just return it return localPath; } - + + // No local copy, so download one + using Stream responseStream = await _registryAPI.Blob.GetStreamAsync(repository, descriptor.Digest, cancellationToken).ConfigureAwait(false); + string tempTarballPath = ContentStore.GetTempFile(); - - int retryCount = 0; - while (retryCount < MaxDownloadRetries) + using (FileStream fs = File.Create(tempTarballPath)) { - try - { - // No local copy, so download one - using Stream responseStream = await _registryAPI.Blob.GetStreamAsync(repository, descriptor.Digest, cancellationToken).ConfigureAwait(false); - - using (FileStream fs = File.Create(tempTarballPath)) - { - await responseStream.CopyToAsync(fs, cancellationToken).ConfigureAwait(false); - } - - // Break the loop if successful - break; - } - catch (Exception ex) - { - retryCount++; - if (retryCount >= MaxDownloadRetries) - { - throw new UnableToDownloadFromRepositoryException(repository); - } - - _logger.LogTrace("Download attempt {0}/{1} for repository '{2}' failed. Error: {3}", retryCount, MaxDownloadRetries, repository, ex.ToString()); - - // Wait before retrying - await Task.Delay(_retryDelayProvider(), cancellationToken).ConfigureAwait(false); - } + await responseStream.CopyToAsync(fs, cancellationToken).ConfigureAwait(false); } - + + cancellationToken.ThrowIfCancellationRequested(); + File.Move(tempTarballPath, localPath, overwrite: true); - + return localPath; } @@ -487,13 +291,13 @@ internal async Task UploadBlobChunkedAsync(Stream con int bytesRead = await contents.ReadAsync(chunkBackingStore, cancellationToken).ConfigureAwait(false); - ByteArrayContent content = new(chunkBackingStore, offset: 0, count: bytesRead); + ByteArrayContent content = new (chunkBackingStore, offset: 0, count: bytesRead); content.Headers.ContentLength = bytesRead; // manual because ACR throws an error with the .NET type {"Range":"bytes 0-84521/*","Reason":"the Content-Range header format is invalid"} // content.Headers.Add("Content-Range", $"0-{contents.Length - 1}"); Debug.Assert(content.Headers.TryAddWithoutValidation("Content-Range", $"{chunkStart}-{chunkStart + bytesRead - 1}")); - + NextChunkUploadInformation nextChunk = await _registryAPI.Blob.Upload.UploadChunkAsync(patchUri, content, cancellationToken).ConfigureAwait(false); patchUri = nextChunk.UploadUri; @@ -554,28 +358,10 @@ private async Task UploadBlobAsync(string repository, string digest, Stream cont } - public async Task PushManifestListAsync( - MultiArchImage multiArchImage, - SourceImageReference sourceImageReference, - DestinationImageReference destinationImageReference, - CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - foreach (var tag in destinationImageReference.Tags) - { - _logger.LogInformation(Strings.Registry_TagUploadStarted, tag, RegistryName); - await _registryAPI.Manifest.PutAsync(destinationImageReference.Repository, tag, multiArchImage.ImageIndex, multiArchImage.ImageIndexMediaType, cancellationToken).ConfigureAwait(false); - _logger.LogInformation(Strings.Registry_TagUploaded, tag, RegistryName); - } - } - - public Task PushAsync(BuiltImage builtImage, SourceImageReference source, DestinationImageReference destination, CancellationToken cancellationToken) - => PushAsync(builtImage, source, destination, pushTags: true, cancellationToken); - - private async Task PushAsync(BuiltImage builtImage, SourceImageReference source, DestinationImageReference destination, bool pushTags, CancellationToken cancellationToken) + public async Task PushAsync(BuiltImage builtImage, ImageReference source, ImageReference destination, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - Registry destinationRegistry = destination.RemoteRegistry!; + Registry destinationRegistry = destination.Registry!; Func uploadLayerFunc = async (descriptor) => { @@ -590,7 +376,7 @@ private async Task PushAsync(BuiltImage builtImage, SourceImageReference source, } // Blob wasn't there; can we tell the server to get it from the base image? - if (!await _registryAPI.Blob.Upload.TryMountAsync(destination.Repository, source.Repository, digest, cancellationToken).ConfigureAwait(false)) + if (! await _registryAPI.Blob.Upload.TryMountAsync(destination.Repository, source.Repository, digest, cancellationToken).ConfigureAwait(false)) { // The blob wasn't already available in another namespace, so fall back to explicitly uploading it @@ -602,8 +388,7 @@ private async Task PushAsync(BuiltImage builtImage, SourceImageReference source, await destinationRegistry.PushLayerAsync(Layer.FromDescriptor(descriptor), destination.Repository, cancellationToken).ConfigureAwait(false); _logger.LogInformation(Strings.Registry_LayerUploaded, digest, destinationRegistry.RegistryName); } - else - { + else { throw new NotImplementedException(Resource.GetString(nameof(Strings.MissingLinkToRegistry))); } } @@ -615,59 +400,30 @@ private async Task PushAsync(BuiltImage builtImage, SourceImageReference source, } else { - foreach (var descriptor in builtImage.LayerDescriptors) + foreach(var descriptor in builtImage.LayerDescriptors) { await uploadLayerFunc(descriptor).ConfigureAwait(false); } } cancellationToken.ThrowIfCancellationRequested(); - using (MemoryStream stringStream = new(Encoding.UTF8.GetBytes(builtImage.Config))) + using (MemoryStream stringStream = new MemoryStream(Encoding.UTF8.GetBytes(builtImage.Config))) { - var configDigest = builtImage.ImageDigest!; + var configDigest = builtImage.ImageDigest; _logger.LogInformation(Strings.Registry_ConfigUploadStarted, configDigest); await UploadBlobAsync(destination.Repository, configDigest, stringStream, cancellationToken).ConfigureAwait(false); _logger.LogInformation(Strings.Registry_ConfigUploaded); } - // Tags can refer to an image manifest or an image manifest list. - // In the first case, we push tags to the registry. - // In the second case, we push the manifest digest so the manifest list can refer to it. - if (pushTags) - { - Debug.Assert(destination.Tags.Length > 0); - foreach (string tag in destination.Tags) - { - _logger.LogInformation(Strings.Registry_TagUploadStarted, tag, RegistryName); - await _registryAPI.Manifest.PutAsync(destination.Repository, tag, builtImage.Manifest, builtImage.ManifestMediaType, cancellationToken).ConfigureAwait(false); - _logger.LogInformation(Strings.Registry_TagUploaded, tag, RegistryName); - } - } - else - { - _logger.LogInformation(Strings.Registry_ManifestUploadStarted, RegistryName, builtImage.ManifestDigest); - await _registryAPI.Manifest.PutAsync(destination.Repository, builtImage.ManifestDigest, builtImage.Manifest, builtImage.ManifestMediaType, cancellationToken).ConfigureAwait(false); - _logger.LogInformation(Strings.Registry_ManifestUploaded, RegistryName); - } - } - - private readonly ref struct RegistryApiFactory - { - private readonly IRegistryAPI? _registryApi; - private readonly RegistryMode? _mode; - public RegistryApiFactory(IRegistryAPI registryApi) - { - _registryApi = registryApi; - } + //manifest upload + string manifestDigest = builtImage.Manifest.GetDigest(); + _logger.LogInformation(Strings.Registry_ManifestUploadStarted, RegistryName, manifestDigest); + await _registryAPI.Manifest.PutAsync(destination.Repository, manifestDigest, builtImage.Manifest, cancellationToken).ConfigureAwait(false); + _logger.LogInformation(Strings.Registry_ManifestUploaded, RegistryName); - public RegistryApiFactory(RegistryMode mode) - { - _mode = mode; - } - - public IRegistryAPI Create(string registryName, Uri baseUri, ILogger logger, bool isInsecureRegistry) - { - return _registryApi ?? new DefaultRegistryAPI(registryName, baseUri, isInsecureRegistry, logger, _mode!.Value); - } + //tag upload + _logger.LogInformation(Strings.Registry_TagUploadStarted, destination.Tag, RegistryName); + await _registryAPI.Manifest.PutAsync(destination.Repository, destination.Tag, builtImage.Manifest, cancellationToken).ConfigureAwait(false); + _logger.LogInformation(Strings.Registry_TagUploaded, destination.Tag, RegistryName); } } diff --git a/src/Containers/Microsoft.NET.Build.Containers/Registry/RegistrySettings.cs b/src/Containers/Microsoft.NET.Build.Containers/Registry/RegistrySettings.cs index 8fc3c795093b..3ac8f41ff254 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Registry/RegistrySettings.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/Registry/RegistrySettings.cs @@ -1,5 +1,6 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// using Microsoft.DotNet.Cli.Utils; @@ -7,25 +8,6 @@ namespace Microsoft.NET.Build.Containers; internal class RegistrySettings { - public RegistrySettings(string? registryName = null, IEnvironmentProvider? environment = null) - { - environment ??= new EnvironmentProvider(); - - ChunkedUploadSizeBytes = environment.GetEnvironmentVariableAsNullableInt(EnvVariables.ChunkedUploadSizeBytes) ?? - environment.GetEnvironmentVariableAsNullableInt(EnvVariables.ChunkedUploadSizeBytesLegacy); - ForceChunkedUpload = Environment.GetEnvironmentVariable(EnvVariables.ForceChunkedUpload) is not null ? - environment.GetEnvironmentVariableAsBool(EnvVariables.ForceChunkedUpload, defaultValue: false) : - environment.GetEnvironmentVariableAsBool(EnvVariables.ForceChunkedUploadLegacy, defaultValue: false); - ParallelUploadEnabled = Environment.GetEnvironmentVariable(EnvVariables.ParallelUploadEnabled) is not null ? - environment.GetEnvironmentVariableAsBool(EnvVariables.ParallelUploadEnabled, defaultValue: true) : - environment.GetEnvironmentVariableAsBool(EnvVariables.ParallelUploadEnabledLegacy, defaultValue: true); - - if (registryName is not null) - { - IsInsecure = IsInsecureRegistry(environment, registryName); - } - } - private const int DefaultChunkSizeBytes = 1024 * 64; private const int FiveMegs = 5_242_880; @@ -35,12 +17,12 @@ public RegistrySettings(string? registryName = null, IEnvironmentProvider? envir /// /// Our default of 64KB is very conservative, so raising this to 1MB or more can speed up layer uploads reasonably well. /// - internal int? ChunkedUploadSizeBytes { get; init; } + internal int? ChunkedUploadSizeBytes { get; init; } = Env.GetEnvironmentVariableAsNullableInt(EnvVariables.ChunkedUploadSizeBytes); /// /// Allows to force chunked upload for debugging purposes. /// - internal bool ForceChunkedUpload { get; init; } + internal bool ForceChunkedUpload { get; init; } = Env.GetEnvironmentVariableAsBool(EnvVariables.ForceChunkedUpload, defaultValue: false); /// /// Whether we should upload blobs in parallel (enabled by default, but disabled for certain registries in conjunction with the explicit support check below). @@ -48,46 +30,13 @@ public RegistrySettings(string? registryName = null, IEnvironmentProvider? envir /// /// Enabling this can swamp some registries, so this is an escape hatch. /// - internal bool ParallelUploadEnabled { get; init; } - - /// - /// Allows ignoring https certificate errors and changing to http when the endpoint is not an https endpoint. - /// - internal bool IsInsecure { get; init; } + internal bool ParallelUploadEnabled { get; init; } = Env.GetEnvironmentVariableAsBool(EnvVariables.ParallelUploadEnabled, defaultValue: true); internal struct EnvVariables { - internal const string ChunkedUploadSizeBytes = "DOTNET_CONTAINER_REGISTRY_CHUNKED_UPLOAD_SIZE_BYTES"; - internal const string ChunkedUploadSizeBytesLegacy = "SDK_CONTAINER_REGISTRY_CHUNKED_UPLOAD_SIZE_BYTES"; - - internal const string ForceChunkedUpload = "DOTNET_CONTAINER_DEBUG_REGISTRY_FORCE_CHUNKED_UPLOAD"; - internal const string ForceChunkedUploadLegacy = "SDK_CONTAINER_DEBUG_REGISTRY_FORCE_CHUNKED_UPLOAD"; - internal const string ParallelUploadEnabled = "DOTNET_CONTAINER_REGISTRY_PARALLEL_UPLOAD"; - internal const string ParallelUploadEnabledLegacy = "SDK_CONTAINER_REGISTRY_PARALLEL_UPLOAD"; - - internal const string InsecureRegistries = "DOTNET_CONTAINER_INSECURE_REGISTRIES"; - } - - private static bool IsInsecureRegistry(IEnvironmentProvider environment, string registryName) - { - // Always allow insecure access to 'localhost'. - if (registryName.StartsWith("localhost:", StringComparison.OrdinalIgnoreCase) || - registryName.Equals("localhost", StringComparison.OrdinalIgnoreCase)) - { - return true; - } - - // DOTNET_CONTAINER_INSECURE_REGISTRIES is a semicolon separated list of insecure registry names. - string? insecureRegistriesEnv = environment.GetEnvironmentVariable(EnvVariables.InsecureRegistries); - if (insecureRegistriesEnv is not null) - { - string[] insecureRegistries = insecureRegistriesEnv.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); - if (Array.Exists(insecureRegistries, registry => registryName.Equals(registry, StringComparison.OrdinalIgnoreCase))) - { - return true; - } - } + internal const string ChunkedUploadSizeBytes = "SDK_CONTAINER_REGISTRY_CHUNKED_UPLOAD_SIZE_BYTES"; - return DockerCli.IsInsecureRegistry(registryName); + internal const string ForceChunkedUpload = "SDK_CONTAINER_DEBUG_REGISTRY_FORCE_CHUNKED_UPLOAD"; + internal const string ParallelUploadEnabled = "SDK_CONTAINER_REGISTRY_PARALLEL_UPLOAD"; } } diff --git a/src/Containers/Microsoft.NET.Build.Containers/Registry/SchemaTypes.cs b/src/Containers/Microsoft.NET.Build.Containers/Registry/SchemaTypes.cs index 46c3acc9a6fe..4e001e0aaed7 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Registry/SchemaTypes.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/Registry/SchemaTypes.cs @@ -1,5 +1,6 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// namespace Microsoft.NET.Build.Containers; @@ -8,11 +9,5 @@ internal class SchemaTypes internal const string DockerContainerV1 = "application/vnd.docker.container.image.v1+json"; internal const string DockerManifestListV2 = "application/vnd.docker.distribution.manifest.list.v2+json"; internal const string DockerManifestV2 = "application/vnd.docker.distribution.manifest.v2+json"; - internal const string OciManifestV1 = "application/vnd.oci.image.manifest.v1+json"; // https://containers.gitbook.io/build-containers-the-hard-way/#registry-format-oci-image-manifest - internal const string OciImageIndexV1 = "application/vnd.oci.image.index.v1+json"; - internal const string OciImageConfigV1 = "application/vnd.oci.image.config.v1+json"; - - internal const string DockerLayerGzip = "application/vnd.docker.image.rootfs.diff.tar.gzip"; - internal const string OciLayerGzipV1 = "application/vnd.oci.image.layer.v1.tar+gzip"; } diff --git a/src/Containers/Microsoft.NET.Build.Containers/Registry/StartUploadInformation.cs b/src/Containers/Microsoft.NET.Build.Containers/Registry/StartUploadInformation.cs index db561bce7ab4..39dbc41e74a9 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Registry/StartUploadInformation.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/Registry/StartUploadInformation.cs @@ -1,5 +1,6 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// namespace Microsoft.NET.Build.Containers; diff --git a/src/Containers/Microsoft.NET.Build.Containers/Resources/Resource.cs b/src/Containers/Microsoft.NET.Build.Containers/Resources/Resource.cs index 4991dc9083e1..80cd22993ba3 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Resources/Resource.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/Resources/Resource.cs @@ -21,7 +21,7 @@ namespace Microsoft.NET.Build.Containers.Resources /// internal static class Resource { - internal static readonly ResourceManager Manager = new(typeof(Strings).FullName!, typeof(Resource).GetTypeInfo().Assembly); + internal static readonly ResourceManager Manager = new ResourceManager(typeof(Strings).FullName!, typeof(Resource).GetTypeInfo().Assembly); /// /// Looks up a resource value for a particular name. Looks in the CurrentUICulture, and if not found, all parent CultureInfos. diff --git a/src/Containers/Microsoft.NET.Build.Containers/Resources/Strings.Designer.cs b/src/Containers/Microsoft.NET.Build.Containers/Resources/Strings.Designer.cs index bcf850cdb63b..22f193788220 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Resources/Strings.Designer.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/Resources/Strings.Designer.cs @@ -10,8 +10,8 @@ namespace Microsoft.NET.Build.Containers.Resources { using System; - - + + /// /// A strongly-typed resource class, for looking up localized strings, etc. /// @@ -23,15 +23,15 @@ namespace Microsoft.NET.Build.Containers.Resources { [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class Strings { - + private static global::System.Resources.ResourceManager resourceMan; - + private static global::System.Globalization.CultureInfo resourceCulture; - + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] internal Strings() { } - + /// /// Returns the cached ResourceManager instance used by this class. /// @@ -45,7 +45,7 @@ internal Strings() { return resourceMan; } } - + /// /// Overrides the current thread's CurrentUICulture property for all /// resource lookups using this strongly typed resource class. @@ -59,7 +59,7 @@ internal Strings() { resourceCulture = value; } } - + /// /// Looks up a localized string similar to CONTAINER0000: Value for unit test {0}. /// @@ -68,7 +68,7 @@ internal static string _Test { return ResourceManager.GetString("_Test", resourceCulture); } } - + /// /// Looks up a localized string similar to CONTAINER1002: Request to Amazon Elastic Container Registry failed prematurely. This is often caused when the target repository does not exist in the registry.. /// @@ -77,7 +77,7 @@ internal static string AmazonRegistryFailed { return ResourceManager.GetString("AmazonRegistryFailed", resourceCulture); } } - + /// /// Looks up a localized string similar to CONTAINER2008: Both {0} and {1} were provided, but only one or the other is allowed.. /// @@ -86,43 +86,7 @@ internal static string AmbiguousTags { return ResourceManager.GetString("AmbiguousTags", resourceCulture); } } - - /// - /// Looks up a localized string similar to CONTAINER2025: ContainerAppCommandArgs are provided without specifying a ContainerAppCommand.. - /// - internal static string AppCommandArgsSetNoAppCommand { - get { - return ResourceManager.GetString("AppCommandArgsSetNoAppCommand", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to CONTAINER2026: ContainerAppCommand and ContainerAppCommandArgs must be empty when ContainerAppCommandInstruction is '{0}'.. - /// - internal static string AppCommandSetNotUsed { - get { - return ResourceManager.GetString("AppCommandSetNotUsed", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to local archive at '{0}'. - /// - internal static string ArchiveRegistry_PushInfo { - get { - return ResourceManager.GetString("ArchiveRegistry_PushInfo", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to CONTAINER2022: The base image has an entrypoint that will be overwritten to start the application. Set ContainerAppCommandInstruction to 'Entrypoint' if this is desired. To preserve the base image entrypoint, set ContainerAppCommandInstruction to 'DefaultArgs'.. - /// - internal static string BaseEntrypointOverwritten { - get { - return ResourceManager.GetString("BaseEntrypointOverwritten", resourceCulture); - } - } - + /// /// Looks up a localized string similar to CONTAINER2009: Could not parse {0}: {1}. /// @@ -131,22 +95,22 @@ internal static string BaseImageNameParsingFailed { return ResourceManager.GetString("BaseImageNameParsingFailed", resourceCulture); } } - + /// - /// Looks up a localized string similar to CONTAINER2020: {0} does not specify a registry and will be pulled from Docker Hub. Please prefix the name with the image registry, for example: '{1}/<image>'.. + /// Looks up a localized string similar to CONTAINER2013: {0} had spaces in it, replacing with dashes.. /// - internal static string BaseImageNameRegistryFallback { + internal static string BaseImageNameWithSpaces { get { - return ResourceManager.GetString("BaseImageNameRegistryFallback", resourceCulture); + return ResourceManager.GetString("BaseImageNameWithSpaces", resourceCulture); } } - + /// - /// Looks up a localized string similar to CONTAINER2013: {0} had spaces in it, replacing with dashes.. + /// Looks up a localized string similar to CONTAINER2020: {0} does not specify a registry and will be pulled from Docker Hub. Please prefix the name with the image registry, for example: '{1}/'.. /// - internal static string BaseImageNameWithSpaces { + internal static string BaseImageNameRegistryFallback { get { - return ResourceManager.GetString("BaseImageNameWithSpaces", resourceCulture); + return ResourceManager.GetString("BaseImageNameRegistryFallback", resourceCulture); } } @@ -158,7 +122,7 @@ internal static string BaseImageNotFound { return ResourceManager.GetString("BaseImageNotFound", resourceCulture); } } - + /// /// Looks up a localized string similar to CONTAINER1001: Failed to upload blob using {0}; received status code '{1}'.. /// @@ -167,34 +131,25 @@ internal static string BlobUploadFailed { return ResourceManager.GetString("BlobUploadFailed", resourceCulture); } } - - /// - /// Looks up a localized string similar to Building image index '{0}' on top of manifests {1}.. - /// - internal static string BuildingImageIndex { - get { - return ResourceManager.GetString("BuildingImageIndex", resourceCulture); - } - } - + /// - /// Looks up a localized string similar to Pushed image '{0}' to {1}.. + /// Looks up a localized string similar to Pushed container '{0}' to Docker daemon.. /// internal static string ContainerBuilder_ImageUploadedToLocalDaemon { get { return ResourceManager.GetString("ContainerBuilder_ImageUploadedToLocalDaemon", resourceCulture); } } - + /// - /// Looks up a localized string similar to Pushed image '{0}' to registry '{1}'.. + /// Looks up a localized string similar to Pushed container '{0}' to registry '{1}'.. /// internal static string ContainerBuilder_ImageUploadedToRegistry { get { return ResourceManager.GetString("ContainerBuilder_ImageUploadedToRegistry", resourceCulture); } } - + /// /// Looks up a localized string similar to Building image '{0}' with tags '{1}' on top of base image '{2}'.. /// @@ -203,25 +158,7 @@ internal static string ContainerBuilder_StartBuildingImage { return ResourceManager.GetString("ContainerBuilder_StartBuildingImage", resourceCulture); } } - - /// - /// Looks up a localized string similar to Building image '{0}' for runtime identifier '{1}' on top of base image '{2}'.. - /// - internal static string ContainerBuilder_StartBuildingImageForRid { - get { - return ResourceManager.GetString("ContainerBuilder_StartBuildingImageForRid", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to CONTAINER3001: Failed creating {0} process.. - /// - internal static string ContainerRuntimeProcessCreationFailed { - get { - return ResourceManager.GetString("ContainerRuntimeProcessCreationFailed", resourceCulture); - } - } - + /// /// Looks up a localized string similar to CONTAINER1007: Could not deserialize token from JSON.. /// @@ -230,7 +167,7 @@ internal static string CouldntDeserializeJsonToken { return ResourceManager.GetString("CouldntDeserializeJsonToken", resourceCulture); } } - + /// /// Looks up a localized string similar to CONTAINER2012: Could not recognize registry '{0}'.. /// @@ -239,16 +176,7 @@ internal static string CouldntRecognizeRegistry { return ResourceManager.GetString("CouldntRecognizeRegistry", resourceCulture); } } - - /// - /// Looks up a localized string similar to local registry via '{0}'. - /// - internal static string DockerCli_PushInfo { - get { - return ResourceManager.GetString("DockerCli_PushInfo", resourceCulture); - } - } - + /// /// Looks up a localized string similar to CONTAINER3002: Failed to get docker info({0})\n{1}\n{2}. /// @@ -257,7 +185,7 @@ internal static string DockerInfoFailed { return ResourceManager.GetString("DockerInfoFailed", resourceCulture); } } - + /// /// Looks up a localized string similar to CONTAINER3002: Failed to get docker info: {0}. /// @@ -266,7 +194,16 @@ internal static string DockerInfoFailed_Ex { return ResourceManager.GetString("DockerInfoFailed_Ex", resourceCulture); } } - + + /// + /// Looks up a localized string similar to CONTAINER3001: Failed creating docker process.. + /// + internal static string DockerProcessCreationFailed { + get { + return ResourceManager.GetString("DockerProcessCreationFailed", resourceCulture); + } + } + /// /// Looks up a localized string similar to CONTAINER4006: Property '{0}' is empty or contains whitespace and will be ignored.. /// @@ -275,7 +212,7 @@ internal static string EmptyOrWhitespacePropertyIgnored { return ResourceManager.GetString("EmptyOrWhitespacePropertyIgnored", resourceCulture); } } - + /// /// Looks up a localized string similar to CONTAINER4004: Items '{0}' contain empty item(s) which will be ignored.. /// @@ -284,52 +221,7 @@ internal static string EmptyValuesIgnored { return ResourceManager.GetString("EmptyValuesIgnored", resourceCulture); } } - - /// - /// Looks up a localized string similar to CONTAINER2023: A ContainerEntrypoint and ContainerAppCommandArgs are provided. ContainerAppInstruction must be set to configure how the application is started. Valid instructions are {0}.. - /// - internal static string EntrypointAndAppCommandArgsSetNoAppCommandInstruction { - get { - return ResourceManager.GetString("EntrypointAndAppCommandArgsSetNoAppCommandInstruction", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to CONTAINER2024: ContainerEntrypointArgs are provided without specifying a ContainerEntrypoint.. - /// - internal static string EntrypointArgsSetNoEntrypoint { - get { - return ResourceManager.GetString("EntrypointArgsSetNoEntrypoint", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to CONTAINER2029: ContainerEntrypointArgsSet are provided. Change to use ContainerAppCommandArgs for arguments that must always be set, or ContainerDefaultArgs for arguments that can be overridden when the container is created.. - /// - internal static string EntrypointArgsSetPreferAppCommandArgs { - get { - return ResourceManager.GetString("EntrypointArgsSetPreferAppCommandArgs", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to CONTAINER2028: ContainerEntrypoint can not be combined with ContainerAppCommandInstruction '{0}'.. - /// - internal static string EntrypointConflictAppCommand { - get { - return ResourceManager.GetString("EntrypointConflictAppCommand", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to CONTAINER2027: A ContainerEntrypoint is provided. ContainerAppInstruction must be set to configure how the application is started. Valid instructions are {0}.. - /// - internal static string EntrypointSetNoAppCommandInstruction { - get { - return ResourceManager.GetString("EntrypointSetNoAppCommandInstruction", resourceCulture); - } - } - + /// /// Looks up a localized string similar to CONTAINER1008: Failed retrieving credentials for "{0}": {1}. /// @@ -338,16 +230,7 @@ internal static string FailedRetrievingCredentials { return ResourceManager.GetString("FailedRetrievingCredentials", resourceCulture); } } - - /// - /// Looks up a localized string similar to CONTAINER2030: GenerateLabels was disabled but GenerateDigestLabel was enabled - no digest label will be created.. - /// - internal static string GenerateDigestLabelWithoutGenerateLabels { - get { - return ResourceManager.GetString("GenerateDigestLabelWithoutGenerateLabels", resourceCulture); - } - } - + /// /// Looks up a localized string similar to No host object detected.. /// @@ -356,25 +239,16 @@ internal static string HostObjectNotDetected { return ResourceManager.GetString("HostObjectNotDetected", resourceCulture); } } - - /// - /// Looks up a localized string similar to Pushed image index '{0}' to registry '{1}'.. - /// - internal static string ImageIndexUploadedToRegistry { - get { - return ResourceManager.GetString("ImageIndexUploadedToRegistry", resourceCulture); - } - } - + /// - /// Looks up a localized string similar to CONTAINER1009: Failed to load image from local registry. stdout: {0}. + /// Looks up a localized string similar to CONTAINER1009: Failed to load image to local registry. stdout: {0}. /// internal static string ImageLoadFailed { get { return ResourceManager.GetString("ImageLoadFailed", resourceCulture); } } - + /// /// Looks up a localized string similar to CONTAINER1010: Pulling images from local registry is not supported.. /// @@ -383,22 +257,13 @@ internal static string ImagePullNotSupported { return ResourceManager.GetString("ImagePullNotSupported", resourceCulture); } } - - /// - /// Looks up a localized string similar to Cannot create image index because no images were provided.. - /// - internal static string ImagesEmpty { - get { - return ResourceManager.GetString("ImagesEmpty", resourceCulture); - } - } - + /// - /// Looks up a localized string similar to CONTAINER2031: The container image format '{0}' is not supported. Supported formats are '{1}'.. + /// Looks up a localized string similar to CONTAINER2014: Invalid {0}: {1}.. /// - internal static string InvalidContainerImageFormat { + internal static string InvalidContainerRepository { get { - return ResourceManager.GetString("InvalidContainerImageFormat", resourceCulture); + return ResourceManager.GetString("InvalidContainerRepository", resourceCulture); } } @@ -410,88 +275,16 @@ internal static string InvalidEnvVar { return ResourceManager.GetString("InvalidEnvVar", resourceCulture); } } - - /// - /// Looks up a localized string similar to Cannot create image index because provided images are invalid. Items must have 'Config', 'Manifest', 'ManifestMediaType' and 'ManifestDigest' metadata.. - /// - internal static string InvalidImageMetadata { - get { - return ResourceManager.GetString("InvalidImageMetadata", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Cannot create image index because at least one of the provided images manifest is invalid.. - /// - internal static string InvalidImageManifest { - get { - return ResourceManager.GetString("InvalidImageManifest", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Cannot create image index because at least one of the provided images config is invalid.. - /// - internal static string InvalidImageConfig { - get { - return ResourceManager.GetString("InvalidImageConfig", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Cannot create image index because at least one of the provided images' config is missing 'architecture'.. - /// - internal static string ImageConfigMissingArchitecture { - get { - return ResourceManager.GetString("ImageConfigMissingArchitecture", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Cannot create image index because at least one of the provided images' config is missing 'os'.. - /// - internal static string ImageConfigMissingOs { - get { - return ResourceManager.GetString("ImageConfigMissingOs", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Image index creation for Podman is not supported.. - /// - internal static string ImageIndex_PodmanNotSupported { - get { - return ResourceManager.GetString("ImageIndex_PodmanNotSupported", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to CONTAINER1020: Failed to load image because containerd image store is not enabled for Docker. Tip: You can enable it by checking 'Use containerd for pulling and storing images' in Docker Desktop settings.. - /// - internal static string ImageLoadFailed_ContainerdStoreDisabled { - get { - return ResourceManager.GetString("ImageLoadFailed_ContainerdStoreDisabled", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to CONTAINER2005: The inferred image name '{0}' contains entirely invalid characters. The valid characters for an image name are alphanumeric characters, -, /, or _, and the image name must start with an alphanumeric character.. - /// - internal static string InvalidImageName_EntireNameIsInvalidCharacters { - get { - return ResourceManager.GetString("InvalidImageName_EntireNameIsInvalidCharacters", resourceCulture); - } - } - + /// - /// Looks up a localized string similar to CONTAINER2005: The first character of the image name '{0}' must be a lowercase letter or a digit and all characters in the name must be an alphanumeric character, -, /, or _.. + /// Looks up a localized string similar to CONTAINER2005: The first character of the image name must be a lowercase letter or a digit.. /// - internal static string InvalidImageName_NonAlphanumericStartCharacter { + internal static string InvalidImageName { get { - return ResourceManager.GetString("InvalidImageName_NonAlphanumericStartCharacter", resourceCulture); + return ResourceManager.GetString("InvalidImageName", resourceCulture); } } - + /// /// Looks up a localized string similar to CONTAINER2017: A ContainerPort item was provided with an invalid port number '{0}'. ContainerPort items must have an Include value that is an integer, and a Type value that is either 'tcp' or 'udp'.. /// @@ -500,7 +293,7 @@ internal static string InvalidPort_Number { return ResourceManager.GetString("InvalidPort_Number", resourceCulture); } } - + /// /// Looks up a localized string similar to CONTAINER2017: A ContainerPort item was provided with an invalid port number '{0}' and an invalid port type '{1}'. ContainerPort items must have an Include value that is an integer, and a Type value that is either 'tcp' or 'udp'.. /// @@ -509,7 +302,7 @@ internal static string InvalidPort_NumberAndType { return ResourceManager.GetString("InvalidPort_NumberAndType", resourceCulture); } } - + /// /// Looks up a localized string similar to CONTAINER2017: A ContainerPort item was provided with an invalid port type '{0}'. ContainerPort items must have an Include value that is an integer, and a Type value that is either 'tcp' or 'udp'.. /// @@ -518,7 +311,7 @@ internal static string InvalidPort_Type { return ResourceManager.GetString("InvalidPort_Type", resourceCulture); } } - + /// /// Looks up a localized string similar to CONTAINER2018: Invalid SDK prerelease version '{0}' - only 'rc' and 'preview' are supported.. /// @@ -527,7 +320,7 @@ internal static string InvalidSdkPrereleaseVersion { return ResourceManager.GetString("InvalidSdkPrereleaseVersion", resourceCulture); } } - + /// /// Looks up a localized string similar to CONTAINER2019: Invalid SDK semantic version '{0}'.. /// @@ -536,7 +329,7 @@ internal static string InvalidSdkVersion { return ResourceManager.GetString("InvalidSdkVersion", resourceCulture); } } - + /// /// Looks up a localized string similar to CONTAINER2007: Invalid {0} provided: {1}. Image tags must be alphanumeric, underscore, hyphen, or period.. /// @@ -545,7 +338,7 @@ internal static string InvalidTag { return ResourceManager.GetString("InvalidTag", resourceCulture); } } - + /// /// Looks up a localized string similar to CONTAINER2010: Invalid {0} provided: {1}. {0} must be a semicolon-delimited list of valid image tags. Image tags must be alphanumeric, underscore, hyphen, or period.. /// @@ -554,16 +347,7 @@ internal static string InvalidTags { return ResourceManager.GetString("InvalidTags", resourceCulture); } } - - /// - /// Looks up a localized string similar to Invalid string[] TargetRuntimeIdentifiers. Either all should be 'linux-musl' or none.. - /// - internal static string InvalidTargetRuntimeIdentifiers { - get { - return ResourceManager.GetString("InvalidTargetRuntimeIdentifiers", resourceCulture); - } - } - + /// /// Looks up a localized string similar to CONTAINER1003: Token response had neither token nor access_token.. /// @@ -572,7 +356,7 @@ internal static string InvalidTokenResponse { return ResourceManager.GetString("InvalidTokenResponse", resourceCulture); } } - + /// /// Looks up a localized string similar to CONTAINER4005: Item '{0}' contains items without metadata 'Value', and they will be ignored.. /// @@ -581,7 +365,16 @@ internal static string ItemsWithoutMetadata { return ResourceManager.GetString("ItemsWithoutMetadata", resourceCulture); } } - + + /// + /// Looks up a localized string similar to CONTAINER1012: The local registry is not available, but pushing to a local registry was requested. + /// + internal static string LocalRegistryNotAvailable { + get { + return ResourceManager.GetString("LocalRegistryNotAvailable", resourceCulture); + } + } + /// /// Looks up a localized string similar to Error while reading daemon config: {0}. /// @@ -590,7 +383,7 @@ internal static string LocalDocker_FailedToGetConfig { return ResourceManager.GetString("LocalDocker_FailedToGetConfig", resourceCulture); } } - + /// /// Looks up a localized string similar to The daemon server reported errors: {0}. /// @@ -599,16 +392,7 @@ internal static string LocalDocker_LocalDaemonErrors { return ResourceManager.GetString("LocalDocker_LocalDaemonErrors", resourceCulture); } } - - /// - /// Looks up a localized string similar to CONTAINER1012: The local registry is not available, but pushing to a local registry was requested.. - /// - internal static string LocalRegistryNotAvailable { - get { - return ResourceManager.GetString("LocalRegistryNotAvailable", resourceCulture); - } - } - + /// /// Looks up a localized string similar to CONTAINER2004: Unable to download layer with descriptor '{0}' from registry '{1}' because it does not exist.. /// @@ -617,7 +401,7 @@ internal static string MissingLinkToRegistry { return ResourceManager.GetString("MissingLinkToRegistry", resourceCulture); } } - + /// /// Looks up a localized string similar to CONTAINER2016: ContainerPort item '{0}' does not specify the port number. Please ensure the item's Include is a port number, for example '<ContainerPort Include="80" />'. /// @@ -626,16 +410,7 @@ internal static string MissingPortNumber { return ResourceManager.GetString("MissingPortNumber", resourceCulture); } } - - /// - /// Looks up a localized string similar to 'mediaType' of manifests should be the same in image index.. - /// - internal static string MixedMediaTypes { - get { - return ResourceManager.GetString("MixedMediaTypes", resourceCulture); - } - } - + /// /// Looks up a localized string similar to CONTAINER1004: No RequestUri specified.. /// @@ -644,7 +419,7 @@ internal static string NoRequestUriSpecified { return ResourceManager.GetString("NoRequestUriSpecified", resourceCulture); } } - + /// /// Looks up a localized string similar to '{0}' was not a valid container image name, it was normalized to '{1}'. /// @@ -653,16 +428,7 @@ internal static string NormalizedContainerName { return ResourceManager.GetString("NormalizedContainerName", resourceCulture); } } - - /// - /// Looks up a localized string similar to Unable to create tarball for oci image with multiple tags.. - /// - internal static string OciImageMultipleTagsNotSupported { - get { - return ResourceManager.GetString("OciImageMultipleTagsNotSupported", resourceCulture); - } - } - + /// /// Looks up a localized string similar to CONTAINER2011: {0} '{1}' does not exist. /// @@ -671,7 +437,7 @@ internal static string PublishDirectoryDoesntExist { return ResourceManager.GetString("PublishDirectoryDoesntExist", resourceCulture); } } - + /// /// Looks up a localized string similar to Uploaded config to registry.. /// @@ -680,7 +446,7 @@ internal static string Registry_ConfigUploaded { return ResourceManager.GetString("Registry_ConfigUploaded", resourceCulture); } } - + /// /// Looks up a localized string similar to Uploading config to registry at blob '{0}',. /// @@ -689,7 +455,7 @@ internal static string Registry_ConfigUploadStarted { return ResourceManager.GetString("Registry_ConfigUploadStarted", resourceCulture); } } - + /// /// Looks up a localized string similar to Layer '{0}' already exists.. /// @@ -698,7 +464,7 @@ internal static string Registry_LayerExists { return ResourceManager.GetString("Registry_LayerExists", resourceCulture); } } - + /// /// Looks up a localized string similar to Finished uploading layer '{0}' to '{1}'.. /// @@ -707,7 +473,7 @@ internal static string Registry_LayerUploaded { return ResourceManager.GetString("Registry_LayerUploaded", resourceCulture); } } - + /// /// Looks up a localized string similar to Uploading layer '{0}' to '{1}'.. /// @@ -716,7 +482,7 @@ internal static string Registry_LayerUploadStarted { return ResourceManager.GetString("Registry_LayerUploadStarted", resourceCulture); } } - + /// /// Looks up a localized string similar to Uploaded manifest to '{0}'.. /// @@ -725,7 +491,7 @@ internal static string Registry_ManifestUploaded { return ResourceManager.GetString("Registry_ManifestUploaded", resourceCulture); } } - + /// /// Looks up a localized string similar to Uploading manifest to registry '{0}' as blob '{1}'.. /// @@ -734,7 +500,7 @@ internal static string Registry_ManifestUploadStarted { return ResourceManager.GetString("Registry_ManifestUploadStarted", resourceCulture); } } - + /// /// Looks up a localized string similar to Uploaded tag '{0}' to '{1}'.. /// @@ -743,7 +509,7 @@ internal static string Registry_TagUploaded { return ResourceManager.GetString("Registry_TagUploaded", resourceCulture); } } - + /// /// Looks up a localized string similar to Uploading tag '{0}' to '{1}'.. /// @@ -752,16 +518,7 @@ internal static string Registry_TagUploadStarted { return ResourceManager.GetString("Registry_TagUploadStarted", resourceCulture); } } - - /// - /// Looks up a localized string similar to CONTAINER1017: Unable to communicate with the registry '{0}'.. - /// - internal static string RegistryOperationFailed { - get { - return ResourceManager.GetString("RegistryOperationFailed", resourceCulture); - } - } - + /// /// Looks up a localized string similar to CONTAINER1013: Failed to push to the output registry: {0}. /// @@ -770,34 +527,16 @@ internal static string RegistryOutputPushFailed { return ResourceManager.GetString("RegistryOutputPushFailed", resourceCulture); } } - - /// - /// Looks up a localized string similar to CONTAINER1014: Manifest pull failed.. - /// - internal static string RegistryPullFailed { - get { - return ResourceManager.GetString("RegistryPullFailed", resourceCulture); - } - } - + /// - /// Looks up a localized string similar to CONTAINER1005: Registry push failed; received status code '{0}'.. + /// Looks up a localized string similar to CONTAINER1005: Registry push failed.. /// internal static string RegistryPushFailed { get { return ResourceManager.GetString("RegistryPushFailed", resourceCulture); } } - - /// - /// Looks up a localized string similar to CONTAINER1015: Unable to access the repository '{0}' at tag '{1}' in the registry '{2}'. Please confirm that this name and tag are present in the registry.. - /// - internal static string RepositoryNotFound { - get { - return ResourceManager.GetString("RepositoryNotFound", resourceCulture); - } - } - + /// /// Looks up a localized string similar to CONTAINER4003: Required '{0}' items contain empty items.. /// @@ -806,7 +545,7 @@ internal static string RequiredItemsContainsEmptyItems { return ResourceManager.GetString("RequiredItemsContainsEmptyItems", resourceCulture); } } - + /// /// Looks up a localized string similar to CONTAINER4002: Required '{0}' items were not set.. /// @@ -815,7 +554,7 @@ internal static string RequiredItemsNotSet { return ResourceManager.GetString("RequiredItemsNotSet", resourceCulture); } } - + /// /// Looks up a localized string similar to CONTAINER4001: Required property '{0}' was not set or empty.. /// @@ -824,7 +563,7 @@ internal static string RequiredPropertyNotSetOrEmpty { return ResourceManager.GetString("RequiredPropertyNotSetOrEmpty", resourceCulture); } } - + /// /// Looks up a localized string similar to CONTAINER1006: Too many retries, stopping.. /// @@ -833,76 +572,85 @@ internal static string TooManyRetries { return ResourceManager.GetString("TooManyRetries", resourceCulture); } } - + /// - /// Looks up a localized string similar to CONTAINER1016: Unable to access the repository '{0}' in the registry '{1}'. Please confirm your credentials are correct and that you have access to this repository and registry.. + /// Looks up a localized string similar to CONTAINER2002: Unknown local registry type '{0}'. Valid local container registry types are {1}.. /// - internal static string UnableToAccessRepository { + internal static string UnknownLocalRegistryType { get { - return ResourceManager.GetString("UnableToAccessRepository", resourceCulture); + return ResourceManager.GetString("UnknownLocalRegistryType", resourceCulture); } } - + /// - /// Looks up a localized string similar to CONTAINER1018: Unable to download image from the repository '{0}'.. + /// Looks up a localized string similar to CONTAINER2003: The manifest for {0}:{1} from registry {2} was an unknown type: {3}. Please raise an issue at https://github.com/dotnet/sdk-container-builds/issues with this message.. /// - internal static string UnableToDownloadFromRepository { + internal static string UnknownMediaType { get { - return ResourceManager.GetString("UnableToDownloadFromRepository", resourceCulture); + return ResourceManager.GetString("UnknownMediaType", resourceCulture); } } /// - /// Looks up a localized string similar to CONTAINER2021: Unknown AppCommandInstruction '{0}'. Valid instructions are {1}.. + /// Looks up a localized string similar to CONTAINER2001: Unrecognized mediaType '{0}'.. /// + internal static string UnrecognizedMediaType { + get { + return ResourceManager.GetString("UnrecognizedMediaType", resourceCulture); + } + } + internal static string UnknownAppCommandInstruction { get { return ResourceManager.GetString("UnknownAppCommandInstruction", resourceCulture); } } - /// - /// Looks up a localized string similar to CONTAINER2002: Unknown local registry type '{0}'. Valid local container registry types are {1}.. - /// - internal static string UnknownLocalRegistryType { + internal static string BaseEntrypointOverwritten { get { - return ResourceManager.GetString("UnknownLocalRegistryType", resourceCulture); + return ResourceManager.GetString("BaseEntrypointOverwritten", resourceCulture); } } - /// - /// Looks up a localized string similar to CONTAINER2003: The manifest for {0}:{1} from registry {2} was an unknown type: {3}. Please raise an issue at https://github.com/dotnet/sdk-container-builds/issues with this message.. - /// - internal static string UnknownMediaType { + internal static string EntrypointAndAppCommandArgsSetNoAppCommandInstruction { get { - return ResourceManager.GetString("UnknownMediaType", resourceCulture); + return ResourceManager.GetString("EntrypointAndAppCommandArgsSetNoAppCommandInstruction", resourceCulture); } } - /// - /// Looks up a localized string similar to CONTAINER2001: Unrecognized mediaType '{0}'.. - /// - internal static string UnrecognizedMediaType { + internal static string EntrypointArgsSetNoEntrypoint { get { - return ResourceManager.GetString("UnrecognizedMediaType", resourceCulture); + return ResourceManager.GetString("EntrypointArgsSetNoEntrypoint", resourceCulture); } } - /// - /// Looks up a localized string similar to Cannot create image index for the provided 'mediaType' = '{0}'.. - /// - internal static string UnsupportedMediaType { + internal static string AppCommandArgsSetNoAppCommand { get { - return ResourceManager.GetString("UnsupportedMediaType", resourceCulture); + return ResourceManager.GetString("AppCommandArgsSetNoAppCommand", resourceCulture); } } - /// - /// Looks up a localized string similar to Unable to create tarball for mediaType '{0}'.. - /// - internal static string UnsupportedMediaTypeForTarball { + internal static string AppCommandSetNotUsed { get { - return ResourceManager.GetString("UnsupportedMediaTypeForTarball", resourceCulture); + return ResourceManager.GetString("AppCommandSetNotUsed", resourceCulture); + } + } + + internal static string EntrypointSetNoAppCommandInstruction { + get { + return ResourceManager.GetString("EntrypointSetNoAppCommandInstruction", resourceCulture); + } + } + + internal static string EntrypointConflictAppCommand { + get { + return ResourceManager.GetString("EntrypointConflictAppCommand", resourceCulture); + } + } + + internal static string EntrypointArgsSetPreferAppCommandArgs { + get { + return ResourceManager.GetString("EntrypointArgsSetPreferAppCommandArgs", resourceCulture); } } } diff --git a/src/Containers/Microsoft.NET.Build.Containers/Resources/Strings.resx b/src/Containers/Microsoft.NET.Build.Containers/Resources/Strings.resx index 05e8baba0154..6c5970fb446a 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Resources/Strings.resx +++ b/src/Containers/Microsoft.NET.Build.Containers/Resources/Strings.resx @@ -1,17 +1,17 @@  - - + - + - - - - + + + + - - + + - - + + - - - - + + + + - + - + @@ -119,291 +119,230 @@ CONTAINER1002: Request to Amazon Elastic Container Registry failed prematurely. This is often caused when the target repository does not exist in the registry. - {StrBegins="CONTAINER1002: "} + {StrBegin="CONTAINER1002: "} CONTAINER2008: Both {0} and {1} were provided, but only one or the other is allowed. - {StrBegins="CONTAINER2008: "} + {StrBegin="CONTAINER2008: "} CONTAINER2009: Could not parse {0}: {1} - {StrBegins="CONTAINER2009: "} + {StrBegin="CONTAINER2009: "} CONTAINER2013: {0} had spaces in it, replacing with dashes. - {StrBegins="CONTAINER2013: "} + {StrBegin="CONTAINER2013: "} CONTAINER2020: {0} does not specify a registry and will be pulled from Docker Hub. Please prefix the name with the image registry, for example: '{1}/<image>'. - {StrBegins="CONTAINER2020: "} + {StrBegin="CONTAINER2020: "} CONTAINER1011: Couldn't find matching base image for {0} that matches RuntimeIdentifier {1}. - {StrBegins="CONTAINER1011: "} + {StrBegin="CONTAINER1011: "} CONTAINER1001: Failed to upload blob using {0}; received status code '{1}'. - {StrBegins="CONTAINER1001: "} + {StrBegin="CONTAINER1001: "} CONTAINER1007: Could not deserialize token from JSON. - {StrBegins="CONTAINER1007: "} + {StrBegin="CONTAINER1007: "} CONTAINER2012: Could not recognize registry '{0}'. - {StrBegins="CONTAINER2012: "} + {StrBegin="CONTAINER2012: "} CONTAINER3002: Failed to get docker info({0})\n{1}\n{2} - {StrBegins="CONTAINER3002: "} + {StrBegin="CONTAINER3002: "} CONTAINER3002: Failed to get docker info: {0} - {StrBegins="CONTAINER3002: "} + {StrBegin="CONTAINER3002: "} - + CONTAINER3001: Failed creating {0} process. CONTAINER3001: {0} is the name of the command we failed to run, usually 'docker' or 'podman'. CONTAINER1010: Pulling images from local registry is not supported. - {StrBegins="CONTAINER1010: "} + {StrBegin="CONTAINER1010: "} CONTAINER4006: Property '{0}' is empty or contains whitespace and will be ignored. - {StrBegins="CONTAINER4006: "} + {StrBegin="CONTAINER4006: "} CONTAINER4004: Items '{0}' contain empty item(s) which will be ignored. - {StrBegins="CONTAINER4004: "} + {StrBegin="CONTAINER4004: "} CONTAINER1008: Failed retrieving credentials for "{0}": {1} - {StrBegins="CONTAINER1008: "} + {StrBegin="CONTAINER1008: "} No host object detected. - CONTAINER1009: Failed to load image from local registry. stdout: {0} - {StrBegins="CONTAINER1009: "} + {StrBegin="CONTAINER1009: "} - - CONTAINER1020: Failed to load image because containerd image store is not enabled for Docker. Tip: You can enable it by checking 'Use containerd for pulling and storing images' in Docker Desktop settings. - {StrBegin="CONTAINER1020: "} + + CONTAINER2014: Invalid {0}: {1}. + {StrBegin="CONTAINER2014: "} CONTAINER2015: {0}: '{1}' was not a valid Environment Variable. Ignoring. - {StrBegins="CONTAINER2015: "} + {StrBegin="CONTAINER2015: "} - - CONTAINER2005: The first character of the image name '{0}' must be a lowercase letter or a digit and all characters in the name must be an alphanumeric character, -, /, or _. - {StrBegins="CONTAINER2005: "} + + CONTAINER2005: The first character of the image name must be a lowercase letter or a digit. + {StrBegin="CONTAINER2005: "} CONTAINER2019: Invalid SDK semantic version '{0}'. - {StrBegins="CONTAINER2019: "} - - - Invalid string[] TargetRuntimeIdentifiers. Either all should be 'linux-musl' or none. + {StrBegin="CONTAINER2019: "} CONTAINER2018: Invalid SDK prerelease version '{0}' - only 'rc' and 'preview' are supported. - {StrBegins="CONTAINER2018: "} + {StrBegin="CONTAINER2018: "} CONTAINER2017: A ContainerPort item was provided with an invalid port number '{0}'. ContainerPort items must have an Include value that is an integer, and a Type value that is either 'tcp' or 'udp'. - {StrBegins="CONTAINER2017: "} + {StrBegin="CONTAINER2017: "} CONTAINER2017: A ContainerPort item was provided with an invalid port number '{0}' and an invalid port type '{1}'. ContainerPort items must have an Include value that is an integer, and a Type value that is either 'tcp' or 'udp'. - {StrBegins="CONTAINER2017: "} + {StrBegin="CONTAINER2017: "} CONTAINER2017: A ContainerPort item was provided with an invalid port type '{0}'. ContainerPort items must have an Include value that is an integer, and a Type value that is either 'tcp' or 'udp'. - {StrBegins="CONTAINER2017: "} + {StrBegin="CONTAINER2017: "} CONTAINER2007: Invalid {0} provided: {1}. Image tags must be alphanumeric, underscore, hyphen, or period. - {StrBegins="CONTAINER2007: "} + {StrBegin="CONTAINER2007: "} CONTAINER2010: Invalid {0} provided: {1}. {0} must be a semicolon-delimited list of valid image tags. Image tags must be alphanumeric, underscore, hyphen, or period. - {StrBegins="CONTAINER2010: "} + {StrBegin="CONTAINER2010: "} CONTAINER1003: Token response had neither token nor access_token. - {StrBegins="CONTAINER1003: "} + {StrBegin="CONTAINER1003: "} CONTAINER4005: Item '{0}' contains items without metadata 'Value', and they will be ignored. - {StrBegins="CONTAINER4005: "} + {StrBegin="CONTAINER4005: "} CONTAINER1012: The local registry is not available, but pushing to a local registry was requested. - {StrBegins="CONTAINER1012: "} + {StrBegin="CONTAINER1012: "} CONTAINER2004: Unable to download layer with descriptor '{0}' from registry '{1}' because it does not exist. - {StrBegins="CONTAINER2004: "} - - - Unable to create tarball for mediaType '{0}'. + {StrBegin="CONTAINER2004: "} CONTAINER2016: ContainerPort item '{0}' does not specify the port number. Please ensure the item's Include is a port number, for example '<ContainerPort Include="80" />' - {StrBegins="CONTAINER2016: "} + {StrBegin="CONTAINER2016: "} CONTAINER1004: No RequestUri specified. - {StrBegins="CONTAINER1004: "} + {StrBegin="CONTAINER1004: "} '{0}' was not a valid container image name, it was normalized to '{1}' - CONTAINER2011: {0} '{1}' does not exist - {StrBegins="CONTAINER2011: "} + {StrBegin="CONTAINER2011: "} CONTAINER1013: Failed to push to the output registry: {0} - {StrBegins="CONTAINER1013: "} + {StrBegin="CONTAINER1013: "} - CONTAINER1005: Registry push failed; received status code '{0}'. - {StrBegins="CONTAINER1005: "} + CONTAINER1005: Registry push failed. + {StrBegin="CONTAINER1005: "} CONTAINER4003: Required '{0}' items contain empty items. - {StrBegins="CONTAINER4003: "} + {StrBegin="CONTAINER4003: "} CONTAINER4002: Required '{0}' items were not set. - {StrBegins="CONTAINER4002: "} + {StrBegin="CONTAINER4002: "} CONTAINER4001: Required property '{0}' was not set or empty. - {StrBegins="CONTAINER4001: "} + {StrBegin="CONTAINER4001: "} CONTAINER1006: Too many retries, stopping. - {StrBegins="CONTAINER1006: "} + {StrBegin="CONTAINER1006: "} CONTAINER2002: Unknown local registry type '{0}'. Valid local container registry types are {1}. - {StrBegins="CONTAINER2002: "} + {StrBegin="CONTAINER2002: "} CONTAINER2003: The manifest for {0}:{1} from registry {2} was an unknown type: {3}. Please raise an issue at https://github.com/dotnet/sdk-container-builds/issues with this message. - {StrBegins="CONTAINER2003: "} + {StrBegin="CONTAINER2003: "} CONTAINER2001: Unrecognized mediaType '{0}'. - {StrBegins="CONTAINER2001: "} + {StrBegin="CONTAINER2001: "} CONTAINER2021: Unknown AppCommandInstruction '{0}'. Valid instructions are {1}. - {StrBegins="CONTAINER2021: "} + {StrBegin="CONTAINER2021: "} CONTAINER2022: The base image has an entrypoint that will be overwritten to start the application. Set ContainerAppCommandInstruction to 'Entrypoint' if this is desired. To preserve the base image entrypoint, set ContainerAppCommandInstruction to 'DefaultArgs'. - {StrBegins="CONTAINER2022: "} + {StrBegin="CONTAINER2022: "} CONTAINER2023: A ContainerEntrypoint and ContainerAppCommandArgs are provided. ContainerAppInstruction must be set to configure how the application is started. Valid instructions are {0}. - {StrBegins="CONTAINER2023: "} + {StrBegin="CONTAINER2023: "} CONTAINER2024: ContainerEntrypointArgs are provided without specifying a ContainerEntrypoint. - {StrBegins="CONTAINER2024: "} + {StrBegin="CONTAINER2024: "} CONTAINER2025: ContainerAppCommandArgs are provided without specifying a ContainerAppCommand. - {StrBegins="CONTAINER2025: "} + {StrBegin="CONTAINER2025: "} CONTAINER2026: ContainerAppCommand and ContainerAppCommandArgs must be empty when ContainerAppCommandInstruction is '{0}'. - {StrBegins="CONTAINER2026: "} + {StrBegin="CONTAINER2026: "} CONTAINER2027: A ContainerEntrypoint is provided. ContainerAppInstruction must be set to configure how the application is started. Valid instructions are {0}. - {StrBegins="CONTAINER2027: "} + {StrBegin="CONTAINER2027: "} CONTAINER2028: ContainerEntrypoint can not be combined with ContainerAppCommandInstruction '{0}'. - {StrBegins="CONTAINER2028: "} + {StrBegin="CONTAINER2028: "} CONTAINER2029: ContainerEntrypointArgsSet are provided. Change to use ContainerAppCommandArgs for arguments that must always be set, or ContainerDefaultArgs for arguments that can be overridden when the container is created. - {StrBegins="CONTAINER2029: "} + {StrBegin="CONTAINER2029: "} CONTAINER0000: Value for unit test {0} Used only for unit tests - Pushed image '{0}' to {1}. - + Pushed container '{0}' to Docker daemon. - Pushed image '{0}' to registry '{1}'. - - - - Pushed image index '{0}' to registry '{1}'. - + Pushed container '{0}' to registry '{1}'. Building image '{0}' with tags '{1}' on top of base image '{2}'. - - - - Building image '{0}' for runtime identifier '{1}' on top of base image '{2}'. - - - - Building image index '{0}' on top of manifests {1}. - - {0} is the name of the image index and its tag, {1} is the list of manifests digests - - - - 'mediaType' of manifests should be the same in image index. - - - - Cannot create image index for the provided 'mediaType' = '{0}'. - - - - Cannot create image index because no images were provided. - - - - Cannot create image index because provided images are invalid. Items must have 'Config', 'Manifest', 'ManifestMediaType' and 'ManifestDigest' metadata. - - - - Cannot create image index because at least one of the provided images' manifest is invalid. - - - - Cannot create image index because at least one of the provided images' config is invalid. - - - - Cannot create image index because at least one of the provided images' config is missing 'architecture'. - - - - Cannot create image index because at least one of the provided images' config is missing 'os'. - - - - Image index creation for Podman is not supported. - Error while reading daemon config: {0} @@ -415,15 +354,12 @@ Uploaded config to registry. - Uploading config to registry at blob '{0}', - Layer '{0}' already exists. - Finished uploading layer '{0}' to '{1}'. @@ -449,44 +385,4 @@ Uploading tag '{0}' to '{1}'. {1} is the registry name - - CONTAINER1014: Manifest pull failed. - {StrBegins="CONTAINER1014: "} - - - CONTAINER1017: Unable to communicate with the registry '{0}'. - {StrBegins="CONTAINER1017:" } - - - CONTAINER1015: Unable to access the repository '{0}' at tag '{1}' in the registry '{2}'. Please confirm that this name and tag are present in the registry. - {StrBegins="CONTAINER1015: "} - - - CONTAINER1018: Unable to download image from the repository '{0}'. - {StrBegins="CONTAINER1018:" } - - - CONTAINER1016: Unable to access the repository '{0}' in the registry '{1}'. Please confirm your credentials are correct and that you have access to this repository and registry. - {StrBegins="CONTAINER1016:" } - - - CONTAINER2005: The inferred image name '{0}' contains entirely invalid characters. The valid characters for an image name are alphanumeric characters, -, /, or _, and the image name must start with an alphanumeric character. - {StrBegins="CONTAINER2005: "} - - - local archive at '{0}' - {0} is the path to the file written - - - local registry via '{0}' - {0} is the command used - - - CONTAINER2030: GenerateLabels was disabled but GenerateDigestLabel was enabled - no digest label will be created. - {StrBegins="CONTAINER2030: "} - - - CONTAINER2031: The container image format '{0}' is not supported. Supported formats are '{1}'. - {StrBegins="CONTAINER2031: "} - - + \ No newline at end of file diff --git a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.cs.xlf b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.cs.xlf index d5ed687ee973..1e1bc1947f4c 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.cs.xlf +++ b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.cs.xlf @@ -5,73 +5,61 @@ CONTAINER1002: Request to Amazon Elastic Container Registry failed prematurely. This is often caused when the target repository does not exist in the registry. CONTAINER1002: Požadavek na Amazon Elastic Container Registry předčasně selhal. To je často způsobeno tím, že cílové úložiště v registru neexistuje. - {StrBegins="CONTAINER1002: "} + {StrBegin="CONTAINER1002: "} CONTAINER2008: Both {0} and {1} were provided, but only one or the other is allowed. CONTAINER2008: Byly poskytnuty {0} i {1}, ale je povolen pouze jeden nebo druhý. - {StrBegins="CONTAINER2008: "} + {StrBegin="CONTAINER2008: "} CONTAINER2025: ContainerAppCommandArgs are provided without specifying a ContainerAppCommand. - CONTAINER2025: ContainerAppCommandArgs se poskytují bez zadání ContainerAppCommand. - {StrBegins="CONTAINER2025: "} + CONTAINER2025: ContainerAppCommandArgs are provided without specifying a ContainerAppCommand. + {StrBegin="CONTAINER2025: "} CONTAINER2026: ContainerAppCommand and ContainerAppCommandArgs must be empty when ContainerAppCommandInstruction is '{0}'. - CONTAINER2026: ContainerAppCommand a ContainerAppCommandArgs musí být prázdné, pokud je ContainerAppCommandInstruction „{0}“. - {StrBegins="CONTAINER2026: "} - - - local archive at '{0}' - místní archiv v {0} - {0} is the path to the file written + CONTAINER2026: ContainerAppCommand and ContainerAppCommandArgs must be empty when ContainerAppCommandInstruction is '{0}'. + {StrBegin="CONTAINER2026: "} CONTAINER2022: The base image has an entrypoint that will be overwritten to start the application. Set ContainerAppCommandInstruction to 'Entrypoint' if this is desired. To preserve the base image entrypoint, set ContainerAppCommandInstruction to 'DefaultArgs'. - CONTAINER2022: Základní image má vstupní bod, který se přepíše, aby se aplikace spustila. Pokud je to žádoucí, nastavte ContainerAppCommandInstruction na „Entrypoint“. Pokud chcete zachovat vstupní bod základní image, nastavte ContainerAppCommandInstruction na „DefaultArgs“. - {StrBegins="CONTAINER2022: "} + CONTAINER2022: The base image has an entrypoint that will be overwritten to start the application. Set ContainerAppCommandInstruction to 'Entrypoint' if this is desired. To preserve the base image entrypoint, set ContainerAppCommandInstruction to 'DefaultArgs'. + {StrBegin="CONTAINER2022: "} CONTAINER2009: Could not parse {0}: {1} CONTAINER2009: Nelze analyzovat {0}: {1} - {StrBegins="CONTAINER2009: "} + {StrBegin="CONTAINER2009: "} CONTAINER2020: {0} does not specify a registry and will be pulled from Docker Hub. Please prefix the name with the image registry, for example: '{1}/<image>'. - CONTAINER2020: {0} neurčuje registr a bude načten z Docker Hub. Před název zadejte registr imagí, například: „{1}/<image>“. - {StrBegins="CONTAINER2020: "} + CONTAINER2020: {0} does not specify a registry and will be pulled from Docker Hub. Please prefix the name with the image registry, for example: '{1}/<image>'. + {StrBegin="CONTAINER2020: "} CONTAINER2013: {0} had spaces in it, replacing with dashes. CONTAINER2013: {0} obsahoval mezery, které se nahradily pomlčkami. - {StrBegins="CONTAINER2013: "} + {StrBegin="CONTAINER2013: "} CONTAINER1011: Couldn't find matching base image for {0} that matches RuntimeIdentifier {1}. CONTAINER1011: Pro {0} nelze najít odpovídající základní image, která odpovídá identifikátoru RuntimeIdentifier {1}. - {StrBegins="CONTAINER1011: "} + {StrBegin="CONTAINER1011: "} CONTAINER1001: Failed to upload blob using {0}; received status code '{1}'. CONTAINER1001: Nepovedlo se nahrát objekt blob pomocí {0}; přijatý stavový kód {1} - {StrBegins="CONTAINER1001: "} - - - Building image index '{0}' on top of manifests {1}. - Vytváření '{0}' indexu obrázků nad manifesty {1}. - - {0} is the name of the image index and its tag, {1} is the list of manifests digests - + {StrBegin="CONTAINER1001: "} - Pushed image '{0}' to {1}. - Image {0} byla vložena do {1}. + Pushed container '{0}' to Docker daemon. + Kontejner {0} se odeslal do démona Dockeru. - Pushed image '{0}' to registry '{1}'. - Image {0} byla vložena do registru {1}. + Pushed container '{0}' to registry '{1}'. + Kontejner {0} se odeslal do registru {1}. @@ -79,220 +67,150 @@ Sestavení image {0} se značkami {1} nad základní imagí {2} - - Building image '{0}' for runtime identifier '{1}' on top of base image '{2}'. - Sestavování image {0} pro identifikátor modulu runtime {1} nad základní imagí {2}. - - CONTAINER1007: Could not deserialize token from JSON. CONTAINER1007: Nepovedlo se deserializovat token z JSON. - {StrBegins="CONTAINER1007: "} + {StrBegin="CONTAINER1007: "} CONTAINER2012: Could not recognize registry '{0}'. CONTAINER2012: Nelze rozpoznat registr '{0}'. - {StrBegins="CONTAINER2012: "} - - - local registry via '{0}' - místní registr pomocí {0} - {0} is the command used + {StrBegin="CONTAINER2012: "} CONTAINER3002: Failed to get docker info({0})\n{1}\n{2} CONTAINER3002: Nepovedlo se získat informace o dockeru ({0})\n{1}\n{2} - {StrBegins="CONTAINER3002: "} + {StrBegin="CONTAINER3002: "} CONTAINER3002: Failed to get docker info: {0} CONTAINER3002: Nepovedlo se získat informace o Dockeru: {0} - {StrBegins="CONTAINER3002: "} + {StrBegin="CONTAINER3002: "} - + CONTAINER3001: Failed creating {0} process. - CONTAINER3001: Vytvoření procesu {0} se nezdařilo. + CONTAINER3001: Failed creating {0} process. CONTAINER3001: {0} is the name of the command we failed to run, usually 'docker' or 'podman'. CONTAINER4006: Property '{0}' is empty or contains whitespace and will be ignored. CONTAINER4006: Vlastnost '{0}' je prázdná nebo obsahuje prázdné znaky a bude ignorována. - {StrBegins="CONTAINER4006: "} + {StrBegin="CONTAINER4006: "} CONTAINER4004: Items '{0}' contain empty item(s) which will be ignored. CONTAINER4004: Položky '{0}' obsahují prázdné položky, které budou ignorovány. - {StrBegins="CONTAINER4004: "} + {StrBegin="CONTAINER4004: "} CONTAINER2023: A ContainerEntrypoint and ContainerAppCommandArgs are provided. ContainerAppInstruction must be set to configure how the application is started. Valid instructions are {0}. - CONTAINER2023: Jsou k dispozici ContainerEntrypoint a ContainerAppCommandArgs. Pokud chcete nakonfigurovat způsob spuštění aplikace, musí být nastavená vlastnost ContainerAppInstruction. Platné pokyny jsou {0}. - {StrBegins="CONTAINER2023: "} + CONTAINER2023: A ContainerEntrypoint and ContainerAppCommandArgs are provided. ContainerAppInstruction must be set to configure how the application is started. Valid instructions are {0}. + {StrBegin="CONTAINER2023: "} CONTAINER2027: A ContainerEntrypoint is provided. ContainerAppInstruction must be set to configure how the application is started. Valid instructions are {0}. - CONTAINER2027: Je k dispozici ContainerEntrypoint. Pokud chcete nakonfigurovat způsob spuštění aplikace, musí být nastavená vlastnost ContainerAppInstruction. Platné pokyny jsou {0}. - {StrBegins="CONTAINER2027: "} + CONTAINER2027: A ContainerEntrypoint is provided. ContainerAppInstruction must be set to configure how the application is started. Valid instructions are {0}. + {StrBegin="CONTAINER2027: "} CONTAINER2024: ContainerEntrypointArgs are provided without specifying a ContainerEntrypoint. - CONTAINER2024: ContainerEntrypointArgs se poskytují bez zadání ContainerEntrypoint. - {StrBegins="CONTAINER2024: "} + CONTAINER2024: ContainerEntrypointArgs are provided without specifying a ContainerEntrypoint. + {StrBegin="CONTAINER2024: "} CONTAINER2029: ContainerEntrypointArgsSet are provided. Change to use ContainerAppCommandArgs for arguments that must always be set, or ContainerDefaultArgs for arguments that can be overridden when the container is created. - CONTAINER2029: Je k dispozici containerEntrypointArgsSet. Proveďte změnu, aby se pro argumenty, které musí být vždycky nastavené, používaly ContainerAppCommandArgs, nebo ContainerDefaultArgs pro argumenty, které se dají přepsat při vytvoření kontejneru. - {StrBegins="CONTAINER2029: "} + CONTAINER2029: ContainerEntrypointArgsSet are provided. Change to use ContainerAppCommandArgs for arguments that must always be set, or ContainerDefaultArgs for arguments that can be overridden when the container is created. + {StrBegin="CONTAINER2029: "} CONTAINER2028: ContainerEntrypoint can not be combined with ContainerAppCommandInstruction '{0}'. - CONTAINER2028: ContainerEntrypoint nejde kombinovat s ContainerAppCommandInstruction „{0}“. - {StrBegins="CONTAINER2028: "} + CONTAINER2028: ContainerEntrypoint can not be combined with ContainerAppCommandInstruction '{0}'. + {StrBegin="CONTAINER2028: "} CONTAINER1008: Failed retrieving credentials for "{0}": {1} CONTAINER1008: Načtení přihlašovacích údajů pro „{0}“ se nezdařilo: {1} - {StrBegins="CONTAINER1008: "} - - - CONTAINER2030: GenerateLabels was disabled but GenerateDigestLabel was enabled - no digest label will be created. - CONTAINER2030: GenerateLabels bylo zakázáno, ale bylo povoleno GenerateDigestLabel – nebude vytvořen žádný popisek přehledu. - {StrBegins="CONTAINER2030: "} + {StrBegin="CONTAINER1008: "} No host object detected. Nebyl zjištěn žádný objekt hostitele. - - Cannot create image index because at least one of the provided images' config is missing 'architecture'. - Index image nejde vytvořit, protože v konfiguraci aspoň jedné ze zadaných imagí chybí architecture. - - - - Cannot create image index because at least one of the provided images' config is missing 'os'. - Index image nejde vytvořit, protože v konfiguraci aspoň jedné ze zadaných imagí chybí os. - - - - Pushed image index '{0}' to registry '{1}'. - Image s indexem {0} byla vložena do registru {1}. - - - - Image index creation for Podman is not supported. - Vytvoření indexu image pro Podman se nepodporuje. - - CONTAINER1009: Failed to load image from local registry. stdout: {0} - CONTAINER1009: Nepodařilo se načíst bitovou kopii z místního registru. stdout: {0} - {StrBegins="CONTAINER1009: "} - - - CONTAINER1020: Failed to load image because containerd image store is not enabled for Docker. Tip: You can enable it by checking 'Use containerd for pulling and storing images' in Docker Desktop settings. - CONTAINER1020: Image se nepovedlo načíst, protože pro Docker není povolené úložiště kontejnerových imagí. Tip: Tuto možnost můžete povolit tak, že v nastavení Docker Desktopu zaškrtnete Použít kontejnery pro přijímání a ukládání imagí. - {StrBegin="CONTAINER1020: "} + CONTAINER1009: Nepovedlo se načíst image do místního procesu démon Dockeru. stdout: {0} + {StrBegin="CONTAINER1009: "} CONTAINER1010: Pulling images from local registry is not supported. - CONTAINER1010: Načítání imagí z místního registru se nepodporuje. - {StrBegins="CONTAINER1010: "} + CONTAINER1010: Pulling images from local registry is not supported. + {StrBegin="CONTAINER1010: "} - - Cannot create image index because no images were provided. - Index image nelze vytvořit, protože nebyly zadány žádné obrázky. - - - - CONTAINER2031: The container image format '{0}' is not supported. Supported formats are '{1}'. - CONTAINER2031: '{0}' formátu kontejnerové image se nepodporuje. Podporované formáty jsou '{1}'. - {StrBegins="CONTAINER2031: "} + + CONTAINER2014: Invalid {0}: {1}. + CONTAINER2014: Neplatná {0}: {1}. + {StrBegin="CONTAINER2014: "} CONTAINER2015: {0}: '{1}' was not a valid Environment Variable. Ignoring. CONTAINER2015: {0}: '{1}' není platná proměnná prostředí. Ignorování. - {StrBegins="CONTAINER2015: "} + {StrBegin="CONTAINER2015: "} - - Cannot create image index because at least one of the provided images' config is invalid. - Index image nejde vytvořit, protože nejméně jedna ze zadaných konfigurací imagí je neplatná. - - - - Cannot create image index because at least one of the provided images' manifest is invalid. - Index image nejde vytvořit, protože nejméně jeden manifest poskytnutých imagí je neplatný. - - - - Cannot create image index because provided images are invalid. Items must have 'Config', 'Manifest', 'ManifestMediaType' and 'ManifestDigest' metadata. - Index image nejde vytvořit, protože zadané image nejsou platné. Položky musí mít metadata Config, Manifest, ManifestMediaType a ManifestDigest. - - - - CONTAINER2005: The inferred image name '{0}' contains entirely invalid characters. The valid characters for an image name are alphanumeric characters, -, /, or _, and the image name must start with an alphanumeric character. - CONTAINER2005: Odvozený název image „{0}“ obsahuje zcela neplatné znaky. Platné znaky pro název obrázku jsou alfanumerické znaky, -, /, nebo _, a název obrázku musí začínat alfanumerickým znakem. - {StrBegins="CONTAINER2005: "} - - - CONTAINER2005: The first character of the image name '{0}' must be a lowercase letter or a digit and all characters in the name must be an alphanumeric character, -, /, or _. - CONTAINER2005: První znak názvu obrázku „{0}“ musí být malé písmeno nebo číslice a všechny znaky v názvu musí být alfanumerické znaky, -, /, nebo _. - {StrBegins="CONTAINER2005: "} + + CONTAINER2005: The first character of the image name must be a lowercase letter or a digit. + CONTAINER2005: První znak názvu obrázku musí být malé písmeno nebo číslice. + {StrBegin="CONTAINER2005: "} CONTAINER2017: A ContainerPort item was provided with an invalid port number '{0}'. ContainerPort items must have an Include value that is an integer, and a Type value that is either 'tcp' or 'udp'. CONTAINER2017: Položka ContainerPort byla poskytnuta s neplatným číslem portu '{0}'. Položky ContainerPort musí mít hodnotu Include, která je celé číslo, a hodnotu Typu, která je buď tcp, nebo udp. - {StrBegins="CONTAINER2017: "} + {StrBegin="CONTAINER2017: "} CONTAINER2017: A ContainerPort item was provided with an invalid port number '{0}' and an invalid port type '{1}'. ContainerPort items must have an Include value that is an integer, and a Type value that is either 'tcp' or 'udp'. CONTAINER2017: Položka ContainerPort byla poskytnuta s neplatným číslem portu '{0}' a neplatným typem portu '{1}'. Položky ContainerPort musí mít hodnotu Include, která je celé číslo, a hodnotu Typu, která je buď tcp, nebo udp. - {StrBegins="CONTAINER2017: "} + {StrBegin="CONTAINER2017: "} CONTAINER2017: A ContainerPort item was provided with an invalid port type '{0}'. ContainerPort items must have an Include value that is an integer, and a Type value that is either 'tcp' or 'udp'. CONTAINER2017: Položka ContainerPort byla poskytnuta s neplatným typem portu '{0}'. Položky ContainerPort musí mít hodnotu Include, která je celé číslo, a hodnotu Typu, která je buď tcp, nebo udp. - {StrBegins="CONTAINER2017: "} + {StrBegin="CONTAINER2017: "} CONTAINER2018: Invalid SDK prerelease version '{0}' - only 'rc' and 'preview' are supported. CONTAINER2018: Neplatná verze předběžné verze sady SDK '{0}' – podporují se jen 'rc' a 'preview'. - {StrBegins="CONTAINER2018: "} + {StrBegin="CONTAINER2018: "} CONTAINER2019: Invalid SDK semantic version '{0}'. CONTAINER2019: Neplatná sémantická verze '{0}' sady SDK. - {StrBegins="CONTAINER2019: "} + {StrBegin="CONTAINER2019: "} CONTAINER2007: Invalid {0} provided: {1}. Image tags must be alphanumeric, underscore, hyphen, or period. CONTAINER2007: Byla zadána neplatná {0} : {1}. Značky obrázků musí být alfanumerické, podtržítka, spojovníky nebo tečky. - {StrBegins="CONTAINER2007: "} + {StrBegin="CONTAINER2007: "} CONTAINER2010: Invalid {0} provided: {1}. {0} must be a semicolon-delimited list of valid image tags. Image tags must be alphanumeric, underscore, hyphen, or period. CONTAINER2010: Byla zadána neplatná {0} : {1}. {0} musí být seznam platných značek obrázků oddělených středníky. Značky obrázků musí být alfanumerické, podtržítka, spojovníky nebo tečky. - {StrBegins="CONTAINER2010: "} - - - Invalid string[] TargetRuntimeIdentifiers. Either all should be 'linux-musl' or none. - Neplatný řetězec[] TargetRuntimeIdentifiers Buď by všechny měly být linux-musl, nebo žádné. - + {StrBegin="CONTAINER2010: "} CONTAINER1003: Token response had neither token nor access_token. CONTAINER1003: Odpověď tokenu neměla token ani access_token. - {StrBegins="CONTAINER1003: "} + {StrBegin="CONTAINER1003: "} CONTAINER4005: Item '{0}' contains items without metadata 'Value', and they will be ignored. CONTAINER4005: Položka '{0}' obsahuje položky bez metadat 'Value' a budou ignorovány. - {StrBegins="CONTAINER4005: "} + {StrBegin="CONTAINER4005: "} CONTAINER1012: The local registry is not available, but pushing to a local registry was requested. - CONTAINER1012: Místní registr není k dispozici, ale bylo požadováno vložení do místního registru. - {StrBegins="CONTAINER1012: "} + CONTAINER1012: The local registry is not available, but pushing to a local registry was requested. + {StrBegin="CONTAINER1012: "} Error while reading daemon config: {0} @@ -307,22 +225,17 @@ CONTAINER2004: Unable to download layer with descriptor '{0}' from registry '{1}' because it does not exist. CONTAINER2004: Nelze stáhnout vrstvu s popisovačem '{0}' z registru '{1}', protože neexistuje. - {StrBegins="CONTAINER2004: "} + {StrBegin="CONTAINER2004: "} CONTAINER2016: ContainerPort item '{0}' does not specify the port number. Please ensure the item's Include is a port number, for example '<ContainerPort Include="80" />' CONTAINER2016: Položka ContainerPort '{0}' neurčuje číslo portu. Ujistěte se prosím, že položka Include je číslo portu, například <ContainerPort Include="80" />. - {StrBegins="CONTAINER2016: "} - - - 'mediaType' of manifests should be the same in image index. - MediaType manifestů by měl být stejný v indexu image. - + {StrBegin="CONTAINER2016: "} CONTAINER1004: No RequestUri specified. CONTAINER1004: Nebyl zadán žádný identifikátor RequestUri. - {StrBegins="CONTAINER1004: "} + {StrBegin="CONTAINER1004: "} '{0}' was not a valid container image name, it was normalized to '{1}' @@ -332,27 +245,17 @@ CONTAINER2011: {0} '{1}' does not exist CONTAINER2011: {0} '{1}' neexistuje. - {StrBegins="CONTAINER2011: "} - - - CONTAINER1017: Unable to communicate with the registry '{0}'. - CONTAINER1017: Nelze komunikovat s registrem „{0}“. - {StrBegins="CONTAINER1017:" } + {StrBegin="CONTAINER2011: "} CONTAINER1013: Failed to push to the output registry: {0} CONTAINER1013: Odeslání do výstupního registru se nezdařilo: {0} - {StrBegins="CONTAINER1013: "} - - - CONTAINER1014: Manifest pull failed. - CONTAINER1014: Načtení manifestu se nezdařilo. - {StrBegins="CONTAINER1014: "} + {StrBegin="CONTAINER1013: "} - CONTAINER1005: Registry push failed; received status code '{0}'. - CONTAINER1005: Vložení registru se nezdařilo; přijal se stavový kód {0}. - {StrBegins="CONTAINER1005: "} + CONTAINER1005: Registry push failed. + CONTAINER1005: Vložení registru se nezdařilo. + {StrBegin="CONTAINER1005: "} Uploading config to registry at blob '{0}', @@ -399,70 +302,45 @@ Značka {0} se nahrála do {1}. {1} is the registry name - - CONTAINER1015: Unable to access the repository '{0}' at tag '{1}' in the registry '{2}'. Please confirm that this name and tag are present in the registry. - CONTAINER1015: Nelze získat přístup k úložišti „{0}“ ve značce „{1}“v registru „{2}“. Potvrďte prosím, že tento název a značka se nacházejí v registru. - {StrBegins="CONTAINER1015: "} - CONTAINER4003: Required '{0}' items contain empty items. CONTAINER4003: Požadované položky '{0}' obsahují prázdné položky. - {StrBegins="CONTAINER4003: "} + {StrBegin="CONTAINER4003: "} CONTAINER4002: Required '{0}' items were not set. CONTAINER4002: Požadované položky '{0}' nebyly nastaveny. - {StrBegins="CONTAINER4002: "} + {StrBegin="CONTAINER4002: "} CONTAINER4001: Required property '{0}' was not set or empty. CONTAINER4001: Požadovaná vlastnost '{0}' nebyla nastavena nebo je prázdná. - {StrBegins="CONTAINER4001: "} + {StrBegin="CONTAINER4001: "} CONTAINER1006: Too many retries, stopping. CONTAINER1006: Příliš mnoho opakovaných pokusů, zastavuje se. - {StrBegins="CONTAINER1006: "} - - - CONTAINER1016: Unable to access the repository '{0}' in the registry '{1}'. Please confirm your credentials are correct and that you have access to this repository and registry. - CONTAINER1016: Nelze získat přístup k úložišti „{0}“ v registru „{1}“. Ověřte prosím správnost vašich přihlašovacích údajů a to, že máte přístup k tomuto úložišti a registru. - {StrBegins="CONTAINER1016:" } - - - CONTAINER1018: Unable to download image from the repository '{0}'. - CONTAINER1018: Nelze stáhnout image z úložiště {0}. - {StrBegins="CONTAINER1018:" } + {StrBegin="CONTAINER1006: "} CONTAINER2021: Unknown AppCommandInstruction '{0}'. Valid instructions are {1}. - CONTAINER2021: Neznámé AppCommandInstruction „{0}“. Platné pokyny jsou {1}. - {StrBegins="CONTAINER2021: "} + CONTAINER2021: Unknown AppCommandInstruction '{0}'. Valid instructions are {1}. + {StrBegin="CONTAINER2021: "} CONTAINER2002: Unknown local registry type '{0}'. Valid local container registry types are {1}. - CONTAINER2002: Neznámý typ místního registru „{0}“. Platné typy registru místního kontejneru jsou {1}. - {StrBegins="CONTAINER2002: "} + CONTAINER2002: Unknown local registry type '{0}'. Valid local container registry types are {1}. + {StrBegin="CONTAINER2002: "} CONTAINER2003: The manifest for {0}:{1} from registry {2} was an unknown type: {3}. Please raise an issue at https://github.com/dotnet/sdk-container-builds/issues with this message. CONTAINER2003: Manifest pro {0}:{1} z registru {2} byl neznámý typ: {3}. Nahlaste prosím problém na https://github.com/dotnet/sdk-container-builds/issues s touto zprávou. - {StrBegins="CONTAINER2003: "} + {StrBegin="CONTAINER2003: "} CONTAINER2001: Unrecognized mediaType '{0}'. CONTAINER2001: Nerozpoznaný typ mediaType '{0}'. - {StrBegins="CONTAINER2001: "} - - - Cannot create image index for the provided 'mediaType' = '{0}'. - Nelze vytvořit index bitové kopie pro zadanou hodnotu mediaType = '{0}'. - - - - Unable to create tarball for mediaType '{0}'. - Nepovedlo se vytvořit tarball pro mediaType {0}. - + {StrBegin="CONTAINER2001: "} CONTAINER0000: Value for unit test {0} diff --git a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.de.xlf b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.de.xlf index 9032e7b789d2..c22bfa15ea44 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.de.xlf +++ b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.de.xlf @@ -5,73 +5,61 @@ CONTAINER1002: Request to Amazon Elastic Container Registry failed prematurely. This is often caused when the target repository does not exist in the registry. CONTAINER1002: Vorzeitiger Fehler bei der Anforderung an Amazon Elastic Container Registry. Dieser Fehler wird häufig verursacht, wenn das Ziel-Repository nicht in der Registrierung vorhanden ist. - {StrBegins="CONTAINER1002: "} + {StrBegin="CONTAINER1002: "} CONTAINER2008: Both {0} and {1} were provided, but only one or the other is allowed. CONTAINER2008: Es wurden sowohl {0} als auch {1} angegeben, es ist jedoch nur die eine oder die andere Angabe zulässig. - {StrBegins="CONTAINER2008: "} + {StrBegin="CONTAINER2008: "} CONTAINER2025: ContainerAppCommandArgs are provided without specifying a ContainerAppCommand. - CONTAINER2025: ContainerAppCommandArgs werden ohne Angabe eines ContainerAppCommand bereitgestellt. - {StrBegins="CONTAINER2025: "} + CONTAINER2025: ContainerAppCommandArgs are provided without specifying a ContainerAppCommand. + {StrBegin="CONTAINER2025: "} CONTAINER2026: ContainerAppCommand and ContainerAppCommandArgs must be empty when ContainerAppCommandInstruction is '{0}'. - CONTAINER2026: ContainerAppCommand und ContainerAppCommandArgs müssen leer sein, wenn "ContainerAppCommandInstruction" den Wert "{0}" aufweist. - {StrBegins="CONTAINER2026: "} - - - local archive at '{0}' - Lokales Archiv bei "{0}" - {0} is the path to the file written + CONTAINER2026: ContainerAppCommand and ContainerAppCommandArgs must be empty when ContainerAppCommandInstruction is '{0}'. + {StrBegin="CONTAINER2026: "} CONTAINER2022: The base image has an entrypoint that will be overwritten to start the application. Set ContainerAppCommandInstruction to 'Entrypoint' if this is desired. To preserve the base image entrypoint, set ContainerAppCommandInstruction to 'DefaultArgs'. - CONTAINER2022: Das Basisimage verfügt über einen Einstiegspunkt, der überschrieben wird, um die Anwendung zu starten. Legen Sie "ContainerAppCommandInstruction" auf "Entrypoint" fest, wenn dies gewünscht ist. Um den Einstiegspunkt des Basisimages beizubehalten, legen Sie "ContainerAppCommandInstruction" auf "DefaultArgs" fest. - {StrBegins="CONTAINER2022: "} + CONTAINER2022: The base image has an entrypoint that will be overwritten to start the application. Set ContainerAppCommandInstruction to 'Entrypoint' if this is desired. To preserve the base image entrypoint, set ContainerAppCommandInstruction to 'DefaultArgs'. + {StrBegin="CONTAINER2022: "} CONTAINER2009: Could not parse {0}: {1} CONTAINER2009: {0} konnte nicht analysiert werden: {1} - {StrBegins="CONTAINER2009: "} + {StrBegin="CONTAINER2009: "} CONTAINER2020: {0} does not specify a registry and will be pulled from Docker Hub. Please prefix the name with the image registry, for example: '{1}/<image>'. - CONTAINER2020: {0} gibt keine Registrierung an und wird aus Docker Hub abgerufen. Stellen Sie dem Namen die Imageregistrierung voran, z. B.: "{1}/<image>". - {StrBegins="CONTAINER2020: "} + CONTAINER2020: {0} does not specify a registry and will be pulled from Docker Hub. Please prefix the name with the image registry, for example: '{1}/<image>'. + {StrBegin="CONTAINER2020: "} CONTAINER2013: {0} had spaces in it, replacing with dashes. CONTAINER2013: {0} enthielt Leerzeichen. Diese werden durch Bindestriche ersetzt. - {StrBegins="CONTAINER2013: "} + {StrBegin="CONTAINER2013: "} CONTAINER1011: Couldn't find matching base image for {0} that matches RuntimeIdentifier {1}. CONTAINER1011: Es wurde kein übereinstimmendes Basisimage für {0} gefunden, das mit RuntimeIdentifier {1} übereinstimmt. - {StrBegins="CONTAINER1011: "} + {StrBegin="CONTAINER1011: "} CONTAINER1001: Failed to upload blob using {0}; received status code '{1}'. CONTAINER1001: Fehler beim Hochladen des Blobs mit {0}; der Statuscode „{1}“ wurde empfangen. - {StrBegins="CONTAINER1001: "} - - - Building image index '{0}' on top of manifests {1}. - Imageindex wird '{0}' über manifesten {1} erstellt. - - {0} is the name of the image index and its tag, {1} is the list of manifests digests - + {StrBegin="CONTAINER1001: "} - Pushed image '{0}' to {1}. - Bild "{0}" wurde in {1} gepusht. + Pushed container '{0}' to Docker daemon. + Der Container „{0}“ wurde per Push an den Docker-Daemon übertragen. - Pushed image '{0}' to registry '{1}'. - Bild "{0}" wurde in die Registrierung "{1}" gepusht. + Pushed container '{0}' to registry '{1}'. + Der Container „{0}“ wurde in die Registrierung „{1}“ gepusht. @@ -79,220 +67,150 @@ Das Image „{0}“ mit den Tags „{1}“ wird auf dem Basisimage „{2}“ erstellt. - - Building image '{0}' for runtime identifier '{1}' on top of base image '{2}'. - Das Image „{0}“ für die Laufzeitkennung „{1}“ auf dem Basisimage „{2}“ wird erstellt. - - CONTAINER1007: Could not deserialize token from JSON. CONTAINER1007: Das Token konnte nicht aus JSON deserialisiert werden. - {StrBegins="CONTAINER1007: "} + {StrBegin="CONTAINER1007: "} CONTAINER2012: Could not recognize registry '{0}'. CONTAINER2012: Die Registrierung „{0}“ wurde nicht erkannt. - {StrBegins="CONTAINER2012: "} - - - local registry via '{0}' - lokale Registrierung über "{0}" - {0} is the command used + {StrBegin="CONTAINER2012: "} CONTAINER3002: Failed to get docker info({0})\n{1}\n{2} CONTAINER3002: Fehler beim Abrufen von Docker-Informationen({0})\n{1}\n{2} - {StrBegins="CONTAINER3002: "} + {StrBegin="CONTAINER3002: "} CONTAINER3002: Failed to get docker info: {0} CONTAINER3002: Fehler beim Abrufen von Docker-Informationen: {0} - {StrBegins="CONTAINER3002: "} + {StrBegin="CONTAINER3002: "} - + CONTAINER3001: Failed creating {0} process. - CONTAINER3001: Fehler beim Erstellen des {0}-Prozesses. + CONTAINER3001: Failed creating {0} process. CONTAINER3001: {0} is the name of the command we failed to run, usually 'docker' or 'podman'. CONTAINER4006: Property '{0}' is empty or contains whitespace and will be ignored. CONTAINER4006: Die Eigenschaft „{0}“ ist leer oder enthält Leerzeichen und wird ignoriert. - {StrBegins="CONTAINER4006: "} + {StrBegin="CONTAINER4006: "} CONTAINER4004: Items '{0}' contain empty item(s) which will be ignored. CONTAINER4004: Elemente „{0}“ enthalten leere Elemente, die ignoriert werden. - {StrBegins="CONTAINER4004: "} + {StrBegin="CONTAINER4004: "} CONTAINER2023: A ContainerEntrypoint and ContainerAppCommandArgs are provided. ContainerAppInstruction must be set to configure how the application is started. Valid instructions are {0}. - CONTAINER2023: Ein ContainerEntrypoint und ContainerAppCommandArgs werden bereitgestellt. ContainerAppInstruction muss festgelegt werden, um zu konfigurieren, wie die Anwendung gestartet wird. Gültige Anweisungen sind {0}. - {StrBegins="CONTAINER2023: "} + CONTAINER2023: A ContainerEntrypoint and ContainerAppCommandArgs are provided. ContainerAppInstruction must be set to configure how the application is started. Valid instructions are {0}. + {StrBegin="CONTAINER2023: "} CONTAINER2027: A ContainerEntrypoint is provided. ContainerAppInstruction must be set to configure how the application is started. Valid instructions are {0}. - CONTAINER2027: Ein ContainerEntrypoint wird bereitgestellt. ContainerAppInstruction muss festgelegt werden, um zu konfigurieren, wie die Anwendung gestartet wird. Gültige Anweisungen sind {0}. - {StrBegins="CONTAINER2027: "} + CONTAINER2027: A ContainerEntrypoint is provided. ContainerAppInstruction must be set to configure how the application is started. Valid instructions are {0}. + {StrBegin="CONTAINER2027: "} CONTAINER2024: ContainerEntrypointArgs are provided without specifying a ContainerEntrypoint. - CONTAINER2024: ContainerEntrypointArgs werden ohne Angabe eines ContainerEntrypoint bereitgestellt. - {StrBegins="CONTAINER2024: "} + CONTAINER2024: ContainerEntrypointArgs are provided without specifying a ContainerEntrypoint. + {StrBegin="CONTAINER2024: "} CONTAINER2029: ContainerEntrypointArgsSet are provided. Change to use ContainerAppCommandArgs for arguments that must always be set, or ContainerDefaultArgs for arguments that can be overridden when the container is created. - CONTAINER2029: ContainerEntrypointArgsSet werden bereitgestellt. Ändern Sie diese Einstellung, um ContainerAppCommandArgs für Argumente zu verwenden, die immer festgelegt werden müssen, oder ContainerDefaultArgs für Argumente, die beim Erstellen des Containers überschrieben werden können. - {StrBegins="CONTAINER2029: "} + CONTAINER2029: ContainerEntrypointArgsSet are provided. Change to use ContainerAppCommandArgs for arguments that must always be set, or ContainerDefaultArgs for arguments that can be overridden when the container is created. + {StrBegin="CONTAINER2029: "} CONTAINER2028: ContainerEntrypoint can not be combined with ContainerAppCommandInstruction '{0}'. - CONTAINER2028: ContainerEntrypoint kann nicht mit ContainerAppCommandInstruction "{0}" kombiniert werden. - {StrBegins="CONTAINER2028: "} + CONTAINER2028: ContainerEntrypoint can not be combined with ContainerAppCommandInstruction '{0}'. + {StrBegin="CONTAINER2028: "} CONTAINER1008: Failed retrieving credentials for "{0}": {1} CONTAINER1008: Fehler beim Abrufen der Anmeldeinformationen für „{0}“: {1} - {StrBegins="CONTAINER1008: "} - - - CONTAINER2030: GenerateLabels was disabled but GenerateDigestLabel was enabled - no digest label will be created. - CONTAINER2030: GenerateLabels wurde deaktiviert, aber GenerateDigestLabel wurde aktiviert. Es wird keine Digestbezeichnung erstellt. - {StrBegins="CONTAINER2030: "} + {StrBegin="CONTAINER1008: "} No host object detected. Es wurde kein Hostobjekt erkannt. - - Cannot create image index because at least one of the provided images' config is missing 'architecture'. - Der Imageindex kann nicht erstellt werden, da für mindestens eine der bereitgestellten Images die Architektur fehlt. - - - - Cannot create image index because at least one of the provided images' config is missing 'os'. - Der Imageindex kann nicht erstellt werden, da in mindestens einer der bereitgestellten Images die Konfiguration "os" fehlt. - - - - Pushed image index '{0}' to registry '{1}'. - Bildindex „{0}“ wurde in die Registrierung „{1}“ gepusht. - - - - Image index creation for Podman is not supported. - Die Imageindexerstellung für Podman wird nicht unterstützt. - - CONTAINER1009: Failed to load image from local registry. stdout: {0} - CONTAINER1009: Fehler beim Laden des Images aus der lokalen Registrierung. stdout: {0} - {StrBegins="CONTAINER1009: "} - - - CONTAINER1020: Failed to load image because containerd image store is not enabled for Docker. Tip: You can enable it by checking 'Use containerd for pulling and storing images' in Docker Desktop settings. - CONTAINER1020: Fehler beim Laden des Images, weil der Containerimagespeicher für Docker nicht aktiviert ist. Tipp: Aktivieren Sie die Option, indem Sie in den Docker Desktop-Einstellungen die Option "Container zum Pullen und Speichern von Bildern verwenden" aktivieren. - {StrBegin="CONTAINER1020: "} + CONTAINER1009: Fehler beim Laden des Images in den lokalen Docker-Daemon. stdout: {0} + {StrBegin="CONTAINER1009: "} CONTAINER1010: Pulling images from local registry is not supported. - CONTAINER1010: Das Pullen von Images aus der lokalen Registrierung wird nicht unterstützt. - {StrBegins="CONTAINER1010: "} + CONTAINER1010: Pulling images from local registry is not supported. + {StrBegin="CONTAINER1010: "} - - Cannot create image index because no images were provided. - Der Imageindex kann nicht erstellt werden, da keine Bilder bereitgestellt wurden. - - - - CONTAINER2031: The container image format '{0}' is not supported. Supported formats are '{1}'. - CONTAINER2031: Das Containerimageformat '{0}' wird nicht unterstützt. Unterstützte Formate sind '{1}'. - {StrBegins="CONTAINER2031: "} + + CONTAINER2014: Invalid {0}: {1}. + CONTAINER2014: Ungültiger {0}: {1}. + {StrBegin="CONTAINER2014: "} CONTAINER2015: {0}: '{1}' was not a valid Environment Variable. Ignoring. CONTAINER2015: {0}: „{1}“ war keine gültige Umgebungsvariable. Sie wird ignoriert. - {StrBegins="CONTAINER2015: "} + {StrBegin="CONTAINER2015: "} - - Cannot create image index because at least one of the provided images' config is invalid. - Der Imageindex kann nicht erstellt werden, da mindestens eine der bereitgestellten Images ungültig ist. - - - - Cannot create image index because at least one of the provided images' manifest is invalid. - Der Imageindex kann nicht erstellt werden, da mindestens eines der bereitgestellten Images ungültig ist. - - - - Cannot create image index because provided images are invalid. Items must have 'Config', 'Manifest', 'ManifestMediaType' and 'ManifestDigest' metadata. - Der Imageindex kann nicht erstellt werden, da die angegebenen Bilder ungültig sind. Elemente müssen die Metadaten "Config", "Manifest", "ManifestMediaType" und "ManifestDigest" aufweisen. - - - - CONTAINER2005: The inferred image name '{0}' contains entirely invalid characters. The valid characters for an image name are alphanumeric characters, -, /, or _, and the image name must start with an alphanumeric character. - CONTAINER2005: Der abgeleitete Imagename '{0}' enthält vollständig ungültige Zeichen. Die gültigen Zeichen für einen Bildnamen sind alphanumerische Zeichen, -, /, oder _, und der Bildname muss mit einem alphanumerischen Zeichen beginnen. - {StrBegins="CONTAINER2005: "} - - - CONTAINER2005: The first character of the image name '{0}' must be a lowercase letter or a digit and all characters in the name must be an alphanumeric character, -, /, or _. - CONTAINER2005: Das erste Zeichen des Bildnamens "{0}" muss ein Kleinbuchstabe oder eine Ziffer sein, und alle Zeichen im Namen müssen ein alphanumerisches Zeichen, -, /oder _ sein. - {StrBegins="CONTAINER2005: "} + + CONTAINER2005: The first character of the image name must be a lowercase letter or a digit. + CONTAINER2005: Das erste Zeichen des Imagenamens muss ein Kleinbuchstabe oder eine Ziffer sein. + {StrBegin="CONTAINER2005: "} CONTAINER2017: A ContainerPort item was provided with an invalid port number '{0}'. ContainerPort items must have an Include value that is an integer, and a Type value that is either 'tcp' or 'udp'. CONTAINER2017: Ein ContainerPort-Element wurde mit einer ungültigen Portnummer „{0}“ angegeben. ContainerPort-Elemente müssen einen Include-Wert aufweisen, der eine ganze Zahl ist, und einen Type-Wert, der entweder „tcp“ oder „udp“ ist. - {StrBegins="CONTAINER2017: "} + {StrBegin="CONTAINER2017: "} CONTAINER2017: A ContainerPort item was provided with an invalid port number '{0}' and an invalid port type '{1}'. ContainerPort items must have an Include value that is an integer, and a Type value that is either 'tcp' or 'udp'. CONTAINER2017: Ein ContainerPort-Element wurde mit einer ungültigen Portnummer „{0}“ und einem ungültigen Port-Typ „{1}“ angegeben. ContainerPort-Elemente müssen einen Include-Wert aufweisen, der eine ganze Zahl ist, und einen Type-Wert, der entweder „tcp“ oder „udp“ ist. - {StrBegins="CONTAINER2017: "} + {StrBegin="CONTAINER2017: "} CONTAINER2017: A ContainerPort item was provided with an invalid port type '{0}'. ContainerPort items must have an Include value that is an integer, and a Type value that is either 'tcp' or 'udp'. CONTAINER2017: Ein ContainerPort-Element wurde mit einem ungültigen Port-Typ „{0}“ angegeben. ContainerPort-Elemente müssen einen Include-Wert aufweisen, der eine ganze Zahl ist, und einen Type-Wert, der entweder „tcp“ oder „udp“ ist. - {StrBegins="CONTAINER2017: "} + {StrBegin="CONTAINER2017: "} CONTAINER2018: Invalid SDK prerelease version '{0}' - only 'rc' and 'preview' are supported. CONTAINER2018: Ungültige SDK-Vorabversion „{0}“. Es werden nur „rc“ und „preview“ unterstützt. - {StrBegins="CONTAINER2018: "} + {StrBegin="CONTAINER2018: "} CONTAINER2019: Invalid SDK semantic version '{0}'. CONTAINER2019: Ungültige SDK-semantische Version „{0}“. - {StrBegins="CONTAINER2019: "} + {StrBegin="CONTAINER2019: "} CONTAINER2007: Invalid {0} provided: {1}. Image tags must be alphanumeric, underscore, hyphen, or period. CONTAINER2007: Ungültige {0} angegeben: {1}. Imagetags müssen alphanumerisch, Unterstrich, Bindestrich oder Punkt sein. - {StrBegins="CONTAINER2007: "} + {StrBegin="CONTAINER2007: "} CONTAINER2010: Invalid {0} provided: {1}. {0} must be a semicolon-delimited list of valid image tags. Image tags must be alphanumeric, underscore, hyphen, or period. CONTAINER2010: Ungültige {0} angegeben: {1}. {0} muss eine durch Semikolons getrennte Liste gültiger Imagetags sein. Imagetags müssen alphanumerisch, Unterstrich, Bindestrich oder Punkt sein. - {StrBegins="CONTAINER2010: "} - - - Invalid string[] TargetRuntimeIdentifiers. Either all should be 'linux-musl' or none. - Ungültige string[] TargetRuntimeIdentifiers. Entweder sollten alle "linux-musl" oder "none" sein. - + {StrBegin="CONTAINER2010: "} CONTAINER1003: Token response had neither token nor access_token. CONTAINER1003: Die Tokenantwort enthielt weder ein Token noch access_token. - {StrBegins="CONTAINER1003: "} + {StrBegin="CONTAINER1003: "} CONTAINER4005: Item '{0}' contains items without metadata 'Value', and they will be ignored. CONTAINER4005: Das Element „{0}“ enthält Elemente ohne Metadatum „Value“. Diese werden ignoriert. - {StrBegins="CONTAINER4005: "} + {StrBegin="CONTAINER4005: "} CONTAINER1012: The local registry is not available, but pushing to a local registry was requested. - CONTAINER1012: Die lokale Registrierung ist nicht verfügbar, es wurde jedoch eine Pushübertragung in eine lokale Registrierung angefordert. - {StrBegins="CONTAINER1012: "} + CONTAINER1012: The local registry is not available, but pushing to a local registry was requested. + {StrBegin="CONTAINER1012: "} Error while reading daemon config: {0} @@ -307,22 +225,17 @@ CONTAINER2004: Unable to download layer with descriptor '{0}' from registry '{1}' because it does not exist. CONTAINER2004: Die Ebene mit dem Deskriptor „{0}“ kann nicht aus der Registrierung „{1}“ heruntergeladen werden, da sie nicht vorhanden ist. - {StrBegins="CONTAINER2004: "} + {StrBegin="CONTAINER2004: "} CONTAINER2016: ContainerPort item '{0}' does not specify the port number. Please ensure the item's Include is a port number, for example '<ContainerPort Include="80" />' CONTAINER2016: Das ContainerPort-Element „{0}“ gibt keine Portnummer an. Stellen Sie sicher, dass der Include des Elements eine Portnummer ist, z. B. „<ContainerPort Include="80" />“ - {StrBegins="CONTAINER2016: "} - - - 'mediaType' of manifests should be the same in image index. - "mediaType" der Manifeste sollte im Bildindex identisch sein. - + {StrBegin="CONTAINER2016: "} CONTAINER1004: No RequestUri specified. CONTAINER1004: Es wurde kein RequestUri angegeben. - {StrBegins="CONTAINER1004: "} + {StrBegin="CONTAINER1004: "} '{0}' was not a valid container image name, it was normalized to '{1}' @@ -332,27 +245,17 @@ CONTAINER2011: {0} '{1}' does not exist CONTAINER2011: {0} „{1}“ ist nicht vorhanden. - {StrBegins="CONTAINER2011: "} - - - CONTAINER1017: Unable to communicate with the registry '{0}'. - CONTAINER1017: Die Kommunikation mit der Registrierung "{0}" ist nicht möglich. - {StrBegins="CONTAINER1017:" } + {StrBegin="CONTAINER2011: "} CONTAINER1013: Failed to push to the output registry: {0} CONTAINER1013: Fehler beim Pushen in die Ausgaberegistrierung: {0} - {StrBegins="CONTAINER1013: "} - - - CONTAINER1014: Manifest pull failed. - CONTAINER1014: Fehler beim Abrufen des Manifests. - {StrBegins="CONTAINER1014: "} + {StrBegin="CONTAINER1013: "} - CONTAINER1005: Registry push failed; received status code '{0}'. - CONTAINER1005: Registrierungspushfehler. Statuscode "{0}" wurde empfangen. - {StrBegins="CONTAINER1005: "} + CONTAINER1005: Registry push failed. + CONTAINER1005: Fehler beim Pushen der Registrierung. + {StrBegin="CONTAINER1005: "} Uploading config to registry at blob '{0}', @@ -399,70 +302,45 @@ Das Tag „{0}“ wurde in „{1}“ hochgeladen. {1} is the registry name - - CONTAINER1015: Unable to access the repository '{0}' at tag '{1}' in the registry '{2}'. Please confirm that this name and tag are present in the registry. - CONTAINER1015: Auf das Repository "{0}" am Tag "{1}" in der Registrierung "{2}" kann nicht zugegriffen werden. Vergewissern Sie sich, dass dieser Name und dieses Tag in der Registrierung vorhanden sind. - {StrBegins="CONTAINER1015: "} - CONTAINER4003: Required '{0}' items contain empty items. CONTAINER4003: Erforderliche „{0}“-Elemente enthalten leere Elemente. - {StrBegins="CONTAINER4003: "} + {StrBegin="CONTAINER4003: "} CONTAINER4002: Required '{0}' items were not set. CONTAINER4002: Erforderliche „{0}“-Elemente wurden nicht festgelegt. - {StrBegins="CONTAINER4002: "} + {StrBegin="CONTAINER4002: "} CONTAINER4001: Required property '{0}' was not set or empty. CONTAINER4001: Die erforderliche Eigenschaft „{0}“ wurde nicht festgelegt oder ist leer. - {StrBegins="CONTAINER4001: "} + {StrBegin="CONTAINER4001: "} CONTAINER1006: Too many retries, stopping. CONTAINER1006: Zu viele Wiederholungsversuche, Vorgang wird beendet. - {StrBegins="CONTAINER1006: "} - - - CONTAINER1016: Unable to access the repository '{0}' in the registry '{1}'. Please confirm your credentials are correct and that you have access to this repository and registry. - CONTAINER1016: Auf das Repository "{0}" in der Registrierung "{1}" kann nicht zugegriffen werden. Vergewissern Sie sich, dass Ihre Anmeldeinformationen korrekt sind und dass Sie Zugriff auf dieses Repository und die Registrierung haben. - {StrBegins="CONTAINER1016:" } - - - CONTAINER1018: Unable to download image from the repository '{0}'. - CONTAINER1018: Das Image kann nicht aus dem Repository „{0}“ heruntergeladen werden. - {StrBegins="CONTAINER1018:" } + {StrBegin="CONTAINER1006: "} CONTAINER2021: Unknown AppCommandInstruction '{0}'. Valid instructions are {1}. - CONTAINER2021: Unbekannte AppCommandInstruction "{0}". Gültige Anweisungen sind {1}. - {StrBegins="CONTAINER2021: "} + CONTAINER2021: Unknown AppCommandInstruction '{0}'. Valid instructions are {1}. + {StrBegin="CONTAINER2021: "} CONTAINER2002: Unknown local registry type '{0}'. Valid local container registry types are {1}. - CONTAINER2002: Unbekannter lokaler Registrierungstyp "{0}". Gültige lokale Containerregistrierungstypen sind {1}. - {StrBegins="CONTAINER2002: "} + CONTAINER2002: Unknown local registry type '{0}'. Valid local container registry types are {1}. + {StrBegin="CONTAINER2002: "} CONTAINER2003: The manifest for {0}:{1} from registry {2} was an unknown type: {3}. Please raise an issue at https://github.com/dotnet/sdk-container-builds/issues with this message. CONTAINER2003: Das Manifest für {0}:{1} aus der Registrierung {2} war ein unbekannter Typ: {3}. Bitte melden Sie das Problem unter https://github.com/dotnet/sdk-container-builds/issues mit dieser Meldung. - {StrBegins="CONTAINER2003: "} + {StrBegin="CONTAINER2003: "} CONTAINER2001: Unrecognized mediaType '{0}'. CONTAINER2001: Unbekannter mediaType „{0}“. - {StrBegins="CONTAINER2001: "} - - - Cannot create image index for the provided 'mediaType' = '{0}'. - Der Bildindex für den angegebenen mediaType = '{0}' kann nicht erstellt werden. - - - - Unable to create tarball for mediaType '{0}'. - Tarball für mediaType '{0}' kann nicht erstellt werden. - + {StrBegin="CONTAINER2001: "} CONTAINER0000: Value for unit test {0} diff --git a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.es.xlf b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.es.xlf index 164c9d447a5d..00a533f30e33 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.es.xlf +++ b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.es.xlf @@ -5,73 +5,61 @@ CONTAINER1002: Request to Amazon Elastic Container Registry failed prematurely. This is often caused when the target repository does not exist in the registry. CONTAINER1002: Error prematuro en la solicitud al registro de contenedor elástico de Amazon. Esto suele ocurrir cuando el repositorio de destino no existe en el registro. - {StrBegins="CONTAINER1002: "} + {StrBegin="CONTAINER1002: "} CONTAINER2008: Both {0} and {1} were provided, but only one or the other is allowed. CONTAINER2008: Se proporcionaron {0} y {1}, pero solo se permite uno de los dos. - {StrBegins="CONTAINER2008: "} + {StrBegin="CONTAINER2008: "} CONTAINER2025: ContainerAppCommandArgs are provided without specifying a ContainerAppCommand. - CONTAINER2025: ContainerAppCommandArgs se proporcionan sin especificar containerAppCommand. - {StrBegins="CONTAINER2025: "} + CONTAINER2025: ContainerAppCommandArgs are provided without specifying a ContainerAppCommand. + {StrBegin="CONTAINER2025: "} CONTAINER2026: ContainerAppCommand and ContainerAppCommandArgs must be empty when ContainerAppCommandInstruction is '{0}'. - CONTAINER2026: ContainerAppCommand y ContainerAppCommandArgs deben estar vacíos cuando ContainerAppCommandInstruction es '{0}'. - {StrBegins="CONTAINER2026: "} - - - local archive at '{0}' - archivo local en “{0}” - {0} is the path to the file written + CONTAINER2026: ContainerAppCommand and ContainerAppCommandArgs must be empty when ContainerAppCommandInstruction is '{0}'. + {StrBegin="CONTAINER2026: "} CONTAINER2022: The base image has an entrypoint that will be overwritten to start the application. Set ContainerAppCommandInstruction to 'Entrypoint' if this is desired. To preserve the base image entrypoint, set ContainerAppCommandInstruction to 'DefaultArgs'. - CONTAINER2022: la imagen base tiene un punto de entrada que se sobrescribirá para iniciar la aplicación. Establezca ContainerAppCommandInstruction en "Entrypoint" si lo desea. Para conservar el punto de entrada de la imagen base, establezca ContainerAppCommandInstruction en "DefaultArgs". - {StrBegins="CONTAINER2022: "} + CONTAINER2022: The base image has an entrypoint that will be overwritten to start the application. Set ContainerAppCommandInstruction to 'Entrypoint' if this is desired. To preserve the base image entrypoint, set ContainerAppCommandInstruction to 'DefaultArgs'. + {StrBegin="CONTAINER2022: "} CONTAINER2009: Could not parse {0}: {1} CONTAINER2009: No se pudo analizar {0}: {1} - {StrBegins="CONTAINER2009: "} + {StrBegin="CONTAINER2009: "} CONTAINER2020: {0} does not specify a registry and will be pulled from Docker Hub. Please prefix the name with the image registry, for example: '{1}/<image>'. - CONTAINER2020: {0} no especifica un registro y se extraerá de Docker Hub. Anteponer el nombre al registro de imágenes, por ejemplo: "{1}/<image>". - {StrBegins="CONTAINER2020: "} + CONTAINER2020: {0} does not specify a registry and will be pulled from Docker Hub. Please prefix the name with the image registry, for example: '{1}/<image>'. + {StrBegin="CONTAINER2020: "} CONTAINER2013: {0} had spaces in it, replacing with dashes. CONTAINER2013: {0} tenía espacios, reemplazando por guiones. - {StrBegins="CONTAINER2013: "} + {StrBegin="CONTAINER2013: "} CONTAINER1011: Couldn't find matching base image for {0} that matches RuntimeIdentifier {1}. CONTAINER1011: No se pudo encontrar una imagen base coincidente para {0} que coincida con el RuntimeIdentifier {1}. - {StrBegins="CONTAINER1011: "} + {StrBegin="CONTAINER1011: "} CONTAINER1001: Failed to upload blob using {0}; received status code '{1}'. CONTAINER1001: no se pudo cargar el blob mediante {0}; se ha recibido el código de estado "{1}". - {StrBegins="CONTAINER1001: "} - - - Building image index '{0}' on top of manifests {1}. - Generando '{0}' de índice de imagen encima de los manifiestos {1}. - - {0} is the name of the image index and its tag, {1} is the list of manifests digests - + {StrBegin="CONTAINER1001: "} - Pushed image '{0}' to {1}. - Insertada la imagen “{0}” en {1}. + Pushed container '{0}' to Docker daemon. + Se insertó el contenedor "{0}" en el demonio de Docker. - Pushed image '{0}' to registry '{1}'. - Insertada la imagen “{0}” en el registro “{1}”. + Pushed container '{0}' to registry '{1}'. + Se insertó el contenedor "{0}" en el registro "{1}". @@ -79,220 +67,150 @@ Compilando imagen "{0}" con etiquetas "{1}" encima de la imagen base "{2}". - - Building image '{0}' for runtime identifier '{1}' on top of base image '{2}'. - Compilando la imagen '{0}' para el identificador en tiempo de ejecución '{1}' encima de la imagen base '{2}'. - - CONTAINER1007: Could not deserialize token from JSON. CONTAINER1007: No se pudo deserializar el token de JSON. - {StrBegins="CONTAINER1007: "} + {StrBegin="CONTAINER1007: "} CONTAINER2012: Could not recognize registry '{0}'. CONTAINER2012: No se pudo reconocer el registro "{0}". - {StrBegins="CONTAINER2012: "} - - - local registry via '{0}' - registro local mediante “{0}” - {0} is the command used + {StrBegin="CONTAINER2012: "} CONTAINER3002: Failed to get docker info({0})\n{1}\n{2} CONTAINER3002: No se pudo obtener la información de docker ({0})\n{1}\n{2} - {StrBegins="CONTAINER3002: "} + {StrBegin="CONTAINER3002: "} CONTAINER3002: Failed to get docker info: {0} CONTAINER3002: No se pudo obtener la información de docker: {0} - {StrBegins="CONTAINER3002: "} + {StrBegin="CONTAINER3002: "} - + CONTAINER3001: Failed creating {0} process. - CONTAINER3001: No se pudo crear {0} el proceso. + CONTAINER3001: Failed creating {0} process. CONTAINER3001: {0} is the name of the command we failed to run, usually 'docker' or 'podman'. CONTAINER4006: Property '{0}' is empty or contains whitespace and will be ignored. CONTAINER4006: La propiedad "{0}" está vacía o contiene espacios en blanco y se omitirá. - {StrBegins="CONTAINER4006: "} + {StrBegin="CONTAINER4006: "} CONTAINER4004: Items '{0}' contain empty item(s) which will be ignored. CONTAINER4004: los elementos "{0}" contienen elementos vacíos que se omitirán. - {StrBegins="CONTAINER4004: "} + {StrBegin="CONTAINER4004: "} CONTAINER2023: A ContainerEntrypoint and ContainerAppCommandArgs are provided. ContainerAppInstruction must be set to configure how the application is started. Valid instructions are {0}. - CONTAINER2023: se proporcionan ContainerEntrypoint y ContainerAppCommandArgs. ContainerAppInstruction debe establecerse para configurar cómo se inicia la aplicación. Las instrucciones válidas son {0}. - {StrBegins="CONTAINER2023: "} + CONTAINER2023: A ContainerEntrypoint and ContainerAppCommandArgs are provided. ContainerAppInstruction must be set to configure how the application is started. Valid instructions are {0}. + {StrBegin="CONTAINER2023: "} CONTAINER2027: A ContainerEntrypoint is provided. ContainerAppInstruction must be set to configure how the application is started. Valid instructions are {0}. - CONTAINER2027: se proporciona containerEntrypoint. ContainerAppInstruction debe establecerse para configurar cómo se inicia la aplicación. Las instrucciones válidas son {0}. - {StrBegins="CONTAINER2027: "} + CONTAINER2027: A ContainerEntrypoint is provided. ContainerAppInstruction must be set to configure how the application is started. Valid instructions are {0}. + {StrBegin="CONTAINER2027: "} CONTAINER2024: ContainerEntrypointArgs are provided without specifying a ContainerEntrypoint. - CONTAINER2024: ContainerEntrypointArgs se proporcionan sin especificar containerEntrypoint. - {StrBegins="CONTAINER2024: "} + CONTAINER2024: ContainerEntrypointArgs are provided without specifying a ContainerEntrypoint. + {StrBegin="CONTAINER2024: "} CONTAINER2029: ContainerEntrypointArgsSet are provided. Change to use ContainerAppCommandArgs for arguments that must always be set, or ContainerDefaultArgs for arguments that can be overridden when the container is created. - CONTAINER2029: se proporciona ContainerEntrypointArgsSet. Cambie para usar ContainerAppCommandArgs para los argumentos que siempre se deben establecer o ContainerDefaultArgs para los argumentos que se pueden invalidar cuando se crea el contenedor. - {StrBegins="CONTAINER2029: "} + CONTAINER2029: ContainerEntrypointArgsSet are provided. Change to use ContainerAppCommandArgs for arguments that must always be set, or ContainerDefaultArgs for arguments that can be overridden when the container is created. + {StrBegin="CONTAINER2029: "} CONTAINER2028: ContainerEntrypoint can not be combined with ContainerAppCommandInstruction '{0}'. - CONTAINER2028: ContainerEntrypoint no se puede combinar con ContainerAppCommandInstruction '{0}'. - {StrBegins="CONTAINER2028: "} + CONTAINER2028: ContainerEntrypoint can not be combined with ContainerAppCommandInstruction '{0}'. + {StrBegin="CONTAINER2028: "} CONTAINER1008: Failed retrieving credentials for "{0}": {1} CONTAINER1008: No se pudieron recuperar las credenciales de "{0}": {1} - {StrBegins="CONTAINER1008: "} - - - CONTAINER2030: GenerateLabels was disabled but GenerateDigestLabel was enabled - no digest label will be created. - CONTAINER2030: GenerateLabels se deshabilitó, pero GenerateDigestLabel se deshabilitó; no se creará ninguna etiqueta de resumen. - {StrBegins="CONTAINER2030: "} + {StrBegin="CONTAINER1008: "} No host object detected. No se detectó ningún objeto host. - - Cannot create image index because at least one of the provided images' config is missing 'architecture'. - No se puede crear el índice de imagen porque falta "architecture" en al menos una de las configuraciones de imágenes proporcionadas. - - - - Cannot create image index because at least one of the provided images' config is missing 'os'. - No se puede crear el índice de imagen porque falta 'os' en al menos una de las configuraciones de imágenes proporcionadas. - - - - Pushed image index '{0}' to registry '{1}'. - Imagen “{0}” insertada en el registro “{1}”. - - - - Image index creation for Podman is not supported. - No se admite la creación de índices de imagen para Podman. - - CONTAINER1009: Failed to load image from local registry. stdout: {0} - CONTAINER1009: no se pudo cargar la imagen desde el registro local. Stdout: {0} - {StrBegins="CONTAINER1009: "} - - - CONTAINER1020: Failed to load image because containerd image store is not enabled for Docker. Tip: You can enable it by checking 'Use containerd for pulling and storing images' in Docker Desktop settings. - CONTAINER1020: no se pudo cargar la imagen porque el almacén de imágenes en contenedor no está habilitado para Docker. Sugerencia: Para habilitarla, active "Usar contenedores para extraer y almacenar imágenes" en la configuración de Docker Desktop. - {StrBegin="CONTAINER1020: "} + CONTAINER1009: No se pudo cargar la imagen en el demonio de Docker local. stdout: {0} + {StrBegin="CONTAINER1009: "} CONTAINER1010: Pulling images from local registry is not supported. - CONTAINER1010: No se admite la extracción de imágenes del registro local. - {StrBegins="CONTAINER1010: "} + CONTAINER1010: No se admite la extracción de imágenes del demonio de Docker local. + {StrBegin="CONTAINER1010: "} - - Cannot create image index because no images were provided. - No se puede crear el índice de imágenes porque no se proporcionó ninguna imagen. - - - - CONTAINER2031: The container image format '{0}' is not supported. Supported formats are '{1}'. - CONTAINER2031: no se admite el formato de imagen de contenedor '{0}'. Los formatos admitidos son '{1}'. - {StrBegins="CONTAINER2031: "} + + CONTAINER2014: Invalid {0}: {1}. + CONTAINER2014: {0} no válido: {1}. + {StrBegin="CONTAINER2014: "} CONTAINER2015: {0}: '{1}' was not a valid Environment Variable. Ignoring. CONTAINER2015: {0}: "{1}" no era una variable de entorno válida. Ignorando. - {StrBegins="CONTAINER2015: "} + {StrBegin="CONTAINER2015: "} - - Cannot create image index because at least one of the provided images' config is invalid. - No se puede crear el índice de imagen porque al menos una de las imágenes proporcionadas no es válida. - - - - Cannot create image index because at least one of the provided images' manifest is invalid. - No se puede crear el índice de imagen porque al menos uno de los manifiestos de las imágenes proporcionadas no es válido. - - - - Cannot create image index because provided images are invalid. Items must have 'Config', 'Manifest', 'ManifestMediaType' and 'ManifestDigest' metadata. - No se puede crear el índice de imágenes porque las imágenes proporcionadas no son válidas. Los elementos deben tener metadatos 'Config', 'Manifest', 'ManifestMediaType' y 'ManifestDigest'. - - - - CONTAINER2005: The inferred image name '{0}' contains entirely invalid characters. The valid characters for an image name are alphanumeric characters, -, /, or _, and the image name must start with an alphanumeric character. - CONTAINER2005: el nombre de imagen inferido '{0}' contiene caracteres totalmente no válidos. Los caracteres válidos para un nombre de imagen son los caracteres alfanuméricos, -, /, o _; el nombre de imagen tiene que comenzar con uno. - {StrBegins="CONTAINER2005: "} - - - CONTAINER2005: The first character of the image name '{0}' must be a lowercase letter or a digit and all characters in the name must be an alphanumeric character, -, /, or _. - CONTAINER2005: el primer carácter del nombre de imagen '{0}' tiene que ser una letra minúscula o un dígito y todos los caracteres del nombre deben ser alfanuméricos, -, /, o _. - {StrBegins="CONTAINER2005: "} + + CONTAINER2005: The first character of the image name must be a lowercase letter or a digit. + CONTAINER2005: El primer carácter del nombre de la imagen debe ser una letra minúscula o un dígito. + {StrBegin="CONTAINER2005: "} CONTAINER2017: A ContainerPort item was provided with an invalid port number '{0}'. ContainerPort items must have an Include value that is an integer, and a Type value that is either 'tcp' or 'udp'. CONTAINER2017: Se proporcionó un elemento ContainerPort con un número de puerto "{0}". Los elementos ContainerPort deben tener un valor Include que sea un entero y un valor Type que sea "tcp" o "udp". - {StrBegins="CONTAINER2017: "} + {StrBegin="CONTAINER2017: "} CONTAINER2017: A ContainerPort item was provided with an invalid port number '{0}' and an invalid port type '{1}'. ContainerPort items must have an Include value that is an integer, and a Type value that is either 'tcp' or 'udp'. CONTAINER2017: Se proporcionó un elemento ContainerPort con un número de puerto no válido "{0}" y un tipo de puerto no válido "{1}". Los elementos ContainerPort deben tener un valor Include que sea un entero y un valor Type que sea "tcp" o "udp". - {StrBegins="CONTAINER2017: "} + {StrBegin="CONTAINER2017: "} CONTAINER2017: A ContainerPort item was provided with an invalid port type '{0}'. ContainerPort items must have an Include value that is an integer, and a Type value that is either 'tcp' or 'udp'. CONTAINER2017: Se proporcionó un elemento ContainerPort con un tipo de puerto "{0}". Los elementos ContainerPort deben tener un valor Include que sea un entero y un valor Type que sea "tcp" o "udp". - {StrBegins="CONTAINER2017: "} + {StrBegin="CONTAINER2017: "} CONTAINER2018: Invalid SDK prerelease version '{0}' - only 'rc' and 'preview' are supported. CONTAINER2018: Versión preliminar del SDK "{0}" no válida : solo se admiten "rc" y "preview". - {StrBegins="CONTAINER2018: "} + {StrBegin="CONTAINER2018: "} CONTAINER2019: Invalid SDK semantic version '{0}'. CONTAINER2019: Versión "{0}" de semántica del SDK no válida. - {StrBegins="CONTAINER2019: "} + {StrBegin="CONTAINER2019: "} CONTAINER2007: Invalid {0} provided: {1}. Image tags must be alphanumeric, underscore, hyphen, or period. CONTAINER2007: Se proporcionó un {0} no válido: {1}. Las etiquetas de imagen deben ser alfanuméricas, con guion bajo, guiones o puntos. - {StrBegins="CONTAINER2007: "} + {StrBegin="CONTAINER2007: "} CONTAINER2010: Invalid {0} provided: {1}. {0} must be a semicolon-delimited list of valid image tags. Image tags must be alphanumeric, underscore, hyphen, or period. CONTAINER2010: se proporcionó un {0} no válido: {1}. {0} debe ser una lista delimitada por punto y coma de etiquetas de imagen válidas. Las etiquetas de imagen deben ser alfanuméricas, con guion bajo, guiones o puntos. - {StrBegins="CONTAINER2010: "} - - - Invalid string[] TargetRuntimeIdentifiers. Either all should be 'linux-musl' or none. - String[] TargetRuntimeIdentifiers no válido. Todos deben ser 'linux-musl' o ninguno. - + {StrBegin="CONTAINER2010: "} CONTAINER1003: Token response had neither token nor access_token. CONTAINER1003: La respuesta del token no tenía ningún token ni access_token. - {StrBegins="CONTAINER1003: "} + {StrBegin="CONTAINER1003: "} CONTAINER4005: Item '{0}' contains items without metadata 'Value', and they will be ignored. CONTAINER4005: El elemento "{0}" contiene elementos sin metadatos "Value" y se omitirán. - {StrBegins="CONTAINER4005: "} + {StrBegin="CONTAINER4005: "} CONTAINER1012: The local registry is not available, but pushing to a local registry was requested. - CONTAINER1012: el registro local no está disponible, pero se solicitó la inserción en un registro local. - {StrBegins="CONTAINER1012: "} + CONTAINER1012: The local registry is not available, but pushing to a local registry was requested. + {StrBegin="CONTAINER1012: "} Error while reading daemon config: {0} @@ -307,22 +225,17 @@ CONTAINER2004: Unable to download layer with descriptor '{0}' from registry '{1}' because it does not exist. CONTAINER2004: No se puede descargar la capa con el descriptor "{0}" del registro "{1}" porque no existe. - {StrBegins="CONTAINER2004: "} + {StrBegin="CONTAINER2004: "} CONTAINER2016: ContainerPort item '{0}' does not specify the port number. Please ensure the item's Include is a port number, for example '<ContainerPort Include="80" />' CONTAINER2016: El elemento ContainerPort "{0}" no especifica el número de puerto. Asegúrate de que la inclusión del elemento es un número de puerto, por ejemplo, "<ContainerPort Include="80" />" - {StrBegins="CONTAINER2016: "} - - - 'mediaType' of manifests should be the same in image index. - El valor de "mediaType" de los manifiestos debe ser el mismo en el índice de imágenes. - + {StrBegin="CONTAINER2016: "} CONTAINER1004: No RequestUri specified. CONTAINER1004: No se especificó RequestUri. - {StrBegins="CONTAINER1004: "} + {StrBegin="CONTAINER1004: "} '{0}' was not a valid container image name, it was normalized to '{1}' @@ -332,27 +245,17 @@ CONTAINER2011: {0} '{1}' does not exist CONTAINER2011: {0} "{1}" no existe - {StrBegins="CONTAINER2011: "} - - - CONTAINER1017: Unable to communicate with the registry '{0}'. - CONTAINER1017: no se puede comunicar con el registro ''{0}''. - {StrBegins="CONTAINER1017:" } + {StrBegin="CONTAINER2011: "} CONTAINER1013: Failed to push to the output registry: {0} CONTAINER1013: No se pudieron enviar cambios al el Registro de salida: {0} - {StrBegins="CONTAINER1013: "} - - - CONTAINER1014: Manifest pull failed. - CONTAINER1014: error al extraer el manifiesto. - {StrBegins="CONTAINER1014: "} + {StrBegin="CONTAINER1013: "} - CONTAINER1005: Registry push failed; received status code '{0}'. - CONTAINER1005: error de inserción del Registro; recibió el código de estado '{0}'. - {StrBegins="CONTAINER1005: "} + CONTAINER1005: Registry push failed. + CONTAINER1005: Error al insertar el Registro. + {StrBegin="CONTAINER1005: "} Uploading config to registry at blob '{0}', @@ -399,70 +302,45 @@ Etiqueta cargada "{0}" en "{1}". {1} is the registry name - - CONTAINER1015: Unable to access the repository '{0}' at tag '{1}' in the registry '{2}'. Please confirm that this name and tag are present in the registry. - CONTAINER1015: no se puede acceder al repositorio ''{0}'' en la etiqueta ''{1}'' del Registro ''{2}''. Confirme que este nombre y esta etiqueta están presentes en el registro. - {StrBegins="CONTAINER1015: "} - CONTAINER4003: Required '{0}' items contain empty items. CONTAINER4003: Los elementos de "{0}" necesarios contienen elementos vacíos. - {StrBegins="CONTAINER4003: "} + {StrBegin="CONTAINER4003: "} CONTAINER4002: Required '{0}' items were not set. CONTAINER4002: No se establecieron los elementos de "{0}" necesarios. - {StrBegins="CONTAINER4002: "} + {StrBegin="CONTAINER4002: "} CONTAINER4001: Required property '{0}' was not set or empty. CONTAINER4001: La propiedad necesaria "{0}" no se estableció o estaba vacía. - {StrBegins="CONTAINER4001: "} + {StrBegin="CONTAINER4001: "} CONTAINER1006: Too many retries, stopping. CONTAINER1006: Demasiados reintentos, deteniendo. - {StrBegins="CONTAINER1006: "} - - - CONTAINER1016: Unable to access the repository '{0}' in the registry '{1}'. Please confirm your credentials are correct and that you have access to this repository and registry. - CONTAINER1016: no se puede acceder al repositorio ''{0}'' en el registro ''{1}''. Confirme que las credenciales son correctas y que tiene acceso a este repositorio y registro. - {StrBegins="CONTAINER1016:" } - - - CONTAINER1018: Unable to download image from the repository '{0}'. - CONTAINER1018: no se puede descargar la imagen del repositorio ''{0}". - {StrBegins="CONTAINER1018:" } + {StrBegin="CONTAINER1006: "} CONTAINER2021: Unknown AppCommandInstruction '{0}'. Valid instructions are {1}. - CONTAINER2021: AppCommandInstruction ''{0}desconocido. Las instrucciones válidas son {1}. - {StrBegins="CONTAINER2021: "} + CONTAINER2021: Unknown AppCommandInstruction '{0}'. Valid instructions are {1}. + {StrBegin="CONTAINER2021: "} CONTAINER2002: Unknown local registry type '{0}'. Valid local container registry types are {1}. - CONTAINER2002: tipo de registro local desconocido '{0}'. Los tipos de registro de contenedor locales válidos son {1}. - {StrBegins="CONTAINER2002: "} + CONTAINER2002: Unknown local registry type '{0}'. Valid local container registry types are {1}. + {StrBegin="CONTAINER2002: "} CONTAINER2003: The manifest for {0}:{1} from registry {2} was an unknown type: {3}. Please raise an issue at https://github.com/dotnet/sdk-container-builds/issues with this message. CONTAINER2003: El manifiesto de {0}:{1} del registro {2} era de un tipo desconocido: {3}. Presente un problema en https://github.com/dotnet/sdk-container-builds/issues con este mensaje. - {StrBegins="CONTAINER2003: "} + {StrBegin="CONTAINER2003: "} CONTAINER2001: Unrecognized mediaType '{0}'. CONTAINER2001: mediaType "{0}" no reconocido. - {StrBegins="CONTAINER2001: "} - - - Cannot create image index for the provided 'mediaType' = '{0}'. - No se puede crear el índice de imagen para el valor "mediaType" = '{0}' proporcionado. - - - - Unable to create tarball for mediaType '{0}'. - No se puede crear un tarball para el mediaType "{0}". - + {StrBegin="CONTAINER2001: "} CONTAINER0000: Value for unit test {0} diff --git a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.fr.xlf b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.fr.xlf index f0d3a647de7b..4c0fe2593028 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.fr.xlf +++ b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.fr.xlf @@ -5,73 +5,61 @@ CONTAINER1002: Request to Amazon Elastic Container Registry failed prematurely. This is often caused when the target repository does not exist in the registry. CONTAINER1002: la demande à Amazon Elastic Container Registry a échoué prématurément. Cela est souvent dû au fait que le dépôt cible n’existe pas dans le Registre. - {StrBegins="CONTAINER1002: "} + {StrBegin="CONTAINER1002: "} CONTAINER2008: Both {0} and {1} were provided, but only one or the other is allowed. CONTAINER2008: {0} et {1} ont été fournis, mais seul l’un ou l’autre est autorisé. - {StrBegins="CONTAINER2008: "} + {StrBegin="CONTAINER2008: "} CONTAINER2025: ContainerAppCommandArgs are provided without specifying a ContainerAppCommand. - CONTAINER2025: les ContainerAppCommandArgs sont fournis sans spécifier de ContainerAppCommand. - {StrBegins="CONTAINER2025: "} + CONTAINER2025: ContainerAppCommandArgs are provided without specifying a ContainerAppCommand. + {StrBegin="CONTAINER2025: "} CONTAINER2026: ContainerAppCommand and ContainerAppCommandArgs must be empty when ContainerAppCommandInstruction is '{0}'. - CONTAINER2026: ContainerAppCommand et ContainerAppCommandArgs doivent être vides lorsque ContainerAppCommandInstruction est '{0}'. - {StrBegins="CONTAINER2026: "} - - - local archive at '{0}' - archive locale à «{0}» - {0} is the path to the file written + CONTAINER2026: ContainerAppCommand and ContainerAppCommandArgs must be empty when ContainerAppCommandInstruction is '{0}'. + {StrBegin="CONTAINER2026: "} CONTAINER2022: The base image has an entrypoint that will be overwritten to start the application. Set ContainerAppCommandInstruction to 'Entrypoint' if this is desired. To preserve the base image entrypoint, set ContainerAppCommandInstruction to 'DefaultArgs'. - CONTAINER2022: L'image de base a un point d'entrée qui sera écrasé pour démarrer l'application. Définissez ContainerAppCommandInstruction sur 'Entrypoint' si vous le souhaitez. Pour conserver le point d'entrée de l'image de base, définissez ContainerAppCommandInstruction sur "DefaultArgs". - {StrBegins="CONTAINER2022: "} + CONTAINER2022: The base image has an entrypoint that will be overwritten to start the application. Set ContainerAppCommandInstruction to 'Entrypoint' if this is desired. To preserve the base image entrypoint, set ContainerAppCommandInstruction to 'DefaultArgs'. + {StrBegin="CONTAINER2022: "} CONTAINER2009: Could not parse {0}: {1} CONTAINER2009: impossible d’analyser {0} : {1} - {StrBegins="CONTAINER2009: "} + {StrBegin="CONTAINER2009: "} CONTAINER2020: {0} does not specify a registry and will be pulled from Docker Hub. Please prefix the name with the image registry, for example: '{1}/<image>'. - CONTAINER2020: {0} ne spécifie pas de registre et sera extrait de Docker Hub. Veuillez préfixer le nom avec le registre d'images, par exemple : '{1}/<image>'. - {StrBegins="CONTAINER2020: "} + CONTAINER2020: {0} does not specify a registry and will be pulled from Docker Hub. Please prefix the name with the image registry, for example: '{1}/<image>'. + {StrBegin="CONTAINER2020: "} CONTAINER2013: {0} had spaces in it, replacing with dashes. CONTAINER2013: {0} contenait des espaces, remplacés par des tirets. - {StrBegins="CONTAINER2013: "} + {StrBegin="CONTAINER2013: "} CONTAINER1011: Couldn't find matching base image for {0} that matches RuntimeIdentifier {1}. CONTAINER1011: impossible de trouver une image de base correspondante pour {0} qui correspond à RuntimeIdentifier {1}. - {StrBegins="CONTAINER1011: "} + {StrBegin="CONTAINER1011: "} CONTAINER1001: Failed to upload blob using {0}; received status code '{1}'. CONTAINER1001: échec du chargement de l’objet blob à l’aide de {0}; le code d’état «{1}» a été reçu. - {StrBegins="CONTAINER1001: "} - - - Building image index '{0}' on top of manifests {1}. - Génération de l’index d’image '{0}' au-dessus des manifestes {1}. - - {0} is the name of the image index and its tag, {1} is the list of manifests digests - + {StrBegin="CONTAINER1001: "} - Pushed image '{0}' to {1}. - L’image '{0}' a été envoyée à {1}. + Pushed container '{0}' to Docker daemon. + Le conteneur «{0}» a été envoyé au démon Docker. - Pushed image '{0}' to registry '{1}'. - L’image «{0}» a été envoyée au registre «{1}». + Pushed container '{0}' to registry '{1}'. + Le conteneur «{0}» a été envoyé au registre «{1}». @@ -79,220 +67,150 @@ Génération de l’image «{0}» avec des balises «{1}» au-dessus de l’image de base «{2}». - - Building image '{0}' for runtime identifier '{1}' on top of base image '{2}'. - Génération de l’image «{0}» pour l’identificateur d’exécution «{1}» au-dessus de l’image de base «{2}». - - CONTAINER1007: Could not deserialize token from JSON. CONTAINER1007: impossible de désérialiser le jeton à partir de JSON. - {StrBegins="CONTAINER1007: "} + {StrBegin="CONTAINER1007: "} CONTAINER2012: Could not recognize registry '{0}'. CONTAINER2012: impossible de reconnaître le registre '{0}'. - {StrBegins="CONTAINER2012: "} - - - local registry via '{0}' - registre local via «{0}» - {0} is the command used + {StrBegin="CONTAINER2012: "} CONTAINER3002: Failed to get docker info({0})\n{1}\n{2} CONTAINER3002: échec de l’obtention des informations docker ({0})\n{1}\n{2} - {StrBegins="CONTAINER3002: "} + {StrBegin="CONTAINER3002: "} CONTAINER3002: Failed to get docker info: {0} CONTAINER3002: échec de l’obtention des informations docker : {0} - {StrBegins="CONTAINER3002: "} + {StrBegin="CONTAINER3002: "} - + CONTAINER3001: Failed creating {0} process. - CONTAINER3001: Échec du processus de création {0}. + CONTAINER3001: Failed creating {0} process. CONTAINER3001: {0} is the name of the command we failed to run, usually 'docker' or 'podman'. CONTAINER4006: Property '{0}' is empty or contains whitespace and will be ignored. CONTAINER4006: la propriété '{0}' est vide ou contient un espace blanc et sera ignorée. - {StrBegins="CONTAINER4006: "} + {StrBegin="CONTAINER4006: "} CONTAINER4004: Items '{0}' contain empty item(s) which will be ignored. CONTAINER4004: les éléments '{0}' contiennent un ou plusieurs éléments vides qui seront ignorés. - {StrBegins="CONTAINER4004: "} + {StrBegin="CONTAINER4004: "} CONTAINER2023: A ContainerEntrypoint and ContainerAppCommandArgs are provided. ContainerAppInstruction must be set to configure how the application is started. Valid instructions are {0}. - CONTAINER2023: Un ContainerEntrypoint et ContainerAppCommandArgs sont fournis. ContainerAppInstruction doit être défini pour configurer le mode de démarrage de l'application. Les instructions valides sont {0}. - {StrBegins="CONTAINER2023: "} + CONTAINER2023: A ContainerEntrypoint and ContainerAppCommandArgs are provided. ContainerAppInstruction must be set to configure how the application is started. Valid instructions are {0}. + {StrBegin="CONTAINER2023: "} CONTAINER2027: A ContainerEntrypoint is provided. ContainerAppInstruction must be set to configure how the application is started. Valid instructions are {0}. - CONTAINER2027: Un ContainerEntrypoint est fourni. ContainerAppInstruction doit être défini pour configurer le mode de démarrage de l'application. Les instructions valides sont {0}. - {StrBegins="CONTAINER2027: "} + CONTAINER2027: A ContainerEntrypoint is provided. ContainerAppInstruction must be set to configure how the application is started. Valid instructions are {0}. + {StrBegin="CONTAINER2027: "} CONTAINER2024: ContainerEntrypointArgs are provided without specifying a ContainerEntrypoint. - CONTAINER2024: les ContainerEntrypointArgs sont fournis sans spécifier de ContainerEntrypoint. - {StrBegins="CONTAINER2024: "} + CONTAINER2024: ContainerEntrypointArgs are provided without specifying a ContainerEntrypoint. + {StrBegin="CONTAINER2024: "} CONTAINER2029: ContainerEntrypointArgsSet are provided. Change to use ContainerAppCommandArgs for arguments that must always be set, or ContainerDefaultArgs for arguments that can be overridden when the container is created. - CONTAINER2029: ContainerEntrypointArgsSet est fourni. Changez pour utiliser ContainerAppCommandArgs pour les arguments qui doivent toujours être définis, ou ContainerDefaultArgs pour les arguments qui peuvent être remplacés lors de la création du conteneur. - {StrBegins="CONTAINER2029: "} + CONTAINER2029: ContainerEntrypointArgsSet are provided. Change to use ContainerAppCommandArgs for arguments that must always be set, or ContainerDefaultArgs for arguments that can be overridden when the container is created. + {StrBegin="CONTAINER2029: "} CONTAINER2028: ContainerEntrypoint can not be combined with ContainerAppCommandInstruction '{0}'. - CONTAINER2028: ContainerEntrypoint ne peut pas être combiné avec ContainerAppCommandInstruction '{0}'. - {StrBegins="CONTAINER2028: "} + CONTAINER2028: ContainerEntrypoint can not be combined with ContainerAppCommandInstruction '{0}'. + {StrBegin="CONTAINER2028: "} CONTAINER1008: Failed retrieving credentials for "{0}": {1} CONTAINER1008: échec de la récupération des informations d’identification pour «{0}» : {1} - {StrBegins="CONTAINER1008: "} - - - CONTAINER2030: GenerateLabels was disabled but GenerateDigestLabel was enabled - no digest label will be created. - CONTAINER2030: GenerateLabels était désactivé, mais GenerateDigestLabel était activé : aucune étiquette digest ne sera créée. - {StrBegins="CONTAINER2030: "} + {StrBegin="CONTAINER1008: "} No host object detected. Aucun objet hôte détecté. - - Cannot create image index because at least one of the provided images' config is missing 'architecture'. - Impossible de créer l’index d’image, car il manque « architecture » dans la configuration d’au moins une des images fournies. - - - - Cannot create image index because at least one of the provided images' config is missing 'os'. - Impossible de créer l’index d’image, car au moins l’une des configurations d’images fournies ne contient pas 'os'. - - - - Pushed image index '{0}' to registry '{1}'. - L’index de l’image « {0} » a été envoyé vers le registre « {1} ». - - - - Image index creation for Podman is not supported. - La création d’index d’image pour Podman n’est pas prise en charge. - - CONTAINER1009: Failed to load image from local registry. stdout: {0} - CONTAINER1009: Échec du chargement de l'image à partir du registre local. sortie standard : {0} - {StrBegins="CONTAINER1009: "} - - - CONTAINER1020: Failed to load image because containerd image store is not enabled for Docker. Tip: You can enable it by checking 'Use containerd for pulling and storing images' in Docker Desktop settings. - CONTAINER1020: échec du chargement de l’image, car le magasin d’images en conteneur n’est pas activé pour Docker. Conseil : vous pouvez l’activer en vérifiant « Utiliser conteneur pour extraire et stocker des images » dans les paramètres de Docker Desktop. - {StrBegin="CONTAINER1020: "} + CONTAINER1009: échec du chargement de l’image dans le démon Docker local. stdout : {0} + {StrBegin="CONTAINER1009: "} CONTAINER1010: Pulling images from local registry is not supported. - CONTAINER1010: L'extraction d'images à partir du registre local n'est pas prise en charge. - {StrBegins="CONTAINER1010: "} + CONTAINER1010: Pulling images from local registry is not supported. + {StrBegin="CONTAINER1010: "} - - Cannot create image index because no images were provided. - Impossible de créer l’index d’image, car aucune image n’a été fournie. - - - - CONTAINER2031: The container image format '{0}' is not supported. Supported formats are '{1}'. - CONTAINER2031: le format d’image conteneur «{0}» n’est pas pris en charge. Les formats pris en charge sont « {1} ». - {StrBegins="CONTAINER2031: "} + + CONTAINER2014: Invalid {0}: {1}. + CONTAINER2014: {0} non valide : {1}. + {StrBegin="CONTAINER2014: "} CONTAINER2015: {0}: '{1}' was not a valid Environment Variable. Ignoring. CONTAINER2015: {0} : '{1}' n’était pas une variable d’environnement valide. Ignorant. - {StrBegins="CONTAINER2015: "} + {StrBegin="CONTAINER2015: "} - - Cannot create image index because at least one of the provided images' config is invalid. - Impossible de créer l’index d’image, car au moins une des configurations d’images fournies n’est pas valide. - - - - Cannot create image index because at least one of the provided images' manifest is invalid. - Impossible de créer l’index d’image, car au moins l’un des manifestes d’images fournis n’est pas valide. - - - - Cannot create image index because provided images are invalid. Items must have 'Config', 'Manifest', 'ManifestMediaType' and 'ManifestDigest' metadata. - Impossible de créer l’index d’images, car les images fournies ne sont pas valides. Les éléments doivent avoir les métadonnées 'Config', 'Manifest', 'ManifestMediaType' et 'ManifestDigest'. - - - - CONTAINER2005: The inferred image name '{0}' contains entirely invalid characters. The valid characters for an image name are alphanumeric characters, -, /, or _, and the image name must start with an alphanumeric character. - CONTAINER2005: le nom d'image déduit '{0}' contient des caractères entièrement non valides. Les caractères valides pour un nom d'image sont les caractères alphanumériques, -, / ou _, et le nom de l'image doit commencer par un caractère alphanumérique. - {StrBegins="CONTAINER2005: "} - - - CONTAINER2005: The first character of the image name '{0}' must be a lowercase letter or a digit and all characters in the name must be an alphanumeric character, -, /, or _. - CONTAINER2005: le premier caractère du nom de l'image '{0}' doit être une lettre minuscule ou un chiffre et tous les caractères du nom doivent être un caractère alphanumérique, -, / ou _. - {StrBegins="CONTAINER2005: "} + + CONTAINER2005: The first character of the image name must be a lowercase letter or a digit. + CONTAINER2005: le premier caractère du nom de l’image doit être une lettre minuscule ou un chiffre. + {StrBegin="CONTAINER2005: "} CONTAINER2017: A ContainerPort item was provided with an invalid port number '{0}'. ContainerPort items must have an Include value that is an integer, and a Type value that is either 'tcp' or 'udp'. CONTAINER2017: un élément ContainerPort a été fourni avec un numéro de port non valide «{0}». Les éléments ContainerPort doivent avoir une valeur Include qui est un entier et une valeur Type qui est 'tcp' ou 'udp'. - {StrBegins="CONTAINER2017: "} + {StrBegin="CONTAINER2017: "} CONTAINER2017: A ContainerPort item was provided with an invalid port number '{0}' and an invalid port type '{1}'. ContainerPort items must have an Include value that is an integer, and a Type value that is either 'tcp' or 'udp'. CONTAINER2017: un élément ContainerPort a été fourni avec un numéro de port non valide '{0}' et un type de port non valide '{1}'. Les éléments ContainerPort doivent avoir une valeur Include qui est un entier et une valeur Type qui est 'tcp' ou 'udp'. - {StrBegins="CONTAINER2017: "} + {StrBegin="CONTAINER2017: "} CONTAINER2017: A ContainerPort item was provided with an invalid port type '{0}'. ContainerPort items must have an Include value that is an integer, and a Type value that is either 'tcp' or 'udp'. CONTAINER2017: un élément ContainerPort a été fourni avec un type de port non valide «{0}». Les éléments ContainerPort doivent avoir une valeur Include qui est un entier et une valeur Type qui est 'tcp' ou 'udp'. - {StrBegins="CONTAINER2017: "} + {StrBegin="CONTAINER2017: "} CONTAINER2018: Invalid SDK prerelease version '{0}' - only 'rc' and 'preview' are supported. CONTAINER2018: version préliminaire du SDK non valide '{0}' - seuls 'rc' et 'preview' sont pris en charge. - {StrBegins="CONTAINER2018: "} + {StrBegin="CONTAINER2018: "} CONTAINER2019: Invalid SDK semantic version '{0}'. CONTAINER2019: version sémantique du kit SDK non valide '{0}'. - {StrBegins="CONTAINER2019: "} + {StrBegin="CONTAINER2019: "} CONTAINER2007: Invalid {0} provided: {1}. Image tags must be alphanumeric, underscore, hyphen, or period. CONTAINER2007: {0} non valide fournie : {1}. Les balises d’image doivent être alphanumériques, traits de soulignement, traits d’union ou point. - {StrBegins="CONTAINER2007: "} + {StrBegin="CONTAINER2007: "} CONTAINER2010: Invalid {0} provided: {1}. {0} must be a semicolon-delimited list of valid image tags. Image tags must be alphanumeric, underscore, hyphen, or period. CONTAINER2010: {0} non valide fournie : {1}. {0} doit être une liste de balises d’image valides délimitées par des points-virgules. Les balises d’image doivent être alphanumériques, traits de soulignement, traits d’union ou point. - {StrBegins="CONTAINER2010: "} - - - Invalid string[] TargetRuntimeIdentifiers. Either all should be 'linux-musl' or none. - Chaîne[] TargetRuntimeIdentifiers non valide. Soit tout doit être 'linux-keyl', soit aucun. - + {StrBegin="CONTAINER2010: "} CONTAINER1003: Token response had neither token nor access_token. CONTAINER1003: la réponse de jeton n’avait ni jeton ni access_token. - {StrBegins="CONTAINER1003: "} + {StrBegin="CONTAINER1003: "} CONTAINER4005: Item '{0}' contains items without metadata 'Value', and they will be ignored. CONTAINER4005: l’élément '{0}' contient des éléments sans métadonnées 'Value'. Ils seront ignorés. - {StrBegins="CONTAINER4005: "} + {StrBegin="CONTAINER4005: "} CONTAINER1012: The local registry is not available, but pushing to a local registry was requested. - CONTAINER1012: Le registre local n'est pas disponible, mais la transmission vers un registre local a été demandée. - {StrBegins="CONTAINER1012: "} + CONTAINER1012: The local registry is not available, but pushing to a local registry was requested. + {StrBegin="CONTAINER1012: "} Error while reading daemon config: {0} @@ -307,22 +225,17 @@ CONTAINER2004: Unable to download layer with descriptor '{0}' from registry '{1}' because it does not exist. CONTAINER2004: impossible de télécharger la couche avec le descripteur '{0}' à partir du Registre '{1}', car elle n’existe pas. - {StrBegins="CONTAINER2004: "} + {StrBegin="CONTAINER2004: "} CONTAINER2016: ContainerPort item '{0}' does not specify the port number. Please ensure the item's Include is a port number, for example '<ContainerPort Include="80" />' CONTAINER2016: l’élément ContainerPort '{0}' ne spécifie pas le numéro de port. Vérifiez que l’élément Include est un numéro de port, par exemple '<ContainerPort Include="80" />' - {StrBegins="CONTAINER2016: "} - - - 'mediaType' of manifests should be the same in image index. - 'mediaType' des manifestes doit être identique dans l’index d’image. - + {StrBegin="CONTAINER2016: "} CONTAINER1004: No RequestUri specified. CONTAINER1004: aucun RequestUri spécifié. - {StrBegins="CONTAINER1004: "} + {StrBegin="CONTAINER1004: "} '{0}' was not a valid container image name, it was normalized to '{1}' @@ -332,27 +245,17 @@ CONTAINER2011: {0} '{1}' does not exist CONTAINER2011: {0} '{1}' n’existe pas - {StrBegins="CONTAINER2011: "} - - - CONTAINER1017: Unable to communicate with the registry '{0}'. - CONTAINER1017: nous n’avons pas pu communiquer avec le Registre '{0}'. - {StrBegins="CONTAINER1017:" } + {StrBegin="CONTAINER2011: "} CONTAINER1013: Failed to push to the output registry: {0} CONTAINER1013: échec de l’envoi (push) vers le Registre de sortie : {0} - {StrBegins="CONTAINER1013: "} - - - CONTAINER1014: Manifest pull failed. - CONTAINER1014: échec du tirage (pull) du manifeste. - {StrBegins="CONTAINER1014: "} + {StrBegin="CONTAINER1013: "} - CONTAINER1005: Registry push failed; received status code '{0}'. - CONTAINER1005: échec de l’envoi (push) du Registre ; code d’état « {0} » reçu. - {StrBegins="CONTAINER1005: "} + CONTAINER1005: Registry push failed. + CONTAINER1005: échec de l’envoi (push) du Registre. + {StrBegin="CONTAINER1005: "} Uploading config to registry at blob '{0}', @@ -399,70 +302,45 @@ Balise «{0}» chargée sur «{1}». {1} is the registry name - - CONTAINER1015: Unable to access the repository '{0}' at tag '{1}' in the registry '{2}'. Please confirm that this name and tag are present in the registry. - CONTAINER1015: nous n’avons pas pu accéder au référentiel '{0}' à la balise '{1}' dans le Registre '{2}'. Veuillez confirmer que ce nom et cette balise sont présents dans le Registre. - {StrBegins="CONTAINER1015: "} - CONTAINER4003: Required '{0}' items contain empty items. CONTAINER4003: les éléments de '{0}' obligatoires contiennent des éléments vides. - {StrBegins="CONTAINER4003: "} + {StrBegin="CONTAINER4003: "} CONTAINER4002: Required '{0}' items were not set. CONTAINER4002: les éléments de '{0}' requis n’ont pas été définis. - {StrBegins="CONTAINER4002: "} + {StrBegin="CONTAINER4002: "} CONTAINER4001: Required property '{0}' was not set or empty. CONTAINER4001: la propriété requise '{0}' n’a pas été définie ou vide. - {StrBegins="CONTAINER4001: "} + {StrBegin="CONTAINER4001: "} CONTAINER1006: Too many retries, stopping. CONTAINER1006: trop de tentatives, arrêt. - {StrBegins="CONTAINER1006: "} - - - CONTAINER1016: Unable to access the repository '{0}' in the registry '{1}'. Please confirm your credentials are correct and that you have access to this repository and registry. - CONTAINER1016: nous n’avons pas pu accéder au référentiel '{0}' dans le Registre '{1}'. Confirmez que vos informations d’identification sont correctes et que vous avez accès à ce référentiel et à ce Registre. - {StrBegins="CONTAINER1016:" } - - - CONTAINER1018: Unable to download image from the repository '{0}'. - CONTAINER1018: Désolé, nous ne pouvons pas télécharger l’image depuis le référentiel « {0} ». - {StrBegins="CONTAINER1018:" } + {StrBegin="CONTAINER1006: "} CONTAINER2021: Unknown AppCommandInstruction '{0}'. Valid instructions are {1}. - CONTAINER2021: instruction de commande d'application inconnue '{0}'. Les instructions valides sont {1}. - {StrBegins="CONTAINER2021: "} + CONTAINER2021: Unknown AppCommandInstruction '{0}'. Valid instructions are {1}. + {StrBegin="CONTAINER2021: "} CONTAINER2002: Unknown local registry type '{0}'. Valid local container registry types are {1}. - CONTAINER2002: type de registre local inconnu '{0}'. Les types de registre de conteneurs locaux valides sont {1}. - {StrBegins="CONTAINER2002: "} + CONTAINER2002: Unknown local registry type '{0}'. Valid local container registry types are {1}. + {StrBegin="CONTAINER2002: "} CONTAINER2003: The manifest for {0}:{1} from registry {2} was an unknown type: {3}. Please raise an issue at https://github.com/dotnet/sdk-container-builds/issues with this message. CONTAINER2003: le manifeste pour {0}:{1} du Registre {2} était d’un type inconnu : {3}. Veuillez lever un problème au https://github.com/dotnet/sdk-container-builds/issues avec ce message. - {StrBegins="CONTAINER2003: "} + {StrBegin="CONTAINER2003: "} CONTAINER2001: Unrecognized mediaType '{0}'. CONTAINER2001: '{0}' mediaType non reconnu. - {StrBegins="CONTAINER2001: "} - - - Cannot create image index for the provided 'mediaType' = '{0}'. - Impossible de créer l’index d’image pour le 'mediaType' = '{0}' fourni. - - - - Unable to create tarball for mediaType '{0}'. - Impossible de créer tarball pour mediaType '{0}'. - + {StrBegin="CONTAINER2001: "} CONTAINER0000: Value for unit test {0} diff --git a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.it.xlf b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.it.xlf index eaf97ac4648f..3c91c77a4ae5 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.it.xlf +++ b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.it.xlf @@ -5,73 +5,61 @@ CONTAINER1002: Request to Amazon Elastic Container Registry failed prematurely. This is often caused when the target repository does not exist in the registry. CONTAINER1002: la richiesta ad Amazon Elastic Container Registry non è riuscita in modo anomalo. Questo problema si verifica spesso quando il repository di destinazione non esiste nel registro. - {StrBegins="CONTAINER1002: "} + {StrBegin="CONTAINER1002: "} CONTAINER2008: Both {0} and {1} were provided, but only one or the other is allowed. CONTAINER2008: sono stati forniti sia {0} che {1}, ma è consentito specificare solo uno di questi valori. - {StrBegins="CONTAINER2008: "} + {StrBegin="CONTAINER2008: "} CONTAINER2025: ContainerAppCommandArgs are provided without specifying a ContainerAppCommand. - CONTAINER2025: sono stati forniti ContainerAppCommandArgs senza specificare un elemento ContainerAppCommand. - {StrBegins="CONTAINER2025: "} + CONTAINER2025: ContainerAppCommandArgs are provided without specifying a ContainerAppCommand. + {StrBegin="CONTAINER2025: "} CONTAINER2026: ContainerAppCommand and ContainerAppCommandArgs must be empty when ContainerAppCommandInstruction is '{0}'. - CONTAINER2026: ContainerAppCommand e ContainerAppCommandArgs devono essere vuoti quando ContainerAppCommandInstruction è '{0}'. - {StrBegins="CONTAINER2026: "} - - - local archive at '{0}' - archivio locale in '{0}' - {0} is the path to the file written + CONTAINER2026: ContainerAppCommand and ContainerAppCommandArgs must be empty when ContainerAppCommandInstruction is '{0}'. + {StrBegin="CONTAINER2026: "} CONTAINER2022: The base image has an entrypoint that will be overwritten to start the application. Set ContainerAppCommandInstruction to 'Entrypoint' if this is desired. To preserve the base image entrypoint, set ContainerAppCommandInstruction to 'DefaultArgs'. - CONTAINER2022: l'immagine di base contiene un punto di ingresso che verrà sovrascritto per avviare l'applicazione. Impostare ContainerAppCommandInstruction su 'Entrypoint', se necessario. Per mantenere il punto di ingresso dell'immagine di base, impostare ContainerAppCommandInstruction su 'DefaultArgs'. - {StrBegins="CONTAINER2022: "} + CONTAINER2022: The base image has an entrypoint that will be overwritten to start the application. Set ContainerAppCommandInstruction to 'Entrypoint' if this is desired. To preserve the base image entrypoint, set ContainerAppCommandInstruction to 'DefaultArgs'. + {StrBegin="CONTAINER2022: "} CONTAINER2009: Could not parse {0}: {1} CONTAINER2009: non è stato possibile analizzare {0}: {1} - {StrBegins="CONTAINER2009: "} + {StrBegin="CONTAINER2009: "} CONTAINER2020: {0} does not specify a registry and will be pulled from Docker Hub. Please prefix the name with the image registry, for example: '{1}/<image>'. - CONTAINER2020: {0} non specifica un Registro di sistema e verrà estratto da Docker Hub. Aggiungere al nome il prefisso del registro immagini, ad esempio '{1}/<image>'. - {StrBegins="CONTAINER2020: "} + CONTAINER2020: {0} does not specify a registry and will be pulled from Docker Hub. Please prefix the name with the image registry, for example: '{1}/<image>'. + {StrBegin="CONTAINER2020: "} CONTAINER2013: {0} had spaces in it, replacing with dashes. CONTAINER2013: {0} conteneva spazi, che verranno sostituiti con trattini. - {StrBegins="CONTAINER2013: "} + {StrBegin="CONTAINER2013: "} CONTAINER1011: Couldn't find matching base image for {0} that matches RuntimeIdentifier {1}. CONTAINER1011: non è stato possibile trovare l'immagine di base corrispondente per {0} che corrisponde a RuntimeIdentifier {1}. - {StrBegins="CONTAINER1011: "} + {StrBegin="CONTAINER1011: "} CONTAINER1001: Failed to upload blob using {0}; received status code '{1}'. CONTAINER1001: non è stato possibile caricare il BLOB usando {0}; codice di stato ricevuto '{1}'. - {StrBegins="CONTAINER1001: "} - - - Building image index '{0}' on top of manifests {1}. - Compilazione dell'indice delle immagini '{0}' sui manifesti {1}. - - {0} is the name of the image index and its tag, {1} is the list of manifests digests - + {StrBegin="CONTAINER1001: "} - Pushed image '{0}' to {1}. - È stato eseguito il push dell'immagine '{0}' in {1}. + Pushed container '{0}' to Docker daemon. + È stato eseguito il push del contenitore '{0}' nel daemon Docker. - Pushed image '{0}' to registry '{1}'. - È stato eseguito il push dell'immagine '{0}' nel Registro '{1}'. + Pushed container '{0}' to registry '{1}'. + È stato eseguito il push del contenitore '{0}' nel registro '{1}'. @@ -79,220 +67,150 @@ Compilazione dell'immagine '{0}' con i tag '{1}' sopra l'immagine di base '{2}'. - - Building image '{0}' for runtime identifier '{1}' on top of base image '{2}'. - Creazione dell'immagine '{0}' per l'identificatore di runtime '{1}' sopra l'immagine di base '{2}'. - - CONTAINER1007: Could not deserialize token from JSON. CONTAINER1007: non è stato possibile deserializzare il token da JSON. - {StrBegins="CONTAINER1007: "} + {StrBegin="CONTAINER1007: "} CONTAINER2012: Could not recognize registry '{0}'. CONTAINER2012: non è stato possibile riconoscere il registro '{0}'. - {StrBegins="CONTAINER2012: "} - - - local registry via '{0}' - registro locale tramite '{0}' - {0} is the command used + {StrBegin="CONTAINER2012: "} CONTAINER3002: Failed to get docker info({0})\n{1}\n{2} CONTAINER3002: non è stato possibile ottenere le informazioni su Docker ({0})\n{1}\n{2} - {StrBegins="CONTAINER3002: "} + {StrBegin="CONTAINER3002: "} CONTAINER3002: Failed to get docker info: {0} CONTAINER3002: non è stato possibile ottenere le informazioni su Docker: {0} - {StrBegins="CONTAINER3002: "} + {StrBegin="CONTAINER3002: "} - + CONTAINER3001: Failed creating {0} process. - CONTAINER3001: creazione del processo {0} non riuscita. + CONTAINER3001: Failed creating {0} process. CONTAINER3001: {0} is the name of the command we failed to run, usually 'docker' or 'podman'. CONTAINER4006: Property '{0}' is empty or contains whitespace and will be ignored. CONTAINER4006: la proprietà '{0}' è vuota o contiene spazi vuoti e verrà ignorata. - {StrBegins="CONTAINER4006: "} + {StrBegin="CONTAINER4006: "} CONTAINER4004: Items '{0}' contain empty item(s) which will be ignored. CONTAINER4004: gli elementi '{0}' contengono elementi vuoti che verranno ignorati. - {StrBegins="CONTAINER4004: "} + {StrBegin="CONTAINER4004: "} CONTAINER2023: A ContainerEntrypoint and ContainerAppCommandArgs are provided. ContainerAppInstruction must be set to configure how the application is started. Valid instructions are {0}. - CONTAINER2023: sono stati forniti ContainerEntrypoint e ContainerAppCommandArgs. ContainerAppInstruction deve essere impostato per configurare la modalità di avvio dell'applicazione. Istruzioni valide sono {0}. - {StrBegins="CONTAINER2023: "} + CONTAINER2023: A ContainerEntrypoint and ContainerAppCommandArgs are provided. ContainerAppInstruction must be set to configure how the application is started. Valid instructions are {0}. + {StrBegin="CONTAINER2023: "} CONTAINER2027: A ContainerEntrypoint is provided. ContainerAppInstruction must be set to configure how the application is started. Valid instructions are {0}. - CONTAINER2027: è stato fornito ContainerEntrypoint. ContainerAppInstruction deve essere impostato per configurare la modalità di avvio dell'applicazione. Istruzioni valide sono {0}. - {StrBegins="CONTAINER2027: "} + CONTAINER2027: A ContainerEntrypoint is provided. ContainerAppInstruction must be set to configure how the application is started. Valid instructions are {0}. + {StrBegin="CONTAINER2027: "} CONTAINER2024: ContainerEntrypointArgs are provided without specifying a ContainerEntrypoint. - CONTAINER2024: sono stati forniti ContainerEntrypointArgs senza specificare un elemento ContainerEntrypoint. - {StrBegins="CONTAINER2024: "} + CONTAINER2024: ContainerEntrypointArgs are provided without specifying a ContainerEntrypoint. + {StrBegin="CONTAINER2024: "} CONTAINER2029: ContainerEntrypointArgsSet are provided. Change to use ContainerAppCommandArgs for arguments that must always be set, or ContainerDefaultArgs for arguments that can be overridden when the container is created. - CONTAINER2029: è stato fornito ContainerEntrypointArgsSet. Modificare per usare ContainerAppCommandArgs per gli argomenti che devono essere sempre impostati o ContainerDefaultArgs per gli argomenti di cui è possibile eseguire l'override quando viene creato il contenitore. - {StrBegins="CONTAINER2029: "} + CONTAINER2029: ContainerEntrypointArgsSet are provided. Change to use ContainerAppCommandArgs for arguments that must always be set, or ContainerDefaultArgs for arguments that can be overridden when the container is created. + {StrBegin="CONTAINER2029: "} CONTAINER2028: ContainerEntrypoint can not be combined with ContainerAppCommandInstruction '{0}'. - CONTAINER2028: non è possibile combinare ContainerEntrypoint con '{0}' ContainerAppCommandInstruction. - {StrBegins="CONTAINER2028: "} + CONTAINER2028: ContainerEntrypoint can not be combined with ContainerAppCommandInstruction '{0}'. + {StrBegin="CONTAINER2028: "} CONTAINER1008: Failed retrieving credentials for "{0}": {1} CONTAINER1008: non è stato possibile recuperare le credenziali per "{0}": {1} - {StrBegins="CONTAINER1008: "} - - - CONTAINER2030: GenerateLabels was disabled but GenerateDigestLabel was enabled - no digest label will be created. - CONTAINER2030: GenerateLabels è stato disabilitato ma GenerateDigestLabel è stato abilitato. Non verrà creata alcuna etichetta digest. - {StrBegins="CONTAINER2030: "} + {StrBegin="CONTAINER1008: "} No host object detected. Nessun oggetto host rilevato. - - Cannot create image index because at least one of the provided images' config is missing 'architecture'. - Non è possibile creare l'indice dell'immagine perché in almeno una delle configurazioni delle immagini fornite manca 'architecture'. - - - - Cannot create image index because at least one of the provided images' config is missing 'os'. - Non è possibile creare l'indice dell'immagine perché in almeno una delle configurazioni delle immagini fornite manca 'os'. - - - - Pushed image index '{0}' to registry '{1}'. - È stato eseguito il push dell'indice immagine "{0}" nel registro "{1}". - - - - Image index creation for Podman is not supported. - La creazione dell'indice delle immagini per Podman non è supportata. - - CONTAINER1009: Failed to load image from local registry. stdout: {0} - CONTAINER1009: non è stato possibile caricare l'immagine dal registro locale. stdout: {0} - {StrBegins="CONTAINER1009: "} - - - CONTAINER1020: Failed to load image because containerd image store is not enabled for Docker. Tip: You can enable it by checking 'Use containerd for pulling and storing images' in Docker Desktop settings. - CONTAINER1020: non è stato possibile caricare l'immagine perché l'archivio immagini in contenitori non è abilitato per Docker. Suggerimento: è possibile abilitarlo selezionando 'Usa contenitore per il pull e l'archiviazione delle immagini' nelle impostazioni di Docker Desktop. - {StrBegin="CONTAINER1020: "} + CONTAINER1009: non è stato possibile caricare l'immagine nel daemon Docker locale. stdout: {0} + {StrBegin="CONTAINER1009: "} CONTAINER1010: Pulling images from local registry is not supported. - CONTAINER1010: il pull di immagini dal registro locale non è supportato. - {StrBegins="CONTAINER1010: "} + CONTAINER1010: Pulling images from local registry is not supported. + {StrBegin="CONTAINER1010: "} - - Cannot create image index because no images were provided. - Non è possibile creare l'indice dell'immagine perché non sono state specificate immagini. - - - - CONTAINER2031: The container image format '{0}' is not supported. Supported formats are '{1}'. - CONTAINER2031: il formato dell'immagine contenitore '{0}' non è supportato. I formati supportati sono '{1}'. - {StrBegins="CONTAINER2031: "} + + CONTAINER2014: Invalid {0}: {1}. + CONTAINER2014: {0}non valido: {1}. + {StrBegin="CONTAINER2014: "} CONTAINER2015: {0}: '{1}' was not a valid Environment Variable. Ignoring. CONTAINER2015: {0}: '{1}' non è una variabile di ambiente valida. Il valore verrà ignorato. - {StrBegins="CONTAINER2015: "} + {StrBegin="CONTAINER2015: "} - - Cannot create image index because at least one of the provided images' config is invalid. - Non è possibile creare l'indice dell'immagine perché almeno una delle configurazioni delle immagini specificate non è valida. - - - - Cannot create image index because at least one of the provided images' manifest is invalid. - Non è possibile creare l'indice dell'immagine perché almeno uno dei manifesti delle immagini forniti non è valido. - - - - Cannot create image index because provided images are invalid. Items must have 'Config', 'Manifest', 'ManifestMediaType' and 'ManifestDigest' metadata. - Non è possibile creare l'indice dell'immagine perché le immagini specificate non sono valide. Gli elementi devono contenere i metadati 'Config', 'Manifest', 'ManifestMediaType' e 'ManifestDigest'. - - - - CONTAINER2005: The inferred image name '{0}' contains entirely invalid characters. The valid characters for an image name are alphanumeric characters, -, /, or _, and the image name must start with an alphanumeric character. - CONTAINER2005: il nome dell'immagine dedotto '{0}' contiene caratteri completamente non validi. I caratteri validi per un nome di immagine sono caratteri alfanumerici, -, / o _, e il nome dell'immagine deve iniziare con un carattere alfanumerico. - {StrBegins="CONTAINER2005: "} - - - CONTAINER2005: The first character of the image name '{0}' must be a lowercase letter or a digit and all characters in the name must be an alphanumeric character, -, /, or _. - CONTAINER2005: il primo carattere del nome dell'immagine '{0}' deve essere una lettera minuscola o una cifra e tutti i caratteri nel nome devono essere un carattere alfanumerico, -, /o _. - {StrBegins="CONTAINER2005: "} + + CONTAINER2005: The first character of the image name must be a lowercase letter or a digit. + CONTAINER2005: il primo carattere del nome dell'immagine deve essere una lettera minuscola o una cifra. + {StrBegin="CONTAINER2005: "} CONTAINER2017: A ContainerPort item was provided with an invalid port number '{0}'. ContainerPort items must have an Include value that is an integer, and a Type value that is either 'tcp' or 'udp'. CONTAINER2017: un elemento ContainerPort è stato fornito con un numero di porta '{0}' non valido. Gli elementi ContainerPort devono avere un valore Include che è un numero intero e un valore Type impostato su 'tcp' o 'udp'. - {StrBegins="CONTAINER2017: "} + {StrBegin="CONTAINER2017: "} CONTAINER2017: A ContainerPort item was provided with an invalid port number '{0}' and an invalid port type '{1}'. ContainerPort items must have an Include value that is an integer, and a Type value that is either 'tcp' or 'udp'. CONTAINER2017: un elemento ContainerPort è stato fornito con un numero di porta '{0}' non valido e un tipo di porta '{1}' non valido. Gli elementi ContainerPort devono avere un valore Include che è un numero intero e un valore Type impostato su 'tcp' o 'udp'. - {StrBegins="CONTAINER2017: "} + {StrBegin="CONTAINER2017: "} CONTAINER2017: A ContainerPort item was provided with an invalid port type '{0}'. ContainerPort items must have an Include value that is an integer, and a Type value that is either 'tcp' or 'udp'. CONTAINER2017: un elemento ContainerPort è stato fornito con un tipo di porta '{0}' non valido. Gli elementi ContainerPort devono avere un valore Include che è un numero intero e un valore Type impostato su 'tcp' o 'udp'. - {StrBegins="CONTAINER2017: "} + {StrBegin="CONTAINER2017: "} CONTAINER2018: Invalid SDK prerelease version '{0}' - only 'rc' and 'preview' are supported. CONTAINER2018: valore '{0}' non valido per la versione non definitiva dell'SDK - Sono supportate solo le versioni 'rc' e 'preview'. - {StrBegins="CONTAINER2018: "} + {StrBegin="CONTAINER2018: "} CONTAINER2019: Invalid SDK semantic version '{0}'. CONTAINER2019: versione semantica dell'SDK non valida '{0}'. - {StrBegins="CONTAINER2019: "} + {StrBegin="CONTAINER2019: "} CONTAINER2007: Invalid {0} provided: {1}. Image tags must be alphanumeric, underscore, hyphen, or period. CONTAINER2007: il valore {0} specificato non è valido: {1}. I tag immagine devono essere alfanumerici, di sottolineatura, trattino o punto. - {StrBegins="CONTAINER2007: "} + {StrBegin="CONTAINER2007: "} CONTAINER2010: Invalid {0} provided: {1}. {0} must be a semicolon-delimited list of valid image tags. Image tags must be alphanumeric, underscore, hyphen, or period. CONTAINER2010: il valore {0} specificato non è valido: {1}. {0} deve essere un elenco delimitato da punto e virgola di tag di immagine validi. I tag immagine devono essere alfanumerici, di sottolineatura, trattino o punto. - {StrBegins="CONTAINER2010: "} - - - Invalid string[] TargetRuntimeIdentifiers. Either all should be 'linux-musl' or none. - Stringa non valida[] TargetRuntimeIdentifiers. Tutti devono essere 'linux-musl' o nessuno. - + {StrBegin="CONTAINER2010: "} CONTAINER1003: Token response had neither token nor access_token. CONTAINER1003: la risposta del token non contiene né token né access_token. - {StrBegins="CONTAINER1003: "} + {StrBegin="CONTAINER1003: "} CONTAINER4005: Item '{0}' contains items without metadata 'Value', and they will be ignored. CONTAINER4005: l'elemento '{0}' contiene elementi senza 'Value' di metadati e tali elementi verranno ignorati. - {StrBegins="CONTAINER4005: "} + {StrBegin="CONTAINER4005: "} CONTAINER1012: The local registry is not available, but pushing to a local registry was requested. - CONTAINER1012: il registro locale non è disponibile, ma è stato richiesto il push a un registro locale. - {StrBegins="CONTAINER1012: "} + CONTAINER1012: The local registry is not available, but pushing to a local registry was requested. + {StrBegin="CONTAINER1012: "} Error while reading daemon config: {0} @@ -307,22 +225,17 @@ CONTAINER2004: Unable to download layer with descriptor '{0}' from registry '{1}' because it does not exist. CONTAINER2004: non è possibile scaricare il livello con descrittore '{0}' dal registro '{1}' perché non esiste. - {StrBegins="CONTAINER2004: "} + {StrBegin="CONTAINER2004: "} CONTAINER2016: ContainerPort item '{0}' does not specify the port number. Please ensure the item's Include is a port number, for example '<ContainerPort Include="80" />' CONTAINER2016: l'elemento ContainerPort '{0}' non specifica il numero di porta. Assicurarsi che il valore Include dell'elemento sia un numero di porta, ad esempio '<ContainerPort Include="80" />' - {StrBegins="CONTAINER2016: "} - - - 'mediaType' of manifests should be the same in image index. - 'mediaType' dei manifesti deve essere lo stesso nell'indice dell'immagine. - + {StrBegin="CONTAINER2016: "} CONTAINER1004: No RequestUri specified. CONTAINER1004: nessun RequestUri specificato. - {StrBegins="CONTAINER1004: "} + {StrBegin="CONTAINER1004: "} '{0}' was not a valid container image name, it was normalized to '{1}' @@ -332,27 +245,17 @@ CONTAINER2011: {0} '{1}' does not exist CONTAINER2011: {0} '{1}' non esiste - {StrBegins="CONTAINER2011: "} - - - CONTAINER1017: Unable to communicate with the registry '{0}'. - CONTAINER1017: impossibile comunicare con il Registro di sistema '{0}'. - {StrBegins="CONTAINER1017:" } + {StrBegin="CONTAINER2011: "} CONTAINER1013: Failed to push to the output registry: {0} CONTAINER1013: non è stato possibile eseguire il push nel registro di output: {0} - {StrBegins="CONTAINER1013: "} - - - CONTAINER1014: Manifest pull failed. - CONTAINER1014: pull del manifesto non riuscito. - {StrBegins="CONTAINER1014: "} + {StrBegin="CONTAINER1013: "} - CONTAINER1005: Registry push failed; received status code '{0}'. - CONTAINER1005: push del Registro di sistema non riuscito; ricevuto codice di stato '{0}'. - {StrBegins="CONTAINER1005: "} + CONTAINER1005: Registry push failed. + CONTAINER1005: push del registro non riuscito. + {StrBegin="CONTAINER1005: "} Uploading config to registry at blob '{0}', @@ -399,70 +302,45 @@ Tag caricato '{0}' in '{1}'. {1} is the registry name - - CONTAINER1015: Unable to access the repository '{0}' at tag '{1}' in the registry '{2}'. Please confirm that this name and tag are present in the registry. - CONTAINER1015: impossibile accedere al repository '{0}' nel tag '{1}' nel Registro di sistema '{2}'. Verificare che il nome e il tag siano presenti nel Registro di sistema. - {StrBegins="CONTAINER1015: "} - CONTAINER4003: Required '{0}' items contain empty items. CONTAINER4003: gli elementi '{0}' obbligatori contengono elementi vuoti. - {StrBegins="CONTAINER4003: "} + {StrBegin="CONTAINER4003: "} CONTAINER4002: Required '{0}' items were not set. CONTAINER4002: gli elementi '{0}' obbligatori non sono stati impostati. - {StrBegins="CONTAINER4002: "} + {StrBegin="CONTAINER4002: "} CONTAINER4001: Required property '{0}' was not set or empty. CONTAINER4001: la proprietà obbligatoria '{0}' non è stata impostata o è vuota. - {StrBegins="CONTAINER4001: "} + {StrBegin="CONTAINER4001: "} CONTAINER1006: Too many retries, stopping. CONTAINER1006: troppi tentativi, arresto. - {StrBegins="CONTAINER1006: "} - - - CONTAINER1016: Unable to access the repository '{0}' in the registry '{1}'. Please confirm your credentials are correct and that you have access to this repository and registry. - CONTAINER1016: impossibile accedere al repository '{0}' nel Registro di sistema '{1}'. Verificare che le credenziali siano corrette e di avere accesso a questo repository e registro. - {StrBegins="CONTAINER1016:" } - - - CONTAINER1018: Unable to download image from the repository '{0}'. - CONTAINER1018: Non è possibile scaricare l'immagine dal repository '{0}'. - {StrBegins="CONTAINER1018:" } + {StrBegin="CONTAINER1006: "} CONTAINER2021: Unknown AppCommandInstruction '{0}'. Valid instructions are {1}. - CONTAINER2021: appCommandInstruction '{0}'sconosciuta. Istruzioni valide sono {1}. - {StrBegins="CONTAINER2021: "} + CONTAINER2021: Unknown AppCommandInstruction '{0}'. Valid instructions are {1}. + {StrBegin="CONTAINER2021: "} CONTAINER2002: Unknown local registry type '{0}'. Valid local container registry types are {1}. - CONTAINER2002: tipo di registro locale '{0}' sconosciuto. I tipi di registri contenitore locali validi sono {1}. - {StrBegins="CONTAINER2002: "} + CONTAINER2002: Unknown local registry type '{0}'. Valid local container registry types are {1}. + {StrBegin="CONTAINER2002: "} CONTAINER2003: The manifest for {0}:{1} from registry {2} was an unknown type: {3}. Please raise an issue at https://github.com/dotnet/sdk-container-builds/issues with this message. CONTAINER2003: il manifesto per {0}:{1} dal registro {2} era un tipo sconosciuto: {3}. Segnalare un problema in https://github.com/dotnet/sdk-container-builds/issues con questo messaggio. - {StrBegins="CONTAINER2003: "} + {StrBegin="CONTAINER2003: "} CONTAINER2001: Unrecognized mediaType '{0}'. CONTAINER2001: mediaType '{0}' non riconosciuto. - {StrBegins="CONTAINER2001: "} - - - Cannot create image index for the provided 'mediaType' = '{0}'. - Non è possibile creare l'indice dell'immagine per il valore 'mediaType' = '{0}' specificato. - - - - Unable to create tarball for mediaType '{0}'. - Impossibile creare il tarball per un mediaType '{0}'. - + {StrBegin="CONTAINER2001: "} CONTAINER0000: Value for unit test {0} diff --git a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.ja.xlf b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.ja.xlf index 8c99589574ad..b6563ce5fcab 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.ja.xlf +++ b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.ja.xlf @@ -5,73 +5,61 @@ CONTAINER1002: Request to Amazon Elastic Container Registry failed prematurely. This is often caused when the target repository does not exist in the registry. CONTAINER1002: Amazon Elastic Container Registry への要求が処理の途中で失敗しました。これは多くの場合、ターゲット リポジトリがレジストリに存在しない場合に発生します。 - {StrBegins="CONTAINER1002: "} + {StrBegin="CONTAINER1002: "} CONTAINER2008: Both {0} and {1} were provided, but only one or the other is allowed. CONTAINER2008: {0} と {1} の両方が指定されましたが、どちらか一方のみが許可されています。 - {StrBegins="CONTAINER2008: "} + {StrBegin="CONTAINER2008: "} CONTAINER2025: ContainerAppCommandArgs are provided without specifying a ContainerAppCommand. - CONTAINER2025: ContainerAppCommandArgs が指定されていますが、ContainerAppCommand が指定されていません。 - {StrBegins="CONTAINER2025: "} + CONTAINER2025: ContainerAppCommandArgs are provided without specifying a ContainerAppCommand. + {StrBegin="CONTAINER2025: "} CONTAINER2026: ContainerAppCommand and ContainerAppCommandArgs must be empty when ContainerAppCommandInstruction is '{0}'. - CONTAINER2026: ContainerAppCommandInstruction が '{0}' の場合、ContainerAppCommand と ContainerAppCommandArgs を空にする必要があります。 - {StrBegins="CONTAINER2026: "} - - - local archive at '{0}' - ローカル アーカイブ ('{0}') - {0} is the path to the file written + CONTAINER2026: ContainerAppCommand and ContainerAppCommandArgs must be empty when ContainerAppCommandInstruction is '{0}'. + {StrBegin="CONTAINER2026: "} CONTAINER2022: The base image has an entrypoint that will be overwritten to start the application. Set ContainerAppCommandInstruction to 'Entrypoint' if this is desired. To preserve the base image entrypoint, set ContainerAppCommandInstruction to 'DefaultArgs'. - CONTAINER2022: 基本イメージに、アプリケーションの開始時に上書きされるエントリ ポイントがあります。このまま上書きする場合は、ContainerAppCommandInstruction を 'Entrypoint' に設定します。基本イメージのエントリ ポイントを保持するには、ContainerAppCommandInstruction を 'DefaultArgs' に設定します。 - {StrBegins="CONTAINER2022: "} + CONTAINER2022: The base image has an entrypoint that will be overwritten to start the application. Set ContainerAppCommandInstruction to 'Entrypoint' if this is desired. To preserve the base image entrypoint, set ContainerAppCommandInstruction to 'DefaultArgs'. + {StrBegin="CONTAINER2022: "} CONTAINER2009: Could not parse {0}: {1} CONTAINER2009: {0} を解析できませんでした: {1} - {StrBegins="CONTAINER2009: "} + {StrBegin="CONTAINER2009: "} CONTAINER2020: {0} does not specify a registry and will be pulled from Docker Hub. Please prefix the name with the image registry, for example: '{1}/<image>'. - CONTAINER2020: {0} でレジストリが指定されていないため、Docker Hub からプルされます。名前の前にイメージ レジストリを付けてください。例: '{1}/<image>'。 - {StrBegins="CONTAINER2020: "} + CONTAINER2020: {0} does not specify a registry and will be pulled from Docker Hub. Please prefix the name with the image registry, for example: '{1}/<image>'. + {StrBegin="CONTAINER2020: "} CONTAINER2013: {0} had spaces in it, replacing with dashes. CONTAINER2013: {0} にスペースが含まれており、ダッシュに置き換えています。 - {StrBegins="CONTAINER2013: "} + {StrBegin="CONTAINER2013: "} CONTAINER1011: Couldn't find matching base image for {0} that matches RuntimeIdentifier {1}. CONTAINER1011: RuntimeIdentifier {1} に一致する {0} に一致する基本イメージが見つかりませんでした。 - {StrBegins="CONTAINER1011: "} + {StrBegin="CONTAINER1011: "} CONTAINER1001: Failed to upload blob using {0}; received status code '{1}'. CONTAINER1001: {0} を使用して BLOB をアップロードできませんでした; 状態コード '{1}' を受信しました。 - {StrBegins="CONTAINER1001: "} - - - Building image index '{0}' on top of manifests {1}. - マニフェスト {1} の上にイメージ インデックス '{0}' を構築しています。 - - {0} is the name of the image index and its tag, {1} is the list of manifests digests - + {StrBegin="CONTAINER1001: "} - Pushed image '{0}' to {1}. - イメージ '{0}' を {1} にプッシュしました。 + Pushed container '{0}' to Docker daemon. + コンテナー '{0}' を Docker デーモンにプッシュしました。 - Pushed image '{0}' to registry '{1}'. - イメージ '{0}' をレジストリ '{1}' にプッシュしました。 + Pushed container '{0}' to registry '{1}'. + コンテナー '{0}' をレジストリ '{1}' にプッシュしました。 @@ -79,220 +67,150 @@ 基本イメージ '{0}' の上にタグ '{1}' 付きのイメージ '{2}' をビルドしています。 - - Building image '{0}' for runtime identifier '{1}' on top of base image '{2}'. - 基本イメージ '{2}' の上にランタイム識別子 '{1}' のイメージ '{0}' をビルドしています。 - - CONTAINER1007: Could not deserialize token from JSON. CONTAINER1007: JSON からトークンを逆シリアル化できませんでした。 - {StrBegins="CONTAINER1007: "} + {StrBegin="CONTAINER1007: "} CONTAINER2012: Could not recognize registry '{0}'. CONTAINER2012: レジストリ '{0}' を認識できませんでした。 - {StrBegins="CONTAINER2012: "} - - - local registry via '{0}' - '{0}' 経由のローカル レジストリ - {0} is the command used + {StrBegin="CONTAINER2012: "} CONTAINER3002: Failed to get docker info({0})\n{1}\n{2} CONTAINER3002: Docker 情報 ({0})\n{1}\n{2} を取得できませんでした - {StrBegins="CONTAINER3002: "} + {StrBegin="CONTAINER3002: "} CONTAINER3002: Failed to get docker info: {0} CONTAINER3002: Docker 情報を取得できませんでした: {0} - {StrBegins="CONTAINER3002: "} + {StrBegin="CONTAINER3002: "} - + CONTAINER3001: Failed creating {0} process. - CONTAINER3001: {0} プロセスを作成できませんでした。 + CONTAINER3001: Failed creating {0} process. CONTAINER3001: {0} is the name of the command we failed to run, usually 'docker' or 'podman'. CONTAINER4006: Property '{0}' is empty or contains whitespace and will be ignored. CONTAINER4006: プロパティ '{0}' が空であるか、空白文字を含んでいるので無視されます。 - {StrBegins="CONTAINER4006: "} + {StrBegin="CONTAINER4006: "} CONTAINER4004: Items '{0}' contain empty item(s) which will be ignored. CONTAINER4004: 無視される空の項目が '{0}' 項目に含まれています。 - {StrBegins="CONTAINER4004: "} + {StrBegin="CONTAINER4004: "} CONTAINER2023: A ContainerEntrypoint and ContainerAppCommandArgs are provided. ContainerAppInstruction must be set to configure how the application is started. Valid instructions are {0}. - CONTAINER2023: ContainerEntrypoint と ContainerAppCommandArgs が指定されています。アプリケーションの開始方法を構成するには、ContainerAppInstruction を設定する必要があります。有効な手順は {0} です。 - {StrBegins="CONTAINER2023: "} + CONTAINER2023: A ContainerEntrypoint and ContainerAppCommandArgs are provided. ContainerAppInstruction must be set to configure how the application is started. Valid instructions are {0}. + {StrBegin="CONTAINER2023: "} CONTAINER2027: A ContainerEntrypoint is provided. ContainerAppInstruction must be set to configure how the application is started. Valid instructions are {0}. - CONTAINER2027: ContainerEntrypoint が指定されています。アプリケーションの開始方法を構成するには、ContainerAppInstruction を設定する必要があります。有効な手順は {0} です。 - {StrBegins="CONTAINER2027: "} + CONTAINER2027: A ContainerEntrypoint is provided. ContainerAppInstruction must be set to configure how the application is started. Valid instructions are {0}. + {StrBegin="CONTAINER2027: "} CONTAINER2024: ContainerEntrypointArgs are provided without specifying a ContainerEntrypoint. - CONTAINER2024: ContainerEntrypointArgs が指定されていますが、ContainerEntrypoint が指定されていません。 - {StrBegins="CONTAINER2024: "} + CONTAINER2024: ContainerEntrypointArgs are provided without specifying a ContainerEntrypoint. + {StrBegin="CONTAINER2024: "} CONTAINER2029: ContainerEntrypointArgsSet are provided. Change to use ContainerAppCommandArgs for arguments that must always be set, or ContainerDefaultArgs for arguments that can be overridden when the container is created. - CONTAINER2029: ContainerEntrypointArgsSet が使用されています。常に設定されている必要がある引数の場合 ContainerAppCommandArgs を使用するか、コンテナーの作成時にオーバーライドできる引数の場合 ContainerDefaultArgs を使用するように変更してください。 - {StrBegins="CONTAINER2029: "} + CONTAINER2029: ContainerEntrypointArgsSet are provided. Change to use ContainerAppCommandArgs for arguments that must always be set, or ContainerDefaultArgs for arguments that can be overridden when the container is created. + {StrBegin="CONTAINER2029: "} CONTAINER2028: ContainerEntrypoint can not be combined with ContainerAppCommandInstruction '{0}'. - CONTAINER2028: ContainerEntrypoint を ContainerAppCommandInstruction '{0}' と組み合わせることはできません。 - {StrBegins="CONTAINER2028: "} + CONTAINER2028: ContainerEntrypoint can not be combined with ContainerAppCommandInstruction '{0}'. + {StrBegin="CONTAINER2028: "} CONTAINER1008: Failed retrieving credentials for "{0}": {1} CONTAINER1008: "{0}" の資格情報を取得できませんでした: {1} - {StrBegins="CONTAINER1008: "} - - - CONTAINER2030: GenerateLabels was disabled but GenerateDigestLabel was enabled - no digest label will be created. - CONTAINER2030: GenerateLabels は無効にされましたが、GenerateDigestLabel が有効になりました。ダイジェスト ラベルは作成されません。 - {StrBegins="CONTAINER2030: "} + {StrBegin="CONTAINER1008: "} No host object detected. ホスト オブジェクトが検出されませんでした。 - - Cannot create image index because at least one of the provided images' config is missing 'architecture'. - 指定されたイメージの構成の少なくとも 1 つに 'architecture' がないため、イメージ インデックスを作成できません。 - - - - Cannot create image index because at least one of the provided images' config is missing 'os'. - 指定されたイメージの構成の少なくとも 1 つに 'os' がないため、イメージ インデックスを作成できません。 - - - - Pushed image index '{0}' to registry '{1}'. - イメージ インデックス '{0}' をレジストリ '{1}' にプッシュしました。 - - - - Image index creation for Podman is not supported. - Podman のイメージ インデックスの作成はサポートされていません。 - - CONTAINER1009: Failed to load image from local registry. stdout: {0} - CONTAINER1009: ローカル レジストリからイメージを読み込めませんでした。stdout: {0} - {StrBegins="CONTAINER1009: "} - - - CONTAINER1020: Failed to load image because containerd image store is not enabled for Docker. Tip: You can enable it by checking 'Use containerd for pulling and storing images' in Docker Desktop settings. - CONTAINER1020: コンテナー化されたイメージ ストアが Docker に対して有効になっていないため、イメージを読み込めませんでした。ヒント: Docker Desktop 設定で [イメージのプルと保存にコンテナー化を使用する] をオンにすると、有効にすることができます。 - {StrBegin="CONTAINER1020: "} + CONTAINER1009: ローカル Docker デーモンにイメージを読み込めませんでした。stdout: {0} + {StrBegin="CONTAINER1009: "} CONTAINER1010: Pulling images from local registry is not supported. - CONTAINER1010: ローカル レジストリからのイメージのプルはサポートされていません。 - {StrBegins="CONTAINER1010: "} + CONTAINER1010: ローカル Docker デーモンからのイメージのプルはサポートされていません。 + {StrBegin="CONTAINER1010: "} - - Cannot create image index because no images were provided. - イメージ インデックスを作成できません。イメージが指定されていません。 - - - - CONTAINER2031: The container image format '{0}' is not supported. Supported formats are '{1}'. - CONTAINER2031: コンテナー イメージ形式 '{0}' はサポートされていません。サポートされている形式は '{1}' です。 - {StrBegins="CONTAINER2031: "} + + CONTAINER2014: Invalid {0}: {1}. + CONTAINER2014: 無効な {0}: {1}。 + {StrBegin="CONTAINER2014: "} CONTAINER2015: {0}: '{1}' was not a valid Environment Variable. Ignoring. CONTAINER2015: {0}: '{1}' は有効な環境変数ではありませんでした。無視しています。 - {StrBegins="CONTAINER2015: "} + {StrBegin="CONTAINER2015: "} - - Cannot create image index because at least one of the provided images' config is invalid. - 指定されたイメージの構成の少なくとも 1 つが無効であるため、イメージ インデックスを作成できません。 - - - - Cannot create image index because at least one of the provided images' manifest is invalid. - 指定されたイメージのマニフェストの少なくとも 1 つが無効であるため、イメージ インデックスを作成できません。 - - - - Cannot create image index because provided images are invalid. Items must have 'Config', 'Manifest', 'ManifestMediaType' and 'ManifestDigest' metadata. - 指定されたイメージが無効なため、イメージ インデックスを作成できません。項目には、'Config'、'Manifest'、'ManifestMediaType'、'ManifestDigest' のメタデータが必要です。 - - - - CONTAINER2005: The inferred image name '{0}' contains entirely invalid characters. The valid characters for an image name are alphanumeric characters, -, /, or _, and the image name must start with an alphanumeric character. - CONTAINER2005: 推定されたイメージ名 '{0}' に、完全に無効な文字が含まれています。イメージ名に有効な文字は英数字、-、/、または _で、イメージ名の先頭には英数字を使用する必要があります。 - {StrBegins="CONTAINER2005: "} - - - CONTAINER2005: The first character of the image name '{0}' must be a lowercase letter or a digit and all characters in the name must be an alphanumeric character, -, /, or _. - CONTAINER2005: イメージ名 '{0}' は小文字は数字で、名前に含まれるすべての文字は英数字、-、または _ である必要があります。 - {StrBegins="CONTAINER2005: "} + + CONTAINER2005: The first character of the image name must be a lowercase letter or a digit. + CONTAINER2005: イメージ名の最初の文字は、小文字または数字である必要があります。 + {StrBegin="CONTAINER2005: "} CONTAINER2017: A ContainerPort item was provided with an invalid port number '{0}'. ContainerPort items must have an Include value that is an integer, and a Type value that is either 'tcp' or 'udp'. CONTAINER2017: 無効なポート場合 '{0}' を使用して ContainerPort 項目が指定されました。ContainerPort 項目には、整数である Include 値と、'tcp' または 'udp' のいずれかである Type 値が必要です。 - {StrBegins="CONTAINER2017: "} + {StrBegin="CONTAINER2017: "} CONTAINER2017: A ContainerPort item was provided with an invalid port number '{0}' and an invalid port type '{1}'. ContainerPort items must have an Include value that is an integer, and a Type value that is either 'tcp' or 'udp'. CONTAINER2017: 無効なポート番号 '{0}' と無効なポートの種類 '{1}' を使用して ContainerPort 項目が指定されました。ContainerPort 項目には、整数である Include 値と、'tcp' または 'udp' のいずれかである Type 値が必要です。 - {StrBegins="CONTAINER2017: "} + {StrBegin="CONTAINER2017: "} CONTAINER2017: A ContainerPort item was provided with an invalid port type '{0}'. ContainerPort items must have an Include value that is an integer, and a Type value that is either 'tcp' or 'udp'. CONTAINER2017: 無効なポートの種類 '{0}' を使用して ContainerPort 項目が指定されました。ContainerPort 項目には、整数である Include 値と、'tcp' または 'udp' のいずれかである Type 値が必要です。 - {StrBegins="CONTAINER2017: "} + {StrBegin="CONTAINER2017: "} CONTAINER2018: Invalid SDK prerelease version '{0}' - only 'rc' and 'preview' are supported. CONTAINER2018: SDK プレリリース バージョン '{0}' が無効です。'rc' と 'preview' のみがサポートされています。 - {StrBegins="CONTAINER2018: "} + {StrBegin="CONTAINER2018: "} CONTAINER2019: Invalid SDK semantic version '{0}'. CONTAINER2019: SDK セマンティック バージョン '{0}' が無効です。 - {StrBegins="CONTAINER2019: "} + {StrBegin="CONTAINER2019: "} CONTAINER2007: Invalid {0} provided: {1}. Image tags must be alphanumeric, underscore, hyphen, or period. CONTAINER2007: 無効な {0} が指定されました: {1}。イメージ タグは、英数字、アンダースコア、ハイフン、またはピリオドである必要があります。 - {StrBegins="CONTAINER2007: "} + {StrBegin="CONTAINER2007: "} CONTAINER2010: Invalid {0} provided: {1}. {0} must be a semicolon-delimited list of valid image tags. Image tags must be alphanumeric, underscore, hyphen, or period. CONTAINER2010: 無効な {0} が指定されました: {1}。{0} は、セミコロンで区切られた有効なイメージ タグのリストである必要があります。イメージ タグは、英数字、アンダースコア、ハイフン、またはピリオドである必要があります。 - {StrBegins="CONTAINER2010: "} - - - Invalid string[] TargetRuntimeIdentifiers. Either all should be 'linux-musl' or none. - string[] TargetRuntimeIdentifiers が無効です。すべて 'linux-musl' または none のいずれかにする必要があります。 - + {StrBegin="CONTAINER2010: "} CONTAINER1003: Token response had neither token nor access_token. CONTAINER1003: トークン応答にトークンも access_token もありませんでした。 - {StrBegins="CONTAINER1003: "} + {StrBegin="CONTAINER1003: "} CONTAINER4005: Item '{0}' contains items without metadata 'Value', and they will be ignored. CONTAINER4005: 項目 '{0}' にメタデータ 'Value' のない項目が含まれており、これらは無視されます。 - {StrBegins="CONTAINER4005: "} + {StrBegin="CONTAINER4005: "} CONTAINER1012: The local registry is not available, but pushing to a local registry was requested. - CONTAINER1012: ローカル レジストリが使用できませんが、ローカル レジストリへのプッシュが要求されました。 - {StrBegins="CONTAINER1012: "} + CONTAINER1012: The local registry is not available, but pushing to a local registry was requested. + {StrBegin="CONTAINER1012: "} Error while reading daemon config: {0} @@ -307,22 +225,17 @@ CONTAINER2004: Unable to download layer with descriptor '{0}' from registry '{1}' because it does not exist. CONTAINER2004: レジストリ '{1}' 内の記述子 '{0}' を持つレイヤーは存在しないため、ダウンロードできません。 - {StrBegins="CONTAINER2004: "} + {StrBegin="CONTAINER2004: "} CONTAINER2016: ContainerPort item '{0}' does not specify the port number. Please ensure the item's Include is a port number, for example '<ContainerPort Include="80" />' CONTAINER2016: ContainerPort 項目 '{0}' でポート番号が指定されていません。項目の Include がポート番号 (e '<ContainerPort Include="80" />' など) であることを確認してください - {StrBegins="CONTAINER2016: "} - - - 'mediaType' of manifests should be the same in image index. - マニフェストの 'mediaType' はイメージ インデックスで同じである必要があります。 - + {StrBegin="CONTAINER2016: "} CONTAINER1004: No RequestUri specified. CONTAINER1004: RequestUri が指定されていません。 - {StrBegins="CONTAINER1004: "} + {StrBegin="CONTAINER1004: "} '{0}' was not a valid container image name, it was normalized to '{1}' @@ -332,27 +245,17 @@ CONTAINER2011: {0} '{1}' does not exist CONTAINER2011: {0} '{1}' が存在しません - {StrBegins="CONTAINER2011: "} - - - CONTAINER1017: Unable to communicate with the registry '{0}'. - CONTAINER1017: レジストリ '{0}' と通信できません。 - {StrBegins="CONTAINER1017:" } + {StrBegin="CONTAINER2011: "} CONTAINER1013: Failed to push to the output registry: {0} CONTAINER1013: 出力レジストリにプッシュできませんでした: {0} - {StrBegins="CONTAINER1013: "} - - - CONTAINER1014: Manifest pull failed. - CONTAINER1014: マニフェストのプルに失敗しました。 - {StrBegins="CONTAINER1014: "} + {StrBegin="CONTAINER1013: "} - CONTAINER1005: Registry push failed; received status code '{0}'. - CONTAINER1005: レジストリのプッシュに失敗しました。状態コード '{0}' を受信しました。 - {StrBegins="CONTAINER1005: "} + CONTAINER1005: Registry push failed. + CONTAINER1005: レジストリのプッシュに失敗しました。 + {StrBegin="CONTAINER1005: "} Uploading config to registry at blob '{0}', @@ -399,70 +302,45 @@ タグ '{0}' を '{1}' にアップロードしました。 {1} is the registry name - - CONTAINER1015: Unable to access the repository '{0}' at tag '{1}' in the registry '{2}'. Please confirm that this name and tag are present in the registry. - CONTAINER1015: レジストリ '{2}' のタグ '{1}' にあるリポジトリ '{0}' にアクセスできません。この名前とタグがレジストリに存在することを確認してください。 - {StrBegins="CONTAINER1015: "} - CONTAINER4003: Required '{0}' items contain empty items. CONTAINER4003: 必須の '{0}' 項目に空の項目が含まれています。 - {StrBegins="CONTAINER4003: "} + {StrBegin="CONTAINER4003: "} CONTAINER4002: Required '{0}' items were not set. CONTAINER4002: 必要な '{0}' 項目が設定されませんでした。 - {StrBegins="CONTAINER4002: "} + {StrBegin="CONTAINER4002: "} CONTAINER4001: Required property '{0}' was not set or empty. CONTAINER4001: 必要なプロパティ '{0}' が設定されていなかったか、空でした。 - {StrBegins="CONTAINER4001: "} + {StrBegin="CONTAINER4001: "} CONTAINER1006: Too many retries, stopping. CONTAINER1006: 再試行回数が多すぎます。停止しています。 - {StrBegins="CONTAINER1006: "} - - - CONTAINER1016: Unable to access the repository '{0}' in the registry '{1}'. Please confirm your credentials are correct and that you have access to this repository and registry. - CONTAINER1016: レジストリ '{1}' のリポジトリ '{0}' にアクセスできません。資格情報が正しいこと、およびこのリポジトリとレジストリへのアクセス権があることを確認してください。 - {StrBegins="CONTAINER1016:" } - - - CONTAINER1018: Unable to download image from the repository '{0}'. - CONTAINER1018: リポジトリ '{0}' から画像をダウンロードできません。 - {StrBegins="CONTAINER1018:" } + {StrBegin="CONTAINER1006: "} CONTAINER2021: Unknown AppCommandInstruction '{0}'. Valid instructions are {1}. - CONTAINER2021: 不明な AppCommandInstruction '{0}'。有効な手順は {1} です。 - {StrBegins="CONTAINER2021: "} + CONTAINER2021: Unknown AppCommandInstruction '{0}'. Valid instructions are {1}. + {StrBegin="CONTAINER2021: "} CONTAINER2002: Unknown local registry type '{0}'. Valid local container registry types are {1}. - CONTAINER2002: 不明なローカル レジストリの種類 '{0}'。有効なローカル コンテナー レジストリの種類は {1} です。 - {StrBegins="CONTAINER2002: "} + CONTAINER2002: Unknown local registry type '{0}'. Valid local container registry types are {1}. + {StrBegin="CONTAINER2002: "} CONTAINER2003: The manifest for {0}:{1} from registry {2} was an unknown type: {3}. Please raise an issue at https://github.com/dotnet/sdk-container-builds/issues with this message. CONTAINER2003: レジストリ {0} からの {1}:{2} のマニフェストは不明な種類でした: {3}。このメッセージを使用して https://github.com/dotnet/sdk-container-builds/issues で問題を発生させてください。 - {StrBegins="CONTAINER2003: "} + {StrBegin="CONTAINER2003: "} CONTAINER2001: Unrecognized mediaType '{0}'. CONTAINER2001: 認識されない mediaType '{0}' です。 - {StrBegins="CONTAINER2001: "} - - - Cannot create image index for the provided 'mediaType' = '{0}'. - 指定された 'mediaType' = '{0}' のイメージ インデックスを作成できません。 - - - - Unable to create tarball for mediaType '{0}'. - mediaType '{0}' の tarball を作成できません。 - + {StrBegin="CONTAINER2001: "} CONTAINER0000: Value for unit test {0} diff --git a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.ko.xlf b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.ko.xlf index 9a04cc60f3a9..8e4107ce67e5 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.ko.xlf +++ b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.ko.xlf @@ -5,73 +5,61 @@ CONTAINER1002: Request to Amazon Elastic Container Registry failed prematurely. This is often caused when the target repository does not exist in the registry. CONTAINER1002: Amazon Elastic Container Registry에 대한 요청이 조기에 실패했습니다. 대상 리포지토리가 레지스트리에 존재하지 않을 때 종종 발생합니다. - {StrBegins="CONTAINER1002: "} + {StrBegin="CONTAINER1002: "} CONTAINER2008: Both {0} and {1} were provided, but only one or the other is allowed. CONTAINER2008: {0} 및 {1}이(가) 모두 제공되었지만 둘 중 하나만 허용됩니다. - {StrBegins="CONTAINER2008: "} + {StrBegin="CONTAINER2008: "} CONTAINER2025: ContainerAppCommandArgs are provided without specifying a ContainerAppCommand. - CONTAINER2025: ContainerAppCommandArgs는 ContainerAppCommand를 지정하지 않고 제공됩니다. - {StrBegins="CONTAINER2025: "} + CONTAINER2025: ContainerAppCommandArgs are provided without specifying a ContainerAppCommand. + {StrBegin="CONTAINER2025: "} CONTAINER2026: ContainerAppCommand and ContainerAppCommandArgs must be empty when ContainerAppCommandInstruction is '{0}'. - CONTAINER2026: ContainerAppCommandInstruction이 '{0}'인 경우 ContainerAppCommand 및 ContainerAppCommandArgs가 비어 있어야 합니다. - {StrBegins="CONTAINER2026: "} - - - local archive at '{0}' - '{0}'의 로컬 보관 - {0} is the path to the file written + CONTAINER2026: ContainerAppCommand and ContainerAppCommandArgs must be empty when ContainerAppCommandInstruction is '{0}'. + {StrBegin="CONTAINER2026: "} CONTAINER2022: The base image has an entrypoint that will be overwritten to start the application. Set ContainerAppCommandInstruction to 'Entrypoint' if this is desired. To preserve the base image entrypoint, set ContainerAppCommandInstruction to 'DefaultArgs'. - CONTAINER2022: 기본 이미지에 응용 프로그램을 시작하기 위해 덮어쓸 진입점이 있습니다. 필요한 경우 ContainerAppCommandInstruction을 'Entrypoint'로 설정합니다. 기본 이미지 진입점을 유지하려면 ContainerAppCommandInstruction을 'DefaultArgs'로 설정하세요. - {StrBegins="CONTAINER2022: "} + CONTAINER2022: The base image has an entrypoint that will be overwritten to start the application. Set ContainerAppCommandInstruction to 'Entrypoint' if this is desired. To preserve the base image entrypoint, set ContainerAppCommandInstruction to 'DefaultArgs'. + {StrBegin="CONTAINER2022: "} CONTAINER2009: Could not parse {0}: {1} CONTAINER2009: {0}을(를) 구문 분석할 수 없습니다: {1} - {StrBegins="CONTAINER2009: "} + {StrBegin="CONTAINER2009: "} CONTAINER2020: {0} does not specify a registry and will be pulled from Docker Hub. Please prefix the name with the image registry, for example: '{1}/<image>'. - CONTAINER2020: {0}은(는) 레지스트리를 지정하지 않으며 Docker Hub에서 끌어오게 됩니다. 이름 앞에 이미지 레지스트리를 추가하세요(예: '{1}/<image>'). - {StrBegins="CONTAINER2020: "} + CONTAINER2020: {0} does not specify a registry and will be pulled from Docker Hub. Please prefix the name with the image registry, for example: '{1}/<image>'. + {StrBegin="CONTAINER2020: "} CONTAINER2013: {0} had spaces in it, replacing with dashes. CONTAINER2013: {0}에 공백이 있었고 대시로 대체되었습니다. - {StrBegins="CONTAINER2013: "} + {StrBegin="CONTAINER2013: "} CONTAINER1011: Couldn't find matching base image for {0} that matches RuntimeIdentifier {1}. CONTAINER1011: RuntimeIdentifier {1}과(와) 일치하는 {0}에 대해 일치하는 기본 이미지를 찾을 수 없습니다. - {StrBegins="CONTAINER1011: "} + {StrBegin="CONTAINER1011: "} CONTAINER1001: Failed to upload blob using {0}; received status code '{1}'. CONTAINER1001: {0}을(를) 사용하여 Blob을 업로드하지 못했습니다. '{1}' 상태 코드를 수신했습니다. - {StrBegins="CONTAINER1001: "} - - - Building image index '{0}' on top of manifests {1}. - 매니페스트 {1} 위에 이미지 인덱스를 '{0}'. - - {0} is the name of the image index and its tag, {1} is the list of manifests digests - + {StrBegin="CONTAINER1001: "} - Pushed image '{0}' to {1}. - '{0}' 이미지를 {1}에 푸시했습니다. + Pushed container '{0}' to Docker daemon. + Docker 디먼에 '{0}' 컨테이너를 푸시했습니다. - Pushed image '{0}' to registry '{1}'. - '{0}' 이미지를 '{1}' 레지스트리에 푸시했습니다. + Pushed container '{0}' to registry '{1}'. + '{1}' 레지스트리에 '{0}' 컨테이너를 푸시했습니다. @@ -79,220 +67,150 @@ 기본 이미지 '{2}' 위에 '{1}' 태그가 있는 '{0}' 이미지를 빌드하는 중입니다. - - Building image '{0}' for runtime identifier '{1}' on top of base image '{2}'. - 기본 이미지 '{2}' 위에 런타임 식별자 '{1}'에 대한 '{0}' 이미지를 빌드하는 중입니다. - - CONTAINER1007: Could not deserialize token from JSON. CONTAINER1007: JSON에서 토큰을 역직렬화할 수 없습니다. - {StrBegins="CONTAINER1007: "} + {StrBegin="CONTAINER1007: "} CONTAINER2012: Could not recognize registry '{0}'. CONTAINER2012: 레지스트리 '{0}'을(를) 인식할 수 없습니다. - {StrBegins="CONTAINER2012: "} - - - local registry via '{0}' - '{0}'을(를) 통한 로컬 레지스트리 - {0} is the command used + {StrBegin="CONTAINER2012: "} CONTAINER3002: Failed to get docker info({0})\n{1}\n{2} CONTAINER3002: 도커 정보({0})를 가져오지 못했습니다.\n{1}\n{2} - {StrBegins="CONTAINER3002: "} + {StrBegin="CONTAINER3002: "} CONTAINER3002: Failed to get docker info: {0} CONTAINER3002: 도커 정보를 가져오지 못했습니다: {0} - {StrBegins="CONTAINER3002: "} + {StrBegin="CONTAINER3002: "} - + CONTAINER3001: Failed creating {0} process. - CONTAINER3001: {0} 프로세스 생성에 실패했습니다. + CONTAINER3001: Failed creating {0} process. CONTAINER3001: {0} is the name of the command we failed to run, usually 'docker' or 'podman'. CONTAINER4006: Property '{0}' is empty or contains whitespace and will be ignored. CONTAINER4006: 속성 '{0}'이(가) 비어 있거나 공백이 포함되어 있으므로 무시됩니다. - {StrBegins="CONTAINER4006: "} + {StrBegin="CONTAINER4006: "} CONTAINER4004: Items '{0}' contain empty item(s) which will be ignored. CONTAINER4004: 항목 '{0}'에는 무시될 빈 항목이 포함되어 있습니다. - {StrBegins="CONTAINER4004: "} + {StrBegin="CONTAINER4004: "} CONTAINER2023: A ContainerEntrypoint and ContainerAppCommandArgs are provided. ContainerAppInstruction must be set to configure how the application is started. Valid instructions are {0}. - CONTAINER2023: ContainerEntrypoint 및 ContainerAppCommandArgs가 제공되었습니다. 애플리케이션 시작 방법을 구성하려면 ContainerAppInstruction을 설정해야 합니다. 올바른 지침은 {0}입니다. - {StrBegins="CONTAINER2023: "} + CONTAINER2023: A ContainerEntrypoint and ContainerAppCommandArgs are provided. ContainerAppInstruction must be set to configure how the application is started. Valid instructions are {0}. + {StrBegin="CONTAINER2023: "} CONTAINER2027: A ContainerEntrypoint is provided. ContainerAppInstruction must be set to configure how the application is started. Valid instructions are {0}. - CONTAINER2027: ContainerEntrypoint가 제공됩니다. 애플리케이션 시작 방법을 구성하려면 ContainerAppInstruction을 설정해야 합니다. 유효한 지침은 {0}입니다. - {StrBegins="CONTAINER2027: "} + CONTAINER2027: A ContainerEntrypoint is provided. ContainerAppInstruction must be set to configure how the application is started. Valid instructions are {0}. + {StrBegin="CONTAINER2027: "} CONTAINER2024: ContainerEntrypointArgs are provided without specifying a ContainerEntrypoint. - CONTAINER2024: ContainerEntrypointArgs는 ContainerEntrypoint를 지정하지 않고 제공됩니다. - {StrBegins="CONTAINER2024: "} + CONTAINER2024: ContainerEntrypointArgs are provided without specifying a ContainerEntrypoint. + {StrBegin="CONTAINER2024: "} CONTAINER2029: ContainerEntrypointArgsSet are provided. Change to use ContainerAppCommandArgs for arguments that must always be set, or ContainerDefaultArgs for arguments that can be overridden when the container is created. - CONTAINER2029: ContainerEntrypointArgsSet이 제공되었습니다. 항상 설정해야 하는 인수에 ContainerAppCommandArgs를 사용하도록 변경하거나 컨테이너를 만들 때 재정의할 수 있는 인수에 ContainerDefaultArgs를 사용하도록 변경합니다. - {StrBegins="CONTAINER2029: "} + CONTAINER2029: ContainerEntrypointArgsSet are provided. Change to use ContainerAppCommandArgs for arguments that must always be set, or ContainerDefaultArgs for arguments that can be overridden when the container is created. + {StrBegin="CONTAINER2029: "} CONTAINER2028: ContainerEntrypoint can not be combined with ContainerAppCommandInstruction '{0}'. - CONTAINER2028: ContainerEntrypoint는 '{0}' ContainerAppCommandInstruction과 함께 사용할 수 없습니다. - {StrBegins="CONTAINER2028: "} + CONTAINER2028: ContainerEntrypoint can not be combined with ContainerAppCommandInstruction '{0}'. + {StrBegin="CONTAINER2028: "} CONTAINER1008: Failed retrieving credentials for "{0}": {1} CONTAINER1008: "{0}"에 대한 자격 증명 검색 실패: {1} - {StrBegins="CONTAINER1008: "} - - - CONTAINER2030: GenerateLabels was disabled but GenerateDigestLabel was enabled - no digest label will be created. - CONTAINER2030: GenerateLabels를 사용하지 않도록 설정했지만 GenerateDigestLabel을 사용하도록 설정했습니다. 다이제스트 레이블이 만들어지지 않습니다. - {StrBegins="CONTAINER2030: "} + {StrBegin="CONTAINER1008: "} No host object detected. 호스트 개체가 검색되지 않았습니다. - - Cannot create image index because at least one of the provided images' config is missing 'architecture'. - 제공된 이미지 구성 중 하나 이상에 'architecture'가 없으므로 이미지 인덱스를 만들 수 없습니다. - - - - Cannot create image index because at least one of the provided images' config is missing 'os'. - 제공된 이미지 구성 중 하나 이상에 'os'가 없으므로 이미지 인덱스를 만들 수 없습니다. - - - - Pushed image index '{0}' to registry '{1}'. - '{0}' 이미지 인덱스를 '{1}' 레지스트리에 푸시했습니다. - - - - Image index creation for Podman is not supported. - Podman에 대한 이미지 인덱스 만들기는 지원되지 않습니다. - - CONTAINER1009: Failed to load image from local registry. stdout: {0} - CONTAINER1009: 로컬 레지스트리에서 이미지를 로드하지 못했습니다. stdout: {0} - {StrBegins="CONTAINER1009: "} - - - CONTAINER1020: Failed to load image because containerd image store is not enabled for Docker. Tip: You can enable it by checking 'Use containerd for pulling and storing images' in Docker Desktop settings. - CONTAINER1020: 컨테이너화된 이미지 저장소가 Docker에 대해 사용하도록 설정되어 있지 않으므로 이미지를 로드하지 못했습니다. 팁: Docker Desktop 설정에서 '이미지를 풀하고 저장하는 데 컨테이너 사용'을 선택하여 사용하도록 설정할 수 있습니다. - {StrBegin="CONTAINER1020: "} + CONTAINER1009: 로컬 Docker 디먼에 이미지를 로드하지 못했습니다. 표준 출력: {0} + {StrBegin="CONTAINER1009: "} CONTAINER1010: Pulling images from local registry is not supported. - CONTAINER1010: 로컬 레지스트리에서 이미지 끌어오기가 지원되지 않습니다. - {StrBegins="CONTAINER1010: "} + CONTAINER1010: 로컬 Docker 디먼에서 이미지 끌어오기가 지원되지 않습니다. + {StrBegin="CONTAINER1010: "} - - Cannot create image index because no images were provided. - 이미지가 제공되지 않았으므로 이미지 인덱스를 만들 수 없습니다. - - - - CONTAINER2031: The container image format '{0}' is not supported. Supported formats are '{1}'. - CONTAINER2031: 컨테이너 이미지 형식 '{0}' 지원되지 않습니다. 지원되는 형식은 '{1}'. - {StrBegins="CONTAINER2031: "} + + CONTAINER2014: Invalid {0}: {1}. + CONTAINER2014: 잘못된 {0}: {1}. + {StrBegin="CONTAINER2014: "} CONTAINER2015: {0}: '{1}' was not a valid Environment Variable. Ignoring. CONTAINER2015: {0}: '{1}'은(는) 유효한 환경 변수가 아닙니다. 무시 중. - {StrBegins="CONTAINER2015: "} + {StrBegin="CONTAINER2015: "} - - Cannot create image index because at least one of the provided images' config is invalid. - 제공된 이미지의 구성 중 하나 이상이 잘못되었으므로 이미지 인덱스를 만들 수 없습니다. - - - - Cannot create image index because at least one of the provided images' manifest is invalid. - 제공된 이미지의 매니페스트 중 하나 이상이 잘못되었으므로 이미지 인덱스를 만들 수 없습니다. - - - - Cannot create image index because provided images are invalid. Items must have 'Config', 'Manifest', 'ManifestMediaType' and 'ManifestDigest' metadata. - 제공된 이미지가 잘못되었으므로 이미지 인덱스를 만들 수 없습니다. 항목에는 'Config', 'Manifest', 'ManifestMediaType' 및 'ManifestDigest' 메타데이터가 있어야 합니다. - - - - CONTAINER2005: The inferred image name '{0}' contains entirely invalid characters. The valid characters for an image name are alphanumeric characters, -, /, or _, and the image name must start with an alphanumeric character. - CONTAINER2005: 유추된 이미지 이름 '{0}'에 완전히 잘못된 문자가 포함되어 있습니다. 이미지 이름의 유효한 문자는 영숫자, -, /또는 _이며 이미지 이름은 영숫자 문자로 시작해야 합니다. - {StrBegins="CONTAINER2005: "} - - - CONTAINER2005: The first character of the image name '{0}' must be a lowercase letter or a digit and all characters in the name must be an alphanumeric character, -, /, or _. - CONTAINER2005: 이미지 이름 '{0}'의 첫 번째 문자는 소문자 또는 숫자여야 하며 이름의 모든 문자는 영숫자, -, /또는 _여야 합니다. - {StrBegins="CONTAINER2005: "} + + CONTAINER2005: The first character of the image name must be a lowercase letter or a digit. + CONTAINER2005: 이미지 이름의 첫 글자는 소문자 또는 숫자여야 합니다. + {StrBegin="CONTAINER2005: "} CONTAINER2017: A ContainerPort item was provided with an invalid port number '{0}'. ContainerPort items must have an Include value that is an integer, and a Type value that is either 'tcp' or 'udp'. CONTAINER2017: ContainerPort 항목에 잘못된 포트 번호 '{0}'이(가) 제공되었습니다. ContainerPort 항목에는 정수인 Include 값과 'tcp' 또는 'udp'인 Type 값이 있어야 합니다. - {StrBegins="CONTAINER2017: "} + {StrBegin="CONTAINER2017: "} CONTAINER2017: A ContainerPort item was provided with an invalid port number '{0}' and an invalid port type '{1}'. ContainerPort items must have an Include value that is an integer, and a Type value that is either 'tcp' or 'udp'. CONTAINER2017: 잘못된 포트 번호 '{0}' 및 잘못된 포트 유형 '{1}'과(와) 함께 ContainerPort 항목이 제공되었습니다. ContainerPort 항목에는 정수인 Include 값과 'tcp' 또는 'udp'인 Type 값이 있어야 합니다. - {StrBegins="CONTAINER2017: "} + {StrBegin="CONTAINER2017: "} CONTAINER2017: A ContainerPort item was provided with an invalid port type '{0}'. ContainerPort items must have an Include value that is an integer, and a Type value that is either 'tcp' or 'udp'. CONTAINER2017: 잘못된 포트 유형 '{0}'과(와) 함께 ContainerPort 항목이 제공되었습니다. ContainerPort 항목에는 정수인 Include 값과 'tcp' 또는 'udp'인 Type 값이 있어야 합니다. - {StrBegins="CONTAINER2017: "} + {StrBegin="CONTAINER2017: "} CONTAINER2018: Invalid SDK prerelease version '{0}' - only 'rc' and 'preview' are supported. CONTAINER2018: 잘못된 SDK 시험판 버전 '{0}' - 'rc' 및 'preview'만 지원됩니다. - {StrBegins="CONTAINER2018: "} + {StrBegin="CONTAINER2018: "} CONTAINER2019: Invalid SDK semantic version '{0}'. CONTAINER2019: 잘못된 SDK 의미 체계 버전 '{0}'입니다. - {StrBegins="CONTAINER2019: "} + {StrBegin="CONTAINER2019: "} CONTAINER2007: Invalid {0} provided: {1}. Image tags must be alphanumeric, underscore, hyphen, or period. CONTAINER2007: 잘못된 {0}이(가) 제공됨: {1}. 이미지 태그는 영숫자, 밑줄, 하이픈 또는 마침표여야 합니다. - {StrBegins="CONTAINER2007: "} + {StrBegin="CONTAINER2007: "} CONTAINER2010: Invalid {0} provided: {1}. {0} must be a semicolon-delimited list of valid image tags. Image tags must be alphanumeric, underscore, hyphen, or period. CONTAINER2010: 잘못된 {0}이(가) 제공됨: {1}. {0}은(는) 세미콜론으로 구분된 유효한 이미지 태그 목록이어야 합니다. 이미지 태그는 영숫자, 밑줄, 하이픈 또는 마침표여야 합니다. - {StrBegins="CONTAINER2010: "} - - - Invalid string[] TargetRuntimeIdentifiers. Either all should be 'linux-musl' or none. - 잘못된 string[] TargetRuntimeIdentifiers입니다. 모두 'linux-readl' 또는 none이어야 합니다. - + {StrBegin="CONTAINER2010: "} CONTAINER1003: Token response had neither token nor access_token. CONTAINER1003: 토큰 응답에 토큰이나 access_token이 없습니다. - {StrBegins="CONTAINER1003: "} + {StrBegin="CONTAINER1003: "} CONTAINER4005: Item '{0}' contains items without metadata 'Value', and they will be ignored. CONTAINER4005: 항목 '{0}'에는 메타데이터 '값'이 없는 항목이 포함되어 있으며 무시됩니다. - {StrBegins="CONTAINER4005: "} + {StrBegin="CONTAINER4005: "} CONTAINER1012: The local registry is not available, but pushing to a local registry was requested. - CONTAINER1012: 로컬 레지스트리를 사용할 수 없는데 로컬 레지스트리로 푸시가 요청되었습니다. - {StrBegins="CONTAINER1012: "} + CONTAINER1012: The local registry is not available, but pushing to a local registry was requested. + {StrBegin="CONTAINER1012: "} Error while reading daemon config: {0} @@ -307,22 +225,17 @@ CONTAINER2004: Unable to download layer with descriptor '{0}' from registry '{1}' because it does not exist. CONTAINER2004: '{1}' 레지스트리에서 설명자가 '{0}'인 레이어가 존재하지 않기 때문에 다운로드할 수 없습니다. - {StrBegins="CONTAINER2004: "} + {StrBegin="CONTAINER2004: "} CONTAINER2016: ContainerPort item '{0}' does not specify the port number. Please ensure the item's Include is a port number, for example '<ContainerPort Include="80" />' CONTAINER2016: ContainerPort 항목 '{0}'이(가) 포트 번호를 지정하지 않습니다. 항목의 포함이 포트 번호인지 확인하세요(예: '<ContainerPort Include="80" />'). - {StrBegins="CONTAINER2016: "} - - - 'mediaType' of manifests should be the same in image index. - 매니페스트의 'mediaType'은 이미지 인덱스에서 같아야 합니다. - + {StrBegin="CONTAINER2016: "} CONTAINER1004: No RequestUri specified. CONTAINER1004: RequestUri가 지정되지 않았습니다. - {StrBegins="CONTAINER1004: "} + {StrBegin="CONTAINER1004: "} '{0}' was not a valid container image name, it was normalized to '{1}' @@ -332,27 +245,17 @@ CONTAINER2011: {0} '{1}' does not exist CONTAINER2011: {0} '{1}'이(가) 존재하지 않습니다. - {StrBegins="CONTAINER2011: "} - - - CONTAINER1017: Unable to communicate with the registry '{0}'. - CONTAINER1017: '{0}' 레지스트리와 통신할 수 없습니다. - {StrBegins="CONTAINER1017:" } + {StrBegin="CONTAINER2011: "} CONTAINER1013: Failed to push to the output registry: {0} CONTAINER1013: 출력 레지스트리로 푸시하지 못했습니다: {0} - {StrBegins="CONTAINER1013: "} - - - CONTAINER1014: Manifest pull failed. - CONTAINER1014: 매니페스트를 끌어오지 못했습니다. - {StrBegins="CONTAINER1014: "} + {StrBegin="CONTAINER1013: "} - CONTAINER1005: Registry push failed; received status code '{0}'. - CONTAINER1005: 레지스트리 푸시에 실패했습니다. 상태 코드 '{0}'을(를) 받았습니다. - {StrBegins="CONTAINER1005: "} + CONTAINER1005: Registry push failed. + CONTAINER1005: 레지스트리를 푸시하지 못했습니다. + {StrBegin="CONTAINER1005: "} Uploading config to registry at blob '{0}', @@ -399,70 +302,45 @@ '{1}'에 '{0}' 태그를 업로드했습니다. {1} is the registry name - - CONTAINER1015: Unable to access the repository '{0}' at tag '{1}' in the registry '{2}'. Please confirm that this name and tag are present in the registry. - CONTAINER1015: '{2}' 레지스트리의 '{1}' 태그에서 '{0}' 리포지토리에 액세스할 수 없습니다. 이 이름 및 태그가 레지스트리에 있는지 확인하세요. - {StrBegins="CONTAINER1015: "} - CONTAINER4003: Required '{0}' items contain empty items. CONTAINER4003: 필수 '{0}' 항목에 빈 항목이 있습니다. - {StrBegins="CONTAINER4003: "} + {StrBegin="CONTAINER4003: "} CONTAINER4002: Required '{0}' items were not set. CONTAINER4002: 필수 '{0}' 항목이 설정되지 않았습니다. - {StrBegins="CONTAINER4002: "} + {StrBegin="CONTAINER4002: "} CONTAINER4001: Required property '{0}' was not set or empty. CONTAINER4001: 필수 속성 '{0}'이(가) 설정되지 않았거나 비어 있습니다. - {StrBegins="CONTAINER4001: "} + {StrBegin="CONTAINER4001: "} CONTAINER1006: Too many retries, stopping. CONTAINER1006: 다시 시도가 너무 많아 중지 중입니다. - {StrBegins="CONTAINER1006: "} - - - CONTAINER1016: Unable to access the repository '{0}' in the registry '{1}'. Please confirm your credentials are correct and that you have access to this repository and registry. - CONTAINER1016: '{1}' 레지스트리의 '{0}' 리포지토리에 액세스할 수 없습니다. 자격 증명이 올바르고 이 리포지토리 및 레지스트리에 액세스할 수 있는지 확인하세요. - {StrBegins="CONTAINER1016:" } - - - CONTAINER1018: Unable to download image from the repository '{0}'. - CONTAINER1018: 리포지토리 '{0}'에서 이미지를 다운로드할 수 없습니다. - {StrBegins="CONTAINER1018:" } + {StrBegin="CONTAINER1006: "} CONTAINER2021: Unknown AppCommandInstruction '{0}'. Valid instructions are {1}. - CONTAINER2021: 알 수 없는 AppCommandInstruction '{0}'. 올바른 지침은 {1}입니다. - {StrBegins="CONTAINER2021: "} + CONTAINER2021: Unknown AppCommandInstruction '{0}'. Valid instructions are {1}. + {StrBegin="CONTAINER2021: "} CONTAINER2002: Unknown local registry type '{0}'. Valid local container registry types are {1}. - CONTAINER2002: 알 수 없는 로컬 레지스트리 유형 '{0}'. 유효한 로컬 컨테이너 레지스트리 유형은 {1}입니다. - {StrBegins="CONTAINER2002: "} + CONTAINER2002: Unknown local registry type '{0}'. Valid local container registry types are {1}. + {StrBegin="CONTAINER2002: "} CONTAINER2003: The manifest for {0}:{1} from registry {2} was an unknown type: {3}. Please raise an issue at https://github.com/dotnet/sdk-container-builds/issues with this message. CONTAINER2003: 레지스트리 {2}의 {0}:{1}에 대한 매니페스트가 알 수 없는 유형입니다: {3}. https://github.com/dotnet/sdk-container-builds/issues에서 이 메시지와 함께 문제를 제기하세요. - {StrBegins="CONTAINER2003: "} + {StrBegin="CONTAINER2003: "} CONTAINER2001: Unrecognized mediaType '{0}'. CONTAINER2001: 미디어 유형 '{0}'을(를) 인식할 수 없습니다. - {StrBegins="CONTAINER2001: "} - - - Cannot create image index for the provided 'mediaType' = '{0}'. - 제공된 'mediaType' = '{0}' 이미지 인덱스를 만들 수 없습니다. - - - - Unable to create tarball for mediaType '{0}'. - mediaType '{0}'에 대한 tarball을 만들 수 없습니다. - + {StrBegin="CONTAINER2001: "} CONTAINER0000: Value for unit test {0} diff --git a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.pl.xlf b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.pl.xlf index 801b6190de72..3900d4350a70 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.pl.xlf +++ b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.pl.xlf @@ -5,73 +5,61 @@ CONTAINER1002: Request to Amazon Elastic Container Registry failed prematurely. This is often caused when the target repository does not exist in the registry. CONTAINER1002: żądanie do usługi Amazon Elastic Container Registry przedwcześnie zakończyło się niepowodzeniem. Jest to często spowodowane tym, że repozytorium docelowe nie istnieje w rejestrze. - {StrBegins="CONTAINER1002: "} + {StrBegin="CONTAINER1002: "} CONTAINER2008: Both {0} and {1} were provided, but only one or the other is allowed. CONTAINER2008: podano {0} i {1}, ale dozwolona jest tylko jedna opcja lub druga. - {StrBegins="CONTAINER2008: "} + {StrBegin="CONTAINER2008: "} CONTAINER2025: ContainerAppCommandArgs are provided without specifying a ContainerAppCommand. - CONTAINER2025: Argumenty ContainerAppCommandArgs są dostarczane bez określania polecenia ContainerAppCommand. - {StrBegins="CONTAINER2025: "} + CONTAINER2025: ContainerAppCommandArgs are provided without specifying a ContainerAppCommand. + {StrBegin="CONTAINER2025: "} CONTAINER2026: ContainerAppCommand and ContainerAppCommandArgs must be empty when ContainerAppCommandInstruction is '{0}'. - CONTAINER2026: ContainerAppCommand i ContainerAppCommandArgs muszą być puste, gdy element ContainerAppCommandInstruction ma wartość „{0}”. - {StrBegins="CONTAINER2026: "} - - - local archive at '{0}' - archiwum lokalne w lokalizacji „{0}” - {0} is the path to the file written + CONTAINER2026: ContainerAppCommand and ContainerAppCommandArgs must be empty when ContainerAppCommandInstruction is '{0}'. + {StrBegin="CONTAINER2026: "} CONTAINER2022: The base image has an entrypoint that will be overwritten to start the application. Set ContainerAppCommandInstruction to 'Entrypoint' if this is desired. To preserve the base image entrypoint, set ContainerAppCommandInstruction to 'DefaultArgs'. - CONTAINER2022: Obraz podstawowy ma punkt wejścia, który zostanie zastąpiony w celu uruchomienia aplikacji. W razie potrzeby ustaw właściwość ContainerAppCommandInstruction na wartość „Entrypoint”. Aby zachować podstawowy punkt wejścia obrazu, ustaw właściwość ContainerAppCommandInstruction na wartość „DefaultArgs”. - {StrBegins="CONTAINER2022: "} + CONTAINER2022: The base image has an entrypoint that will be overwritten to start the application. Set ContainerAppCommandInstruction to 'Entrypoint' if this is desired. To preserve the base image entrypoint, set ContainerAppCommandInstruction to 'DefaultArgs'. + {StrBegin="CONTAINER2022: "} CONTAINER2009: Could not parse {0}: {1} CONTAINER2009: nie można przeanalizować {0}: {1} - {StrBegins="CONTAINER2009: "} + {StrBegin="CONTAINER2009: "} CONTAINER2020: {0} does not specify a registry and will be pulled from Docker Hub. Please prefix the name with the image registry, for example: '{1}/<image>'. - CONTAINER2020: {0} nie określa rejestru i zostanie pobrany z usługi Docker Hub. Poprzedź nazwę rejestrem obrazów, na przykład: „{1}/<image>”. - {StrBegins="CONTAINER2020: "} + CONTAINER2020: {0} does not specify a registry and will be pulled from Docker Hub. Please prefix the name with the image registry, for example: '{1}/<image>'. + {StrBegin="CONTAINER2020: "} CONTAINER2013: {0} had spaces in it, replacing with dashes. CONTAINER2013: element {0} zawierał spacje, zastępując je kreskami. - {StrBegins="CONTAINER2013: "} + {StrBegin="CONTAINER2013: "} CONTAINER1011: Couldn't find matching base image for {0} that matches RuntimeIdentifier {1}. CONTAINER1011: nie można odnaleźć pasującego obrazu podstawowego dla {0} zgodnego z elementem RuntimeIdentifier {1}. - {StrBegins="CONTAINER1011: "} + {StrBegin="CONTAINER1011: "} CONTAINER1001: Failed to upload blob using {0}; received status code '{1}'. CONTAINER1001: nie można przekazać obiektu blob przy użyciu {0}; odebrano kod stanu „{1}”. - {StrBegins="CONTAINER1001: "} - - - Building image index '{0}' on top of manifests {1}. - Tworzenie '{0}' indeksu obrazu na górze manifestów {1}. - - {0} is the name of the image index and its tag, {1} is the list of manifests digests - + {StrBegin="CONTAINER1001: "} - Pushed image '{0}' to {1}. - Wypchnięty obraz „{0}” do „{1}”. + Pushed container '{0}' to Docker daemon. + Wypchnięto kontener „{0}” do demona platformy Docker. - Pushed image '{0}' to registry '{1}'. - Wypchnięty obraz „{0}” do rejestru „{1}”. + Pushed container '{0}' to registry '{1}'. + Wypchnęliśmy 'kontener „{0}” do rejestru „{1}”. @@ -79,220 +67,150 @@ Tworzenie obrazu „{0}” z tagami „{1}” nad obrazem podstawowym „{2}”. - - Building image '{0}' for runtime identifier '{1}' on top of base image '{2}'. - Kompilowanie obrazu „{0}” dla identyfikatora środowiska uruchomieniowego „{1}” na podstawie obrazu podstawowego „{2}”. - - CONTAINER1007: Could not deserialize token from JSON. CONTAINER1007: nie można zdeserializować tokenu z pliku JSON. - {StrBegins="CONTAINER1007: "} + {StrBegin="CONTAINER1007: "} CONTAINER2012: Could not recognize registry '{0}'. CONTAINER2012: nie można rozpoznać rejestru „{0}”. - {StrBegins="CONTAINER2012: "} - - - local registry via '{0}' - rejestr lokalny za pośrednictwem „{0}” - {0} is the command used + {StrBegin="CONTAINER2012: "} CONTAINER3002: Failed to get docker info({0})\n{1}\n{2} CONTAINER3002: nie można pobrać informacji o platformie Docker({0})\n{1}\n{2} - {StrBegins="CONTAINER3002: "} + {StrBegin="CONTAINER3002: "} CONTAINER3002: Failed to get docker info: {0} CONTAINER3002: nie można pobrać informacji o platformie Docker: {0} - {StrBegins="CONTAINER3002: "} + {StrBegin="CONTAINER3002: "} - + CONTAINER3001: Failed creating {0} process. - CONTAINER3001: Nie można utworzyć procesu {0}. + CONTAINER3001: Failed creating {0} process. CONTAINER3001: {0} is the name of the command we failed to run, usually 'docker' or 'podman'. CONTAINER4006: Property '{0}' is empty or contains whitespace and will be ignored. CONTAINER4006: właściwość „{0}” jest pusta lub zawiera białe znaki i zostanie zignorowana. - {StrBegins="CONTAINER4006: "} + {StrBegin="CONTAINER4006: "} CONTAINER4004: Items '{0}' contain empty item(s) which will be ignored. CONTAINER4004: elementy „{0}” zawierają puste elementy, które zostaną zignorowane. - {StrBegins="CONTAINER4004: "} + {StrBegin="CONTAINER4004: "} CONTAINER2023: A ContainerEntrypoint and ContainerAppCommandArgs are provided. ContainerAppInstruction must be set to configure how the application is started. Valid instructions are {0}. - CONTAINER2023: Przesłano elementy ContainerEntrypoint i ContainerAppCommandArgs. Aby skonfigurować sposób uruchamiania aplikacji, należy ustawić element ContainerAppInstruction. Prawidłowe instrukcje: {0}. - {StrBegins="CONTAINER2023: "} + CONTAINER2023: A ContainerEntrypoint and ContainerAppCommandArgs are provided. ContainerAppInstruction must be set to configure how the application is started. Valid instructions are {0}. + {StrBegin="CONTAINER2023: "} CONTAINER2027: A ContainerEntrypoint is provided. ContainerAppInstruction must be set to configure how the application is started. Valid instructions are {0}. - CONTAINER2027: Przesłano element ContainerEntrypoint. Aby skonfigurować sposób uruchamiania aplikacji, należy ustawić element ContainerAppInstruction. Prawidłowe instrukcje to {0}. - {StrBegins="CONTAINER2027: "} + CONTAINER2027: A ContainerEntrypoint is provided. ContainerAppInstruction must be set to configure how the application is started. Valid instructions are {0}. + {StrBegin="CONTAINER2027: "} CONTAINER2024: ContainerEntrypointArgs are provided without specifying a ContainerEntrypoint. - CONTAINER2024: Argumenty ContainerEntrypointArgs są przesyłane bez określania elementu ContainerEntrypoint. - {StrBegins="CONTAINER2024: "} + CONTAINER2024: ContainerEntrypointArgs are provided without specifying a ContainerEntrypoint. + {StrBegin="CONTAINER2024: "} CONTAINER2029: ContainerEntrypointArgsSet are provided. Change to use ContainerAppCommandArgs for arguments that must always be set, or ContainerDefaultArgs for arguments that can be overridden when the container is created. - CONTAINER2029: Przesłano elementy ContainerEntrypointArgsSet. Zmień, aby używać argumentów ContainerAppCommandArgs dla argumentów, które muszą być zawsze ustawione, lub argumentów ContainerDefaultArgs dla argumentów, które mogą zostać zastąpione podczas tworzenia kontenera. - {StrBegins="CONTAINER2029: "} + CONTAINER2029: ContainerEntrypointArgsSet are provided. Change to use ContainerAppCommandArgs for arguments that must always be set, or ContainerDefaultArgs for arguments that can be overridden when the container is created. + {StrBegin="CONTAINER2029: "} CONTAINER2028: ContainerEntrypoint can not be combined with ContainerAppCommandInstruction '{0}'. - CONTAINER2028: Elementu ContainerEntrypoint nie można łączyć z elementem ContainerAppCommandInstruction „{0}”. - {StrBegins="CONTAINER2028: "} + CONTAINER2028: ContainerEntrypoint can not be combined with ContainerAppCommandInstruction '{0}'. + {StrBegin="CONTAINER2028: "} CONTAINER1008: Failed retrieving credentials for "{0}": {1} CONTAINER1008: nie można pobrać poświadczeń dla „{0}”: {1} - {StrBegins="CONTAINER1008: "} - - - CONTAINER2030: GenerateLabels was disabled but GenerateDigestLabel was enabled - no digest label will be created. - CONTAINER2030: Funkcja GenerateLabels była wyłączona, ale funkcja GenerateDigestLabel była włączona — etykieta skrótu nie zostanie utworzona. - {StrBegins="CONTAINER2030: "} + {StrBegin="CONTAINER1008: "} No host object detected. Nie wykryto obiektu hosta. - - Cannot create image index because at least one of the provided images' config is missing 'architecture'. - Nie można utworzyć indeksu obrazu, ponieważ w co najmniej jednej konfiguracji dostarczonego obrazu brakuje elementu "architecture". - - - - Cannot create image index because at least one of the provided images' config is missing 'os'. - Nie można utworzyć indeksu obrazu, ponieważ w co najmniej jednej konfiguracji dostarczonego obrazu brakuje elementu "os". - - - - Pushed image index '{0}' to registry '{1}'. - Wypchnięto indeks obrazu „{0}” do rejestru „{1}”. - - - - Image index creation for Podman is not supported. - Tworzenie indeksu obrazu dla podmanu nie jest obsługiwane. - - CONTAINER1009: Failed to load image from local registry. stdout: {0} - CONTAINER1009: Nie można załadować obrazu z rejestru lokalnego. stdout: {0} - {StrBegins="CONTAINER1009: "} - - - CONTAINER1020: Failed to load image because containerd image store is not enabled for Docker. Tip: You can enable it by checking 'Use containerd for pulling and storing images' in Docker Desktop settings. - CONTAINER1020: nie można załadować obrazu, ponieważ kontenerowy magazyn obrazów nie jest włączony dla platformy Docker. Porada: możesz ją włączyć, zaznaczając pozycję "Użyj kontenera do ściągania i przechowywania obrazów" w ustawieniach programu Docker Desktop. - {StrBegin="CONTAINER1020: "} + CONTAINER1009: nie można załadować obrazu do lokalnego demona platformy Docker. stdout: {0} + {StrBegin="CONTAINER1009: "} CONTAINER1010: Pulling images from local registry is not supported. - CONTAINER1010: Ściąganie obrazów z rejestru lokalnego nie jest obsługiwane. - {StrBegins="CONTAINER1010: "} + CONTAINER1010: ściąganie obrazów z lokalnego demona platformy Docker nie jest obsługiwane. + {StrBegin="CONTAINER1010: "} - - Cannot create image index because no images were provided. - Nie można utworzyć indeksu obrazu, ponieważ nie podano obrazów. - - - - CONTAINER2031: The container image format '{0}' is not supported. Supported formats are '{1}'. - CONTAINER2031: '{0}' formatu obrazu kontenera nie jest obsługiwany. Obsługiwane formaty są '{1}'. - {StrBegins="CONTAINER2031: "} + + CONTAINER2014: Invalid {0}: {1}. + CONTAINER2014: Nieprawidłowy element {0}: {1}. + {StrBegin="CONTAINER2014: "} CONTAINER2015: {0}: '{1}' was not a valid Environment Variable. Ignoring. CONTAINER2015: {0}: „{1}” nie jest prawidłową zmienną środowiskową. Ignorowanie. - {StrBegins="CONTAINER2015: "} + {StrBegin="CONTAINER2015: "} - - Cannot create image index because at least one of the provided images' config is invalid. - Nie można utworzyć indeksu obrazu, ponieważ co najmniej jedna z podanych konfiguracji obrazów jest nieprawidłowa. - - - - Cannot create image index because at least one of the provided images' manifest is invalid. - Nie można utworzyć indeksu obrazu, ponieważ co najmniej jeden z podanych manifestów obrazów jest nieprawidłowy. - - - - Cannot create image index because provided images are invalid. Items must have 'Config', 'Manifest', 'ManifestMediaType' and 'ManifestDigest' metadata. - Nie można utworzyć indeksu obrazu, ponieważ podane obrazy są nieprawidłowe. Elementy muszą mieć metadane "Config", "Manifest", "ManifestMediaType" i "ManifestDigest". - - - - CONTAINER2005: The inferred image name '{0}' contains entirely invalid characters. The valid characters for an image name are alphanumeric characters, -, /, or _, and the image name must start with an alphanumeric character. - CONTAINER2005: Wywnioskowana nazwa obrazu „{0}” zawiera całkowicie nieprawidłowe znaki. Prawidłowe znaki nazwy obrazu to znaki alfanumeryczne oraz —, /, lub _, a nazwa obrazu musi zaczynać się znakiem alfanumerycznym. - {StrBegins="CONTAINER2005: "} - - - CONTAINER2005: The first character of the image name '{0}' must be a lowercase letter or a digit and all characters in the name must be an alphanumeric character, -, /, or _. - CONTAINER2005: Pierwszy znak nazwy obrazu „{0}” musi być małą literą lub cyfrą, a wszystkie znaki w nazwie muszą być znakami alfanumerycznymi, —, /, lub _. - {StrBegins="CONTAINER2005: "} + + CONTAINER2005: The first character of the image name must be a lowercase letter or a digit. + CONTAINER2005: pierwszy znak nazwy obrazu musi być małą literą lub cyfrą. + {StrBegin="CONTAINER2005: "} CONTAINER2017: A ContainerPort item was provided with an invalid port number '{0}'. ContainerPort items must have an Include value that is an integer, and a Type value that is either 'tcp' or 'udp'. CONTAINER2017: element ContainerPort został dostarczony z nieprawidłowym numerem portu „{0}”. Elementy ContainerPort muszą mieć wartość Include będącą liczbą całkowitą oraz wartość typu „tcp” lub „udp”. - {StrBegins="CONTAINER2017: "} + {StrBegin="CONTAINER2017: "} CONTAINER2017: A ContainerPort item was provided with an invalid port number '{0}' and an invalid port type '{1}'. ContainerPort items must have an Include value that is an integer, and a Type value that is either 'tcp' or 'udp'. CONTAINER2017: element ContainerPort został dostarczony z nieprawidłowym numerem portu „{0}” i nieprawidłowym typem portu „{1}”. Elementy ContainerPort muszą mieć wartość Include będącą liczbą całkowitą oraz wartość typu „tcp” lub „udp”. - {StrBegins="CONTAINER2017: "} + {StrBegin="CONTAINER2017: "} CONTAINER2017: A ContainerPort item was provided with an invalid port type '{0}'. ContainerPort items must have an Include value that is an integer, and a Type value that is either 'tcp' or 'udp'. CONTAINER2017: element ContainerPort został dostarczony z nieprawidłowym typem portu „{0}”. Elementy ContainerPort muszą mieć wartość Include będącą liczbą całkowitą oraz wartość typu „tcp” lub „udp”. - {StrBegins="CONTAINER2017: "} + {StrBegin="CONTAINER2017: "} CONTAINER2018: Invalid SDK prerelease version '{0}' - only 'rc' and 'preview' are supported. CONTAINER2018: nieprawidłowa wersja wstępna zestawu SDK „{0}” — obsługiwane są tylko wersje „rc” i „preview”. - {StrBegins="CONTAINER2018: "} + {StrBegin="CONTAINER2018: "} CONTAINER2019: Invalid SDK semantic version '{0}'. CONTAINER2019: nieprawidłowa wersja semantyczna zestawu SDK „{0}”. - {StrBegins="CONTAINER2019: "} + {StrBegin="CONTAINER2019: "} CONTAINER2007: Invalid {0} provided: {1}. Image tags must be alphanumeric, underscore, hyphen, or period. CONTAINER2007: podano nieprawidłowy {0}: {1}. Tagi obrazów muszą być alfanumeryczne, zawierać podkreślenia, łączniki lub kropki. - {StrBegins="CONTAINER2007: "} + {StrBegin="CONTAINER2007: "} CONTAINER2010: Invalid {0} provided: {1}. {0} must be a semicolon-delimited list of valid image tags. Image tags must be alphanumeric, underscore, hyphen, or period. CONTAINER2010: podano nieprawidłowy {0}: {1}. {0} musi być rozdzielaną średnikami listą prawidłowych tagów obrazów. Tagi obrazów muszą być alfanumeryczne, zawierać podkreślenia, łączniki lub kropki. - {StrBegins="CONTAINER2010: "} - - - Invalid string[] TargetRuntimeIdentifiers. Either all should be 'linux-musl' or none. - Nieprawidłowy element string[] TargetRuntimeIdentifiers. Wszystkie powinny mieć wartość "linux-musl" lub brak. - + {StrBegin="CONTAINER2010: "} CONTAINER1003: Token response had neither token nor access_token. CONTAINER1003: odpowiedź tokenu nie miała tokenu ani access_token. - {StrBegins="CONTAINER1003: "} + {StrBegin="CONTAINER1003: "} CONTAINER4005: Item '{0}' contains items without metadata 'Value', and they will be ignored. CONTAINER4005: Element „{0}” zawiera elementy bez metadanych „Value” i zostaną zignorowane. - {StrBegins="CONTAINER4005: "} + {StrBegin="CONTAINER4005: "} CONTAINER1012: The local registry is not available, but pushing to a local registry was requested. - CONTAINER1012: Rejestr lokalny jest niedostępny, ale zażądano wypchnięcia do rejestru lokalnego. - {StrBegins="CONTAINER1012: "} + CONTAINER1012: The local registry is not available, but pushing to a local registry was requested. + {StrBegin="CONTAINER1012: "} Error while reading daemon config: {0} @@ -307,22 +225,17 @@ CONTAINER2004: Unable to download layer with descriptor '{0}' from registry '{1}' because it does not exist. CONTAINER2004: nie można pobrać warstwy o deskryptorze „{0}” z rejestru „{1}”, ponieważ nie istnieje. - {StrBegins="CONTAINER2004: "} + {StrBegin="CONTAINER2004: "} CONTAINER2016: ContainerPort item '{0}' does not specify the port number. Please ensure the item's Include is a port number, for example '<ContainerPort Include="80" />' CONTAINER2016: element ContainerPort „{0}” nie określa numeru portu. Upewnij się, że element Include jest numerem portu, na przykład „<ContainerPort Include="80" />” - {StrBegins="CONTAINER2016: "} - - - 'mediaType' of manifests should be the same in image index. - Element "mediaType" manifestów powinien być taki sam w indeksie obrazu. - + {StrBegin="CONTAINER2016: "} CONTAINER1004: No RequestUri specified. CONTAINER1004: nie określono identyfikatora RequestUri. - {StrBegins="CONTAINER1004: "} + {StrBegin="CONTAINER1004: "} '{0}' was not a valid container image name, it was normalized to '{1}' @@ -332,27 +245,17 @@ CONTAINER2011: {0} '{1}' does not exist CONTAINER2011: {0} „{1}” nie istnieje - {StrBegins="CONTAINER2011: "} - - - CONTAINER1017: Unable to communicate with the registry '{0}'. - CONTAINER1017: nie można nawiązać komunikacji z rejestrem „{0}”. - {StrBegins="CONTAINER1017:" } + {StrBegin="CONTAINER2011: "} CONTAINER1013: Failed to push to the output registry: {0} CONTAINER1013: nie można wypchnąć do rejestru wyjściowego: {0} - {StrBegins="CONTAINER1013: "} - - - CONTAINER1014: Manifest pull failed. - CONTAINER1014: ściąganie manifestu nie powiodło się. - {StrBegins="CONTAINER1014: "} + {StrBegin="CONTAINER1013: "} - CONTAINER1005: Registry push failed; received status code '{0}'. - CONTAINER1005: wypychanie rejestru zakończyło się niepowodzeniem; odebrano kod stanu „{0}”. - {StrBegins="CONTAINER1005: "} + CONTAINER1005: Registry push failed. + CONTAINER1005: wypychanie rejestru nie powiodło się. + {StrBegin="CONTAINER1005: "} Uploading config to registry at blob '{0}', @@ -399,70 +302,45 @@ Przekazano tag „{0}” do „{1}”. {1} is the registry name - - CONTAINER1015: Unable to access the repository '{0}' at tag '{1}' in the registry '{2}'. Please confirm that this name and tag are present in the registry. - CONTAINER1015: nie można uzyskać dostępu do repozytorium „{0}” w tagu „{1}” w rejestrze „{2}”. Upewnij się, że ta nazwa i tag znajdują się w rejestrze. - {StrBegins="CONTAINER1015: "} - CONTAINER4003: Required '{0}' items contain empty items. CONTAINER4003: wymagane elementy „{0}” zawierają puste elementy. - {StrBegins="CONTAINER4003: "} + {StrBegin="CONTAINER4003: "} CONTAINER4002: Required '{0}' items were not set. CONTAINER4002: nie ustawiono wymaganych elementów „{0}”. - {StrBegins="CONTAINER4002: "} + {StrBegin="CONTAINER4002: "} CONTAINER4001: Required property '{0}' was not set or empty. CONTAINER4001: wymagana właściwość „{0}” nie została ustawiona lub jest pusta. - {StrBegins="CONTAINER4001: "} + {StrBegin="CONTAINER4001: "} CONTAINER1006: Too many retries, stopping. CONTAINER1006: zbyt wiele ponownych prób, zatrzymywanie. - {StrBegins="CONTAINER1006: "} - - - CONTAINER1016: Unable to access the repository '{0}' in the registry '{1}'. Please confirm your credentials are correct and that you have access to this repository and registry. - CONTAINER1016: nie można uzyskać dostępu do repozytorium „{0}” w rejestrze „{1}”. Upewnij się, że poświadczenia są poprawne oraz że masz dostęp do tego repozytorium i rejestru. - {StrBegins="CONTAINER1016:" } - - - CONTAINER1018: Unable to download image from the repository '{0}'. - CONTAINER1018: nie można pobrać obrazu z repozytorium „{0}”. - {StrBegins="CONTAINER1018:" } + {StrBegin="CONTAINER1006: "} CONTAINER2021: Unknown AppCommandInstruction '{0}'. Valid instructions are {1}. - CONTAINER2021: Nieznana instrukcja AppCommandInstruction „{0}”. Prawidłowe instrukcje to{1}. - {StrBegins="CONTAINER2021: "} + CONTAINER2021: Unknown AppCommandInstruction '{0}'. Valid instructions are {1}. + {StrBegin="CONTAINER2021: "} CONTAINER2002: Unknown local registry type '{0}'. Valid local container registry types are {1}. - CONTAINER2002: Nieznany typ rejestru lokalnego „{0}”. Prawidłowe typy lokalnego rejestru kontenerów: „{1}”. - {StrBegins="CONTAINER2002: "} + CONTAINER2002: Unknown local registry type '{0}'. Valid local container registry types are {1}. + {StrBegin="CONTAINER2002: "} CONTAINER2003: The manifest for {0}:{1} from registry {2} was an unknown type: {3}. Please raise an issue at https://github.com/dotnet/sdk-container-builds/issues with this message. CONTAINER2003: manifest dla {0}:{1} z rejestru {2} był nieznanym typem: {3}. Zgłoś problem na https://github.com/dotnet/sdk-container-builds/issues za pomocą tej wiadomości. - {StrBegins="CONTAINER2003: "} + {StrBegin="CONTAINER2003: "} CONTAINER2001: Unrecognized mediaType '{0}'. CONTAINER2001: nierozpoznany typ nośnika „{0}”. - {StrBegins="CONTAINER2001: "} - - - Cannot create image index for the provided 'mediaType' = '{0}'. - Nie można utworzyć indeksu obrazu dla podanego elementu "mediaType" = '{0}'. - - - - Unable to create tarball for mediaType '{0}'. - Nie można utworzyć elementu tarball dla elementu mediaType „{0}”. - + {StrBegin="CONTAINER2001: "} CONTAINER0000: Value for unit test {0} diff --git a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.pt-BR.xlf b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.pt-BR.xlf index 971dda801778..9277890d970a 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.pt-BR.xlf +++ b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.pt-BR.xlf @@ -5,73 +5,61 @@ CONTAINER1002: Request to Amazon Elastic Container Registry failed prematurely. This is often caused when the target repository does not exist in the registry. CONTAINER1002: a solicitação para o Amazon Elastic Container Registry falhou prematuramente. Isso geralmente é causado quando o repositório de destino não existe no registro. - {StrBegins="CONTAINER1002: "} + {StrBegin="CONTAINER1002: "} CONTAINER2008: Both {0} and {1} were provided, but only one or the other is allowed. CONTAINER2008: Ambos {0} e {1} foram fornecidos, mas apenas um ou outro é permitido. - {StrBegins="CONTAINER2008: "} + {StrBegin="CONTAINER2008: "} CONTAINER2025: ContainerAppCommandArgs are provided without specifying a ContainerAppCommand. - CONTAINER2025: ContainerAppCommandArgs são fornecidos sem especificar um ContainerAppCommand. - {StrBegins="CONTAINER2025: "} + CONTAINER2025: ContainerAppCommandArgs are provided without specifying a ContainerAppCommand. + {StrBegin="CONTAINER2025: "} CONTAINER2026: ContainerAppCommand and ContainerAppCommandArgs must be empty when ContainerAppCommandInstruction is '{0}'. - CONTAINER2026: ContainerAppCommand e ContainerAppCommandArgs devem estar vazios quando ContainerAppCommandInstruction é '{0}'. - {StrBegins="CONTAINER2026: "} - - - local archive at '{0}' - arquivo morto local em '{0}' - {0} is the path to the file written + CONTAINER2026: ContainerAppCommand and ContainerAppCommandArgs must be empty when ContainerAppCommandInstruction is '{0}'. + {StrBegin="CONTAINER2026: "} CONTAINER2022: The base image has an entrypoint that will be overwritten to start the application. Set ContainerAppCommandInstruction to 'Entrypoint' if this is desired. To preserve the base image entrypoint, set ContainerAppCommandInstruction to 'DefaultArgs'. - CONTAINER2022: A imagem base possui um ponto de entrada que será substituído para iniciar o aplicativo. Defina ContainerAppCommandInstruction como 'Entrypoint' se desejar. Para preservar o ponto de entrada da imagem base, defina ContainerAppCommandInstruction como 'DefaultArgs'. - {StrBegins="CONTAINER2022: "} + CONTAINER2022: The base image has an entrypoint that will be overwritten to start the application. Set ContainerAppCommandInstruction to 'Entrypoint' if this is desired. To preserve the base image entrypoint, set ContainerAppCommandInstruction to 'DefaultArgs'. + {StrBegin="CONTAINER2022: "} CONTAINER2009: Could not parse {0}: {1} CONTAINER2009: não foi possível analisar {0}: {1} - {StrBegins="CONTAINER2009: "} + {StrBegin="CONTAINER2009: "} CONTAINER2020: {0} does not specify a registry and will be pulled from Docker Hub. Please prefix the name with the image registry, for example: '{1}/<image>'. - CONTAINER2020: {0} não especifica um registro e será extraído do Docker Hub. Por favor, prefixe o nome com o registro de imagem, por exemplo: '{1}/<image>'. - {StrBegins="CONTAINER2020: "} + CONTAINER2020: {0} does not specify a registry and will be pulled from Docker Hub. Please prefix the name with the image registry, for example: '{1}/<image>'. + {StrBegin="CONTAINER2020: "} CONTAINER2013: {0} had spaces in it, replacing with dashes. CONTAINER2013: {0} continha espaços, substituindo por travessões. - {StrBegins="CONTAINER2013: "} + {StrBegin="CONTAINER2013: "} CONTAINER1011: Couldn't find matching base image for {0} that matches RuntimeIdentifier {1}. CONTAINER1011: Não foi possível encontrar a imagem base correspondente para {0} que corresponda ao RuntimeIdentifier {1}. - {StrBegins="CONTAINER1011: "} + {StrBegin="CONTAINER1011: "} CONTAINER1001: Failed to upload blob using {0}; received status code '{1}'. CONTAINER1001: Falha ao carregar o blob usando {0}; código de status recebido '{1}'. - {StrBegins="CONTAINER1001: "} - - - Building image index '{0}' on top of manifests {1}. - Criando índice de '{0}' na parte superior dos manifestos {1}. - - {0} is the name of the image index and its tag, {1} is the list of manifests digests - + {StrBegin="CONTAINER1001: "} - Pushed image '{0}' to {1}. - Imagem enviada '{0}' para {1}. + Pushed container '{0}' to Docker daemon. + Enviou o contêiner '{0}' para o daemon do Docker. - Pushed image '{0}' to registry '{1}'. - Imagem enviada por push '{0}' para o registro '{1}'. + Pushed container '{0}' to registry '{1}'. + Contêiner empurrado '{0}' para o registro '{1}'. @@ -79,220 +67,150 @@ Construindo imagem '{0}' com tags '{1}' em cima da imagem base '{2}'. - - Building image '{0}' for runtime identifier '{1}' on top of base image '{2}'. - Criando a imagem "{0}" para o identificador de tempo de execução "{1}" com base na imagem base "{2}". - - CONTAINER1007: Could not deserialize token from JSON. CONTAINER1007: não foi possível desserializar o token do JSON. - {StrBegins="CONTAINER1007: "} + {StrBegin="CONTAINER1007: "} CONTAINER2012: Could not recognize registry '{0}'. CONTAINER2012: não foi possível reconhecer o registro '{0}'. - {StrBegins="CONTAINER2012: "} - - - local registry via '{0}' - registro local pelo '{0}' - {0} is the command used + {StrBegin="CONTAINER2012: "} CONTAINER3002: Failed to get docker info({0})\n{1}\n{2} CONTAINER3002: Falha ao obter informações do docker({0})\n{1}\n{2} - {StrBegins="CONTAINER3002: "} + {StrBegin="CONTAINER3002: "} CONTAINER3002: Failed to get docker info: {0} CONTAINER3002: Falha ao obter informações do docker: {0} - {StrBegins="CONTAINER3002: "} + {StrBegin="CONTAINER3002: "} - + CONTAINER3001: Failed creating {0} process. - CONTAINER3001: Falha ao criar {0} processo. + CONTAINER3001: Failed creating {0} process. CONTAINER3001: {0} is the name of the command we failed to run, usually 'docker' or 'podman'. CONTAINER4006: Property '{0}' is empty or contains whitespace and will be ignored. CONTAINER4006: A propriedade '{0}' está vazia ou contém espaços em branco e será ignorada. - {StrBegins="CONTAINER4006: "} + {StrBegin="CONTAINER4006: "} CONTAINER4004: Items '{0}' contain empty item(s) which will be ignored. CONTAINER4004: Os itens '{0}' contêm itens vazios que serão ignorados. - {StrBegins="CONTAINER4004: "} + {StrBegin="CONTAINER4004: "} CONTAINER2023: A ContainerEntrypoint and ContainerAppCommandArgs are provided. ContainerAppInstruction must be set to configure how the application is started. Valid instructions are {0}. - CONTAINER2023: Um ContainerEntrypoint e ContainerAppCommandArgs são fornecidos. ContainerAppInstruction deve ser definido para configurar como o aplicativo é iniciado. As instruções válidas são {0}. - {StrBegins="CONTAINER2023: "} + CONTAINER2023: A ContainerEntrypoint and ContainerAppCommandArgs are provided. ContainerAppInstruction must be set to configure how the application is started. Valid instructions are {0}. + {StrBegin="CONTAINER2023: "} CONTAINER2027: A ContainerEntrypoint is provided. ContainerAppInstruction must be set to configure how the application is started. Valid instructions are {0}. - CONTAINER2027: Um ContainerEntrypoint é fornecido. ContainerAppInstruction deve ser definido para configurar como o aplicativo é iniciado. As instruções válidas são {0}. - {StrBegins="CONTAINER2027: "} + CONTAINER2027: A ContainerEntrypoint is provided. ContainerAppInstruction must be set to configure how the application is started. Valid instructions are {0}. + {StrBegin="CONTAINER2027: "} CONTAINER2024: ContainerEntrypointArgs are provided without specifying a ContainerEntrypoint. - CONTAINER2024: ContainerEntrypointArgs são fornecidos sem especificar um ContainerEntrypoint. - {StrBegins="CONTAINER2024: "} + CONTAINER2024: ContainerEntrypointArgs are provided without specifying a ContainerEntrypoint. + {StrBegin="CONTAINER2024: "} CONTAINER2029: ContainerEntrypointArgsSet are provided. Change to use ContainerAppCommandArgs for arguments that must always be set, or ContainerDefaultArgs for arguments that can be overridden when the container is created. - CONTAINER2029: ContainerEntrypointArgsSet são fornecidos. Altere para usar ContainerAppCommandArgs para argumentos que sempre devem ser definidos ou ContainerDefaultArgs para argumentos que podem ser substituídos quando o contêiner é criado. - {StrBegins="CONTAINER2029: "} + CONTAINER2029: ContainerEntrypointArgsSet are provided. Change to use ContainerAppCommandArgs for arguments that must always be set, or ContainerDefaultArgs for arguments that can be overridden when the container is created. + {StrBegin="CONTAINER2029: "} CONTAINER2028: ContainerEntrypoint can not be combined with ContainerAppCommandInstruction '{0}'. - CONTAINER2028: ContainerEntrypoint não pode ser combinado com ContainerAppCommandInstruction '{0}'. - {StrBegins="CONTAINER2028: "} + CONTAINER2028: ContainerEntrypoint can not be combined with ContainerAppCommandInstruction '{0}'. + {StrBegin="CONTAINER2028: "} CONTAINER1008: Failed retrieving credentials for "{0}": {1} CONTAINER1008: Falha ao recuperar credenciais para "{0}": {1} - {StrBegins="CONTAINER1008: "} - - - CONTAINER2030: GenerateLabels was disabled but GenerateDigestLabel was enabled - no digest label will be created. - CONTAINER2030: GenerateLabels foi desabilitado, mas GenerateDigestLabel foi habilitado - não serão criados rótulos de resumo. - {StrBegins="CONTAINER2030: "} + {StrBegin="CONTAINER1008: "} No host object detected. Nenhum objeto de host detectado. - - Cannot create image index because at least one of the provided images' config is missing 'architecture'. - Não é possível criar o índice de imagem porque a configuração 'architecture' está ausente em pelo menos uma das imagens fornecidas. - - - - Cannot create image index because at least one of the provided images' config is missing 'os'. - Não é possível criar o índice de imagem porque pelo menos uma das imagens fornecidas está sem a configuração 'os'. - - - - Pushed image index '{0}' to registry '{1}'. - Índice de imagem enviada por push ''{0}'' para o registro ''{1}''. - - - - Image index creation for Podman is not supported. - Não há suporte para a criação de índice de imagem para Podman. - - CONTAINER1009: Failed to load image from local registry. stdout: {0} - CONTAINER1009: falha ao carregar a imagem do registro local. stdout: {0} - {StrBegins="CONTAINER1009: "} - - - CONTAINER1020: Failed to load image because containerd image store is not enabled for Docker. Tip: You can enable it by checking 'Use containerd for pulling and storing images' in Docker Desktop settings. - CONTAINER1020: falha ao carregar a imagem porque o repositório de imagens em contêineres não está habilitado para o Docker. Dica: você pode habilita-la verificando 'Usar contêiner para extrair e armazenar imagens' nas configurações do Docker Desktop. - {StrBegin="CONTAINER1020: "} + CONTAINER1009: falha ao carregar a imagem no daemon do Docker local. stdout: {0} + {StrBegin="CONTAINER1009: "} CONTAINER1010: Pulling images from local registry is not supported. - CONTAINER1010: A extração de imagens do registro local não é suportada. - {StrBegins="CONTAINER1010: "} + CONTAINER1010: não há suporte para a extração de imagens do daemon do Docker local. + {StrBegin="CONTAINER1010: "} - - Cannot create image index because no images were provided. - Não é possível criar índice de imagem porque nenhuma imagem foi fornecida. - - - - CONTAINER2031: The container image format '{0}' is not supported. Supported formats are '{1}'. - CONTAINER2031: não há suporte para o formato '{0}' imagem do contêiner. Os formatos com suporte são '{1}'. - {StrBegins="CONTAINER2031: "} + + CONTAINER2014: Invalid {0}: {1}. + CONTAINER2014: Inválido {0}: {1}. + {StrBegin="CONTAINER2014: "} CONTAINER2015: {0}: '{1}' was not a valid Environment Variable. Ignoring. CONTAINER2015: {0}: '{1}' não era uma variável de ambiente válida. Ignorando. - {StrBegins="CONTAINER2015: "} + {StrBegin="CONTAINER2015: "} - - Cannot create image index because at least one of the provided images' config is invalid. - Não é possível criar o índice de imagem porque pelo menos uma das imagens fornecidas é inválida. - - - - Cannot create image index because at least one of the provided images' manifest is invalid. - Não é possível criar o índice de imagem porque pelo menos um dos manifestos das imagens fornecidas é inválido. - - - - Cannot create image index because provided images are invalid. Items must have 'Config', 'Manifest', 'ManifestMediaType' and 'ManifestDigest' metadata. - Não é possível criar o índice de imagem porque as imagens fornecidas são inválidas. Os itens devem ter metadados 'Config', 'Manifest', 'ManifestMediaType' e 'ManifestDigest'. - - - - CONTAINER2005: The inferred image name '{0}' contains entirely invalid characters. The valid characters for an image name are alphanumeric characters, -, /, or _, and the image name must start with an alphanumeric character. - CONTAINER2005: o nome da imagem inferida '{0}' contém caracteres totalmente inválidos. Os caracteres válidos para um nome de imagem são caracteres alfanuméricos, -, / ou _, e o nome da imagem deve começar com um caractere alfanumérico. - {StrBegins="CONTAINER2005: "} - - - CONTAINER2005: The first character of the image name '{0}' must be a lowercase letter or a digit and all characters in the name must be an alphanumeric character, -, /, or _. - CONTAINER2005: o primeiro caractere do nome da imagem '{0}' deve ser uma letra minúscula ou um dígito e todos os caracteres do nome devem ser um caractere alfanumérico, -, / ou _. - {StrBegins="CONTAINER2005: "} + + CONTAINER2005: The first character of the image name must be a lowercase letter or a digit. + CONTAINER2005: O primeiro caractere do nome da imagem deve ser uma letra minúscula ou um dígito. + {StrBegin="CONTAINER2005: "} CONTAINER2017: A ContainerPort item was provided with an invalid port number '{0}'. ContainerPort items must have an Include value that is an integer, and a Type value that is either 'tcp' or 'udp'. CONTAINER2017: Um item ContainerPort foi fornecido com um número de porta inválido '{0}'. Os itens ContainerPort devem ter um valor Include que seja um número inteiro e um valor Type que seja 'tcp' ou 'udp'. - {StrBegins="CONTAINER2017: "} + {StrBegin="CONTAINER2017: "} CONTAINER2017: A ContainerPort item was provided with an invalid port number '{0}' and an invalid port type '{1}'. ContainerPort items must have an Include value that is an integer, and a Type value that is either 'tcp' or 'udp'. CONTAINER2017: Um item ContainerPort foi fornecido com um número de porta inválido '{0}' e um tipo de porta inválido '{1}'. Os itens ContainerPort devem ter um valor Include que seja um número inteiro e um valor Type que seja 'tcp' ou 'udp'. - {StrBegins="CONTAINER2017: "} + {StrBegin="CONTAINER2017: "} CONTAINER2017: A ContainerPort item was provided with an invalid port type '{0}'. ContainerPort items must have an Include value that is an integer, and a Type value that is either 'tcp' or 'udp'. CONTAINER2017: Um item ContainerPort foi fornecido com um tipo de porta inválido '{0}'. Os itens ContainerPort devem ter um valor Include que seja um número inteiro e um valor Type que seja 'tcp' ou 'udp'. - {StrBegins="CONTAINER2017: "} + {StrBegin="CONTAINER2017: "} CONTAINER2018: Invalid SDK prerelease version '{0}' - only 'rc' and 'preview' are supported. CONTAINER2018: Versão de pré-lançamento inválida do SDK '{0}' - apenas 'rc' e 'preview' são suportados. - {StrBegins="CONTAINER2018: "} + {StrBegin="CONTAINER2018: "} CONTAINER2019: Invalid SDK semantic version '{0}'. CONTAINER2019: versão semântica do SDK inválida '{0}'. - {StrBegins="CONTAINER2019: "} + {StrBegin="CONTAINER2019: "} CONTAINER2007: Invalid {0} provided: {1}. Image tags must be alphanumeric, underscore, hyphen, or period. CONTAINER2007: Inválido {0} fornecido: {1}. As tags de imagem devem ser alfanuméricas, sublinhado, hífen ou ponto. - {StrBegins="CONTAINER2007: "} + {StrBegin="CONTAINER2007: "} CONTAINER2010: Invalid {0} provided: {1}. {0} must be a semicolon-delimited list of valid image tags. Image tags must be alphanumeric, underscore, hyphen, or period. CONTAINER2010: inválido {0} fornecido: {1}. {0} deve ser uma lista delimitada por ponto-e-vírgula de marcas de imagem válidas. As tags de imagem devem ser alfanuméricas, sublinhado, hífen ou ponto. - {StrBegins="CONTAINER2010: "} - - - Invalid string[] TargetRuntimeIdentifiers. Either all should be 'linux-musl' or none. - Cadeia de caracteres inválida[] TargetRuntimeIdentifiers. Todos devem ser 'linux-musl' ou nenhum. - + {StrBegin="CONTAINER2010: "} CONTAINER1003: Token response had neither token nor access_token. CONTAINER1003: A resposta do token não tinha token nem access_token. - {StrBegins="CONTAINER1003: "} + {StrBegin="CONTAINER1003: "} CONTAINER4005: Item '{0}' contains items without metadata 'Value', and they will be ignored. CONTAINER4005: O item '{0}' contém itens sem metadados 'Valor' e eles serão ignorados. - {StrBegins="CONTAINER4005: "} + {StrBegin="CONTAINER4005: "} CONTAINER1012: The local registry is not available, but pushing to a local registry was requested. - CONTAINER1012: O registro local não está disponível, mas o push para um registro local foi solicitado. - {StrBegins="CONTAINER1012: "} + CONTAINER1012: The local registry is not available, but pushing to a local registry was requested. + {StrBegin="CONTAINER1012: "} Error while reading daemon config: {0} @@ -307,22 +225,17 @@ CONTAINER2004: Unable to download layer with descriptor '{0}' from registry '{1}' because it does not exist. CONTAINER2004: Não foi possível baixar a camada com o descritor '{0}' do registro '{1}' porque não existe. - {StrBegins="CONTAINER2004: "} + {StrBegin="CONTAINER2004: "} CONTAINER2016: ContainerPort item '{0}' does not specify the port number. Please ensure the item's Include is a port number, for example '<ContainerPort Include="80" />' CONTAINER2016: O item ContainerPort '{0}' não especifica o número da porta. Certifique-se de que o Include do item seja um número de porta, por exemplo '<ContainerPort Include="80" />' - {StrBegins="CONTAINER2016: "} - - - 'mediaType' of manifests should be the same in image index. - 'mediaType' dos manifestos deve ser o mesmo no índice de imagem. - + {StrBegin="CONTAINER2016: "} CONTAINER1004: No RequestUri specified. CONTAINER1004: Nenhum RequestUri especificado. - {StrBegins="CONTAINER1004: "} + {StrBegin="CONTAINER1004: "} '{0}' was not a valid container image name, it was normalized to '{1}' @@ -332,27 +245,17 @@ CONTAINER2011: {0} '{1}' does not exist CONTAINER2011: {0} '{1}' não existe - {StrBegins="CONTAINER2011: "} - - - CONTAINER1017: Unable to communicate with the registry '{0}'. - CONTAINER1017: não é possível se comunicar com o registro ''{0}''. - {StrBegins="CONTAINER1017:" } + {StrBegin="CONTAINER2011: "} CONTAINER1013: Failed to push to the output registry: {0} CONTAINER1013: Failed to push to the output registry: {0} - {StrBegins="CONTAINER1013: "} - - - CONTAINER1014: Manifest pull failed. - CONTAINER1014: falha ao efetuar pull do manifesto. - {StrBegins="CONTAINER1014: "} + {StrBegin="CONTAINER1013: "} - CONTAINER1005: Registry push failed; received status code '{0}'. - CONTAINER1005: falha no push do registro; código de status recebido '{0}'. - {StrBegins="CONTAINER1005: "} + CONTAINER1005: Registry push failed. + CONTAINER1005: falha no push do registro. + {StrBegin="CONTAINER1005: "} Uploading config to registry at blob '{0}', @@ -399,70 +302,45 @@ Marca carregada '{0}' para '{1}'. {1} is the registry name - - CONTAINER1015: Unable to access the repository '{0}' at tag '{1}' in the registry '{2}'. Please confirm that this name and tag are present in the registry. - CONTAINER1015: não é possível acessar o repositório ''{0}'' na marca ''{1}'' no registro ''{2}''. Confirme se este nome e marca estão presentes no Registro. - {StrBegins="CONTAINER1015: "} - CONTAINER4003: Required '{0}' items contain empty items. CONTAINER4003: Os itens obrigatórios '{0}' contêm itens vazios. - {StrBegins="CONTAINER4003: "} + {StrBegin="CONTAINER4003: "} CONTAINER4002: Required '{0}' items were not set. CONTAINER4002: Os itens '{0}' obrigatórios não foram definidos. - {StrBegins="CONTAINER4002: "} + {StrBegin="CONTAINER4002: "} CONTAINER4001: Required property '{0}' was not set or empty. CONTAINER4001: A propriedade obrigatória '{0}' não foi definida ou está vazia. - {StrBegins="CONTAINER4001: "} + {StrBegin="CONTAINER4001: "} CONTAINER1006: Too many retries, stopping. CONTAINER1006: Muitas tentativas, parando. - {StrBegins="CONTAINER1006: "} - - - CONTAINER1016: Unable to access the repository '{0}' in the registry '{1}'. Please confirm your credentials are correct and that you have access to this repository and registry. - CONTAINER1016: não é possível acessar o repositório ''{0}'' no registro ''{1}''. Confirme se suas credenciais estão corretas e se você tem acesso a este repositório e ao Registro. - {StrBegins="CONTAINER1016:" } - - - CONTAINER1018: Unable to download image from the repository '{0}'. - CONTAINER1018: não é possível baixar a imagem do repositório '{0}'. - {StrBegins="CONTAINER1018:" } + {StrBegin="CONTAINER1006: "} CONTAINER2021: Unknown AppCommandInstruction '{0}'. Valid instructions are {1}. - CONTAINER2021: AppCommandInstruction desconhecido '{0}'. As instruções válidas são {1}. - {StrBegins="CONTAINER2021: "} + CONTAINER2021: Unknown AppCommandInstruction '{0}'. Valid instructions are {1}. + {StrBegin="CONTAINER2021: "} CONTAINER2002: Unknown local registry type '{0}'. Valid local container registry types are {1}. - CONTAINER2002: Tipo de registro local desconhecido '{0}'. Os tipos de registro de contêiner local válidos são {1}. - {StrBegins="CONTAINER2002: "} + CONTAINER2002: Unknown local registry type '{0}'. Valid local container registry types are {1}. + {StrBegin="CONTAINER2002: "} CONTAINER2003: The manifest for {0}:{1} from registry {2} was an unknown type: {3}. Please raise an issue at https://github.com/dotnet/sdk-container-builds/issues with this message. CONTAINER2003: O manifesto para {0}:{1} do registro {2} era um tipo desconhecido:{3}. Levante um problema em https://github.com/dotnet/sdk-container-builds/issues com esta mensagem. - {StrBegins="CONTAINER2003: "} + {StrBegin="CONTAINER2003: "} CONTAINER2001: Unrecognized mediaType '{0}'. CONTAINER2001: MediaType não reconhecido '{0}'. - {StrBegins="CONTAINER2001: "} - - - Cannot create image index for the provided 'mediaType' = '{0}'. - Não é possível criar índice de imagem para o 'mediaType' = '{0}'. - - - - Unable to create tarball for mediaType '{0}'. - Não é possível criar tarball para mediaType "{0}". - + {StrBegin="CONTAINER2001: "} CONTAINER0000: Value for unit test {0} diff --git a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.ru.xlf b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.ru.xlf index 1f4ef89cfdfa..e8dbea25606d 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.ru.xlf +++ b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.ru.xlf @@ -5,73 +5,61 @@ CONTAINER1002: Request to Amazon Elastic Container Registry failed prematurely. This is often caused when the target repository does not exist in the registry. CONTAINER1002: преждевременный сбой запроса к Реестру контейнеров Amazon Elastic. Это часто происходит из-за отсутствия целевого репозитория в реестре. - {StrBegins="CONTAINER1002: "} + {StrBegin="CONTAINER1002: "} CONTAINER2008: Both {0} and {1} were provided, but only one or the other is allowed. CONTAINER2008: {0} и {1} предоставлены, но разрешен только один из них. - {StrBegins="CONTAINER2008: "} + {StrBegin="CONTAINER2008: "} CONTAINER2025: ContainerAppCommandArgs are provided without specifying a ContainerAppCommand. - CONTAINER2025: ContainerAppCommandArgs предоставлены без указания ContainerAppCommand. - {StrBegins="CONTAINER2025: "} + CONTAINER2025: ContainerAppCommandArgs are provided without specifying a ContainerAppCommand. + {StrBegin="CONTAINER2025: "} CONTAINER2026: ContainerAppCommand and ContainerAppCommandArgs must be empty when ContainerAppCommandInstruction is '{0}'. - CONTAINER2026: ContainerAppCommand и ContainerAppCommandArgs должны быть пустыми, если ContainerAppCommandInstruction — "{0}". - {StrBegins="CONTAINER2026: "} - - - local archive at '{0}' - локальное архивирование в "{0}" - {0} is the path to the file written + CONTAINER2026: ContainerAppCommand and ContainerAppCommandArgs must be empty when ContainerAppCommandInstruction is '{0}'. + {StrBegin="CONTAINER2026: "} CONTAINER2022: The base image has an entrypoint that will be overwritten to start the application. Set ContainerAppCommandInstruction to 'Entrypoint' if this is desired. To preserve the base image entrypoint, set ContainerAppCommandInstruction to 'DefaultArgs'. - CONTAINER2022: базовый образ содержит точку входа, которая будет перезаписана для запуска приложения. При необходимости настройте для ContainerAppCommandInstruction значение "Entrypoint". Чтобы сохранить точку входа базового образа, настройте для ContainerAppCommandInstruction значение "DefaultArgs". - {StrBegins="CONTAINER2022: "} + CONTAINER2022: The base image has an entrypoint that will be overwritten to start the application. Set ContainerAppCommandInstruction to 'Entrypoint' if this is desired. To preserve the base image entrypoint, set ContainerAppCommandInstruction to 'DefaultArgs'. + {StrBegin="CONTAINER2022: "} CONTAINER2009: Could not parse {0}: {1} CONTAINER2009: не удалось проанализировать {0}: {1} - {StrBegins="CONTAINER2009: "} + {StrBegin="CONTAINER2009: "} CONTAINER2020: {0} does not specify a registry and will be pulled from Docker Hub. Please prefix the name with the image registry, for example: '{1}/<image>'. - CONTAINER2020: {0} не указывает реестр и будет извлечен из Docker Hub. Добавьте для имени префикс в виде реестра образов, например: "{1}/<image>". - {StrBegins="CONTAINER2020: "} + CONTAINER2020: {0} does not specify a registry and will be pulled from Docker Hub. Please prefix the name with the image registry, for example: '{1}/<image>'. + {StrBegin="CONTAINER2020: "} CONTAINER2013: {0} had spaces in it, replacing with dashes. CONTAINER2013: {0} содержит пробелы, замененные на дефисы. - {StrBegins="CONTAINER2013: "} + {StrBegin="CONTAINER2013: "} CONTAINER1011: Couldn't find matching base image for {0} that matches RuntimeIdentifier {1}. CONTAINER1011: не удалось найти соответствующий базовый образ для {0}, соответствующего RuntimeIdentifier {1}. - {StrBegins="CONTAINER1011: "} + {StrBegin="CONTAINER1011: "} CONTAINER1001: Failed to upload blob using {0}; received status code '{1}'. CONTAINER1001: не удалось отправить BLOB-объект с помощью {0}; получен код состояния "{1}". - {StrBegins="CONTAINER1001: "} - - - Building image index '{0}' on top of manifests {1}. - Построение индекса '{0}' поверх манифестов {1}. - - {0} is the name of the image index and its tag, {1} is the list of manifests digests - + {StrBegin="CONTAINER1001: "} - Pushed image '{0}' to {1}. - Изображение "{0}" отправлено в {1}. + Pushed container '{0}' to Docker daemon. + Принудительно отправлен контейнер "{0}" в управляющую программу Docker. - Pushed image '{0}' to registry '{1}'. - Изображение "{0}" отправлено в реестр "{1}". + Pushed container '{0}' to registry '{1}'. + Принудительно отправлен контейнер "{0}" в реестр "{1}". @@ -79,220 +67,150 @@ Выполняется сборка образа "{0}" с тегами "{1}" поверх базового образа "{2}". - - Building image '{0}' for runtime identifier '{1}' on top of base image '{2}'. - Сборка образа "{0}" для идентификатора среды выполнения "{1}" на основе базового образа "{2}". - - CONTAINER1007: Could not deserialize token from JSON. CONTAINER1007: не удалось десериализовать токен из JSON. - {StrBegins="CONTAINER1007: "} + {StrBegin="CONTAINER1007: "} CONTAINER2012: Could not recognize registry '{0}'. CONTAINER2012: не удалось распознать реестр "{0}". - {StrBegins="CONTAINER2012: "} - - - local registry via '{0}' - локальный реестр через "{0}" - {0} is the command used + {StrBegin="CONTAINER2012: "} CONTAINER3002: Failed to get docker info({0})\n{1}\n{2} CONTAINER3002: не удалось получить сведения Docker({0})\n{1}\n{2} - {StrBegins="CONTAINER3002: "} + {StrBegin="CONTAINER3002: "} CONTAINER3002: Failed to get docker info: {0} CONTAINER3002: не удалось получить сведения Docker: {0} - {StrBegins="CONTAINER3002: "} + {StrBegin="CONTAINER3002: "} - + CONTAINER3001: Failed creating {0} process. - CONTAINER3001: не удалось создать процесс {0}. + CONTAINER3001: Failed creating {0} process. CONTAINER3001: {0} is the name of the command we failed to run, usually 'docker' or 'podman'. CONTAINER4006: Property '{0}' is empty or contains whitespace and will be ignored. CONTAINER4006: свойство "{0}" пусто или содержит пробелы и будет пропущено. - {StrBegins="CONTAINER4006: "} + {StrBegin="CONTAINER4006: "} CONTAINER4004: Items '{0}' contain empty item(s) which will be ignored. CONTAINER4004: элементы "{0}" содержат пустые элементы, которые будут пропущены. - {StrBegins="CONTAINER4004: "} + {StrBegin="CONTAINER4004: "} CONTAINER2023: A ContainerEntrypoint and ContainerAppCommandArgs are provided. ContainerAppInstruction must be set to configure how the application is started. Valid instructions are {0}. - CONTAINER2023: предоставлены ContainerEntrypoint и ContainerAppCommandArgs. Чтобы настроить способ запуска приложения, необходимо настроить ContainerAppInstruction. Допустимые инструкции: {0}. - {StrBegins="CONTAINER2023: "} + CONTAINER2023: A ContainerEntrypoint and ContainerAppCommandArgs are provided. ContainerAppInstruction must be set to configure how the application is started. Valid instructions are {0}. + {StrBegin="CONTAINER2023: "} CONTAINER2027: A ContainerEntrypoint is provided. ContainerAppInstruction must be set to configure how the application is started. Valid instructions are {0}. - CONTAINER2027: предоставлен ContainerEntrypoint. Чтобы настроить способ запуска приложения, необходимо настроить ContainerAppInstruction. Допустимые инструкции: {0}. - {StrBegins="CONTAINER2027: "} + CONTAINER2027: A ContainerEntrypoint is provided. ContainerAppInstruction must be set to configure how the application is started. Valid instructions are {0}. + {StrBegin="CONTAINER2027: "} CONTAINER2024: ContainerEntrypointArgs are provided without specifying a ContainerEntrypoint. - CONTAINER2024: ContainerEntrypointArgs предоставлены без указания ContainerEntrypoint. - {StrBegins="CONTAINER2024: "} + CONTAINER2024: ContainerEntrypointArgs are provided without specifying a ContainerEntrypoint. + {StrBegin="CONTAINER2024: "} CONTAINER2029: ContainerEntrypointArgsSet are provided. Change to use ContainerAppCommandArgs for arguments that must always be set, or ContainerDefaultArgs for arguments that can be overridden when the container is created. - CONTAINER2029: предоставлен параметр ContainerEntrypointArgsSet. Измените, чтобы использовать ContainerAppCommandArgs для аргументов, которые всегда должны быть настроены, или ContainerDefaultArgs для аргументов, которые можно переопределить при создании контейнера. - {StrBegins="CONTAINER2029: "} + CONTAINER2029: ContainerEntrypointArgsSet are provided. Change to use ContainerAppCommandArgs for arguments that must always be set, or ContainerDefaultArgs for arguments that can be overridden when the container is created. + {StrBegin="CONTAINER2029: "} CONTAINER2028: ContainerEntrypoint can not be combined with ContainerAppCommandInstruction '{0}'. - CONTAINER2028: ContainerEntrypoint невозможно объединить с ContainerAppCommandInstruction "{0}". - {StrBegins="CONTAINER2028: "} + CONTAINER2028: ContainerEntrypoint can not be combined with ContainerAppCommandInstruction '{0}'. + {StrBegin="CONTAINER2028: "} CONTAINER1008: Failed retrieving credentials for "{0}": {1} CONTAINER1008: Не удалось получить учетные данные для "{0}": {1} - {StrBegins="CONTAINER1008: "} - - - CONTAINER2030: GenerateLabels was disabled but GenerateDigestLabel was enabled - no digest label will be created. - CONTAINER2030: параметр GenerateLabels был отключен, но параметр GenerateDigestLabel был включен — метка дайджеста не будет создана. - {StrBegins="CONTAINER2030: "} + {StrBegin="CONTAINER1008: "} No host object detected. Объект узла не обнаружен. - - Cannot create image index because at least one of the provided images' config is missing 'architecture'. - Не удается создать индекс образа, так как в конфигурации по крайней мере одного из предоставленных образов отсутствует "architecture". - - - - Cannot create image index because at least one of the provided images' config is missing 'os'. - Не удается создать индекс образа, так как по крайней мере в одной из указанных конфигураций образов отсутствует "os". - - - - Pushed image index '{0}' to registry '{1}'. - Индекс изображения "{0}" отправлен в реестр "{1}". - - - - Image index creation for Podman is not supported. - Создание индекса образа для Podman не поддерживается. - - CONTAINER1009: Failed to load image from local registry. stdout: {0} - CONTAINER1009: не удалось загрузить образ из локального реестра. stdout: {0} - {StrBegins="CONTAINER1009: "} - - - CONTAINER1020: Failed to load image because containerd image store is not enabled for Docker. Tip: You can enable it by checking 'Use containerd for pulling and storing images' in Docker Desktop settings. - CONTAINER1020: не удалось загрузить образ, так как контейнерное хранилище образов не включено для Docker. Совет. Вы можете включить его, настроив флаинг "Использовать контейнер для вытягивание и хранения изображений" в параметрах Docker Desktop. - {StrBegin="CONTAINER1020: "} + CONTAINER1009: не удалось загрузить образ в локальную управляющую программу Docker. StdOut:{0} + {StrBegin="CONTAINER1009: "} CONTAINER1010: Pulling images from local registry is not supported. - CONTAINER1010: извлечение образов из локального реестра не поддерживается. - {StrBegins="CONTAINER1010: "} + CONTAINER1010: извлечение образов из локальной управляющей программы Docker не поддерживается. + {StrBegin="CONTAINER1010: "} - - Cannot create image index because no images were provided. - Не удается создать индекс образа, так как изображения не предоставлены. - - - - CONTAINER2031: The container image format '{0}' is not supported. Supported formats are '{1}'. - CONTAINER2031: формат '{0}' контейнера не поддерживается. Поддерживаемые форматы '{1}'. - {StrBegins="CONTAINER2031: "} + + CONTAINER2014: Invalid {0}: {1}. + CONTAINER2014: недопустимый {0}: {1}. + {StrBegin="CONTAINER2014: "} CONTAINER2015: {0}: '{1}' was not a valid Environment Variable. Ignoring. CONTAINER2015: {0}: "{1}" не является допустимой переменной среды. Пропуск. - {StrBegins="CONTAINER2015: "} + {StrBegin="CONTAINER2015: "} - - Cannot create image index because at least one of the provided images' config is invalid. - Не удается создать индекс образа, так как по крайней мере один из предоставленных образов имеет недопустимую конфигурацию. - - - - Cannot create image index because at least one of the provided images' manifest is invalid. - Не удается создать индекс образа, так как по крайней мере один из предоставленных манифестов изображений недопустим. - - - - Cannot create image index because provided images are invalid. Items must have 'Config', 'Manifest', 'ManifestMediaType' and 'ManifestDigest' metadata. - Не удается создать индекс образа, так как предоставленные образы недопустимы. Элементы должны иметь метаданные "Config", "Manifest", "ManifestMediaType" и "ManifestDigest". - - - - CONTAINER2005: The inferred image name '{0}' contains entirely invalid characters. The valid characters for an image name are alphanumeric characters, -, /, or _, and the image name must start with an alphanumeric character. - CONTAINER2005: Предполагаемое имя изображения "{0}" содержит совершенно недопустимые символы. Допустимыми символами для имени изображения являются буквенно-цифровые символы, -, / или _, а имя изображения должно начинаться с буквенно-цифрового символа. - {StrBegins="CONTAINER2005: "} - - - CONTAINER2005: The first character of the image name '{0}' must be a lowercase letter or a digit and all characters in the name must be an alphanumeric character, -, /, or _. - CONTAINER2005: Первый символ имени изображения "{0}" должен быть строчной буквой или цифрой, а все символы в имени должны быть буквенно-цифровым символом, -, / или _. - {StrBegins="CONTAINER2005: "} + + CONTAINER2005: The first character of the image name must be a lowercase letter or a digit. + CONTAINER2005: первый символ имени изображения должен быть строчной буквой или цифрой. + {StrBegin="CONTAINER2005: "} CONTAINER2017: A ContainerPort item was provided with an invalid port number '{0}'. ContainerPort items must have an Include value that is an integer, and a Type value that is either 'tcp' or 'udp'. CONTAINER2017: элемент ContainerPort предоставлен с недопустимым номером порта "{0}". Элементы ContainerPort должны иметь значение Include, являющееся целым числом, и значением типа "tcp" или "udp". - {StrBegins="CONTAINER2017: "} + {StrBegin="CONTAINER2017: "} CONTAINER2017: A ContainerPort item was provided with an invalid port number '{0}' and an invalid port type '{1}'. ContainerPort items must have an Include value that is an integer, and a Type value that is either 'tcp' or 'udp'. CONTAINER2017: элемент ContainerPort был предоставлен с недопустимым номером порта "{0}" и недопустимым типом порта "{1}". Элементы ContainerPort должны иметь значение Include, которое является числом, и значение типа "tcp" или "udp". - {StrBegins="CONTAINER2017: "} + {StrBegin="CONTAINER2017: "} CONTAINER2017: A ContainerPort item was provided with an invalid port type '{0}'. ContainerPort items must have an Include value that is an integer, and a Type value that is either 'tcp' or 'udp'. CONTAINER2017: элемент ContainerPort предоставлен с недопустимым типом порта "{0}". Элементы ContainerPort должны иметь значение Include, являющееся целым числом, и значением типа "tcp" или "udp". - {StrBegins="CONTAINER2017: "} + {StrBegin="CONTAINER2017: "} CONTAINER2018: Invalid SDK prerelease version '{0}' - only 'rc' and 'preview' are supported. CONTAINER2018: недопустимая предварительная версия SDK "{0}" — поддерживаются только "rc" и "preview". - {StrBegins="CONTAINER2018: "} + {StrBegin="CONTAINER2018: "} CONTAINER2019: Invalid SDK semantic version '{0}'. CONTAINER2019: недопустимая семантическая версия SDK "{0}". - {StrBegins="CONTAINER2019: "} + {StrBegin="CONTAINER2019: "} CONTAINER2007: Invalid {0} provided: {1}. Image tags must be alphanumeric, underscore, hyphen, or period. CONTAINER2007: предоставлен недопустимый {0}: {1}. В качестве тегов изображений допускаются буквы, цифры, символы подчеркивания, дефисы и точки. - {StrBegins="CONTAINER2007: "} + {StrBegin="CONTAINER2007: "} CONTAINER2010: Invalid {0} provided: {1}. {0} must be a semicolon-delimited list of valid image tags. Image tags must be alphanumeric, underscore, hyphen, or period. CONTAINER2010: предоставлен недопустимый {0}: {1}. {0} должен быть списком допустимых тегов изображений, разделенных точкой с запятой. В качестве тегов изображений допускаются буквы, цифры, символы подчеркивания, дефисы и точки. - {StrBegins="CONTAINER2010: "} - - - Invalid string[] TargetRuntimeIdentifiers. Either all should be 'linux-musl' or none. - Недопустимая строка[] TargetRuntimeIdentifiers. Все должны быть либо "linux-маскали", либо нет. - + {StrBegin="CONTAINER2010: "} CONTAINER1003: Token response had neither token nor access_token. CONTAINER1003: ответ токена не содержит маркера и access_token. - {StrBegins="CONTAINER1003: "} + {StrBegin="CONTAINER1003: "} CONTAINER4005: Item '{0}' contains items without metadata 'Value', and they will be ignored. CONTAINER4005: элемент "{0}" содержит элементы без метаданных "Value", и они будут пропущены. - {StrBegins="CONTAINER4005: "} + {StrBegin="CONTAINER4005: "} CONTAINER1012: The local registry is not available, but pushing to a local registry was requested. - CONTAINER1012: локальный реестр недоступен, но была запрошена отправка в локальный реестр. - {StrBegins="CONTAINER1012: "} + CONTAINER1012: The local registry is not available, but pushing to a local registry was requested. + {StrBegin="CONTAINER1012: "} Error while reading daemon config: {0} @@ -307,22 +225,17 @@ CONTAINER2004: Unable to download layer with descriptor '{0}' from registry '{1}' because it does not exist. CONTAINER2004: не удается скачать слой с дескриптором "{0}" из реестра "{1}", так как он не существует. - {StrBegins="CONTAINER2004: "} + {StrBegin="CONTAINER2004: "} CONTAINER2016: ContainerPort item '{0}' does not specify the port number. Please ensure the item's Include is a port number, for example '<ContainerPort Include="80" />' CONTAINER2016: элемент ContainerPort "{0}" не указывает номер порта. Убедитесь, что include элемента является номером порта, например "<ContainerPort Include="80" />" - {StrBegins="CONTAINER2016: "} - - - 'mediaType' of manifests should be the same in image index. - MediaType манифестов должен быть одинаковым в индексе образа. - + {StrBegin="CONTAINER2016: "} CONTAINER1004: No RequestUri specified. CONTAINER1004: не указан RequestUri. - {StrBegins="CONTAINER1004: "} + {StrBegin="CONTAINER1004: "} '{0}' was not a valid container image name, it was normalized to '{1}' @@ -332,27 +245,17 @@ CONTAINER2011: {0} '{1}' does not exist CONTAINER2011: {0} "{1}" не существует - {StrBegins="CONTAINER2011: "} - - - CONTAINER1017: Unable to communicate with the registry '{0}'. - CONTAINER1017: не удается связаться с реестром "{0}". - {StrBegins="CONTAINER1017:" } + {StrBegin="CONTAINER2011: "} CONTAINER1013: Failed to push to the output registry: {0} CONTAINER1013: не удалось отправить в выходной реестр: {0} - {StrBegins="CONTAINER1013: "} - - - CONTAINER1014: Manifest pull failed. - CONTAINER1014: сбой вытягивания манифеста. - {StrBegins="CONTAINER1014: "} + {StrBegin="CONTAINER1013: "} - CONTAINER1005: Registry push failed; received status code '{0}'. - CONTAINER1005: сбой отправки реестра, получен код состояния "{0}". - {StrBegins="CONTAINER1005: "} + CONTAINER1005: Registry push failed. + CONTAINER1005: сбой передачи реестра. + {StrBegin="CONTAINER1005: "} Uploading config to registry at blob '{0}', @@ -399,70 +302,45 @@ Отправлен тег "{0}" в "{1}". {1} is the registry name - - CONTAINER1015: Unable to access the repository '{0}' at tag '{1}' in the registry '{2}'. Please confirm that this name and tag are present in the registry. - CONTAINER1015: не удается получить доступ к репозиторию "{0}" тега "{1}" в реестре "{2}". Подтвердите, что это имя и тег присутствуют в реестре. - {StrBegins="CONTAINER1015: "} - CONTAINER4003: Required '{0}' items contain empty items. CONTAINER4003: необходимые "{0}" содержат пустые элементы. - {StrBegins="CONTAINER4003: "} + {StrBegin="CONTAINER4003: "} CONTAINER4002: Required '{0}' items were not set. CONTAINER4002: обязательные "{0}" не установлены. - {StrBegins="CONTAINER4002: "} + {StrBegin="CONTAINER4002: "} CONTAINER4001: Required property '{0}' was not set or empty. CONTAINER4001: обязательное свойство "{0}" не установлено или пусто. - {StrBegins="CONTAINER4001: "} + {StrBegin="CONTAINER4001: "} CONTAINER1006: Too many retries, stopping. CONTAINER1006: слишком много повторных попыток, остановка. - {StrBegins="CONTAINER1006: "} - - - CONTAINER1016: Unable to access the repository '{0}' in the registry '{1}'. Please confirm your credentials are correct and that you have access to this repository and registry. - CONTAINER1016: не удается получить доступ к репозиторию "{0}" в реестре "{1}". Убедитесь, что ваши учетные данные верны и что у вас есть доступ к этому репозиторию и реестру. - {StrBegins="CONTAINER1016:" } - - - CONTAINER1018: Unable to download image from the repository '{0}'. - CONTAINER1018: не удалось скачать образ из репозитория "{0}". - {StrBegins="CONTAINER1018:" } + {StrBegin="CONTAINER1006: "} CONTAINER2021: Unknown AppCommandInstruction '{0}'. Valid instructions are {1}. - CONTAINER2021: неизвестный элемент AppCommandInstruction "{0}". Допустимые инструкции: {1}. - {StrBegins="CONTAINER2021: "} + CONTAINER2021: Unknown AppCommandInstruction '{0}'. Valid instructions are {1}. + {StrBegin="CONTAINER2021: "} CONTAINER2002: Unknown local registry type '{0}'. Valid local container registry types are {1}. - CONTAINER2002: неизвестный тип локального реестра "{0}". Допустимыми типами реестра локального контейнера являются {1}. - {StrBegins="CONTAINER2002: "} + CONTAINER2002: Unknown local registry type '{0}'. Valid local container registry types are {1}. + {StrBegin="CONTAINER2002: "} CONTAINER2003: The manifest for {0}:{1} from registry {2} was an unknown type: {3}. Please raise an issue at https://github.com/dotnet/sdk-container-builds/issues with this message. CONTAINER2003: манифест для {0}:{1} из реестра{2} был неизвестного типа: {3}. Создайте вопрос на https://github.com/dotnet/sdk-container-builds/issues с этим сообщением. - {StrBegins="CONTAINER2003: "} + {StrBegin="CONTAINER2003: "} CONTAINER2001: Unrecognized mediaType '{0}'. CONTAINER2001: нераспознанный тип мультимедиа "{0}". - {StrBegins="CONTAINER2001: "} - - - Cannot create image index for the provided 'mediaType' = '{0}'. - Не удается создать индекс образа для указанного "mediaType" = '{0}'. - - - - Unable to create tarball for mediaType '{0}'. - Не удалось создать tarball для mediaType "{0}". - + {StrBegin="CONTAINER2001: "} CONTAINER0000: Value for unit test {0} diff --git a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.tr.xlf b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.tr.xlf index faad0ded04f1..d53c44daa45e 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.tr.xlf +++ b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.tr.xlf @@ -5,73 +5,61 @@ CONTAINER1002: Request to Amazon Elastic Container Registry failed prematurely. This is often caused when the target repository does not exist in the registry. CONTAINER1002: Amazon Elastic Container Registry isteği zamanından önce başarısız oldu. Bu durum genellikle hedef depo, kayıt defterinde bulunmadığında ortaya çıkar. - {StrBegins="CONTAINER1002: "} + {StrBegin="CONTAINER1002: "} CONTAINER2008: Both {0} and {1} were provided, but only one or the other is allowed. CONTAINER2008: {0} ve {1} sağlandı, ancak yalnızca birine veya diğerine izin verilir. - {StrBegins="CONTAINER2008: "} + {StrBegin="CONTAINER2008: "} CONTAINER2025: ContainerAppCommandArgs are provided without specifying a ContainerAppCommand. - CONTAINER2025: ContainerAppCommandArgs bir ContainerAppCommand komutu belirtilmeden sağlanır. - {StrBegins="CONTAINER2025: "} + CONTAINER2025: ContainerAppCommandArgs are provided without specifying a ContainerAppCommand. + {StrBegin="CONTAINER2025: "} CONTAINER2026: ContainerAppCommand and ContainerAppCommandArgs must be empty when ContainerAppCommandInstruction is '{0}'. - CONTAINER2026: ContainerAppCommandInstruction '{0}' olduğunda ContainerAppCommand ve ContainerAppCommandArgs komutları boş olmalıdır. - {StrBegins="CONTAINER2026: "} - - - local archive at '{0}' - '{0}' konumundaki yerel arşiv - {0} is the path to the file written + CONTAINER2026: ContainerAppCommand and ContainerAppCommandArgs must be empty when ContainerAppCommandInstruction is '{0}'. + {StrBegin="CONTAINER2026: "} CONTAINER2022: The base image has an entrypoint that will be overwritten to start the application. Set ContainerAppCommandInstruction to 'Entrypoint' if this is desired. To preserve the base image entrypoint, set ContainerAppCommandInstruction to 'DefaultArgs'. - CONTAINER2022: Temel görüntünün, uygulamayı başlatmak için üzerine yazılacak bir giriş noktası var. İsteniyorsa ContainerAppCommandInstruction komutunu 'Entrypoint' olarak ayarlayın. Temel görüntü giriş noktasını korumak için ContainerAppCommandInstruction komutunu 'DefaultArgs' olarak ayarlayın. - {StrBegins="CONTAINER2022: "} + CONTAINER2022: The base image has an entrypoint that will be overwritten to start the application. Set ContainerAppCommandInstruction to 'Entrypoint' if this is desired. To preserve the base image entrypoint, set ContainerAppCommandInstruction to 'DefaultArgs'. + {StrBegin="CONTAINER2022: "} CONTAINER2009: Could not parse {0}: {1} CONTAINER2009: {0} ayrıştırılamadı: {1} - {StrBegins="CONTAINER2009: "} + {StrBegin="CONTAINER2009: "} CONTAINER2020: {0} does not specify a registry and will be pulled from Docker Hub. Please prefix the name with the image registry, for example: '{1}/<image>'. - CONTAINER2020: {0} kayıt defteri belirtmiyor ve Docker Hub’dan çekilecek. Lütfen adı görüntü kayıt defterine önek olarak ekleyin, örneğin: '{1}/<image>'. - {StrBegins="CONTAINER2020: "} + CONTAINER2020: {0} does not specify a registry and will be pulled from Docker Hub. Please prefix the name with the image registry, for example: '{1}/<image>'. + {StrBegin="CONTAINER2020: "} CONTAINER2013: {0} had spaces in it, replacing with dashes. CONTAINER2013: {0} boşluklar içeriyor ve çizgilerle değiştiriliyor. - {StrBegins="CONTAINER2013: "} + {StrBegin="CONTAINER2013: "} CONTAINER1011: Couldn't find matching base image for {0} that matches RuntimeIdentifier {1}. CONTAINER1011: RuntimeIdentifier {1} ile eşleşen {0} için eşleşen temel görüntü bulunamadı. - {StrBegins="CONTAINER1011: "} + {StrBegin="CONTAINER1011: "} CONTAINER1001: Failed to upload blob using {0}; received status code '{1}'. CONTAINER1001: Blob, {0} kullanarak karşıya yüklenemedi; '{1}' durum kodu alındı. - {StrBegins="CONTAINER1001: "} - - - Building image index '{0}' on top of manifests {1}. - Bildirim listesi '{0}' görüntü dizini {1}. - - {0} is the name of the image index and its tag, {1} is the list of manifests digests - + {StrBegin="CONTAINER1001: "} - Pushed image '{0}' to {1}. - '{0}' görüntüsü {1} konumuna gönderildi. + Pushed container '{0}' to Docker daemon. + '{0}' kapsayıcısı, Docker daemon'a gönderildi. - Pushed image '{0}' to registry '{1}'. - '{0}' görüntüsü '{1}' kayıt defterine gönderildi. + Pushed container '{0}' to registry '{1}'. + '{0}' kapsayıcısı, '{1}' kayıt defterine gönderildi. @@ -79,220 +67,150 @@ '{1}' etiketli '{0}' resmi, '{2}' temel görüntüsünün üzerinde oluşturuluyor. - - Building image '{0}' for runtime identifier '{1}' on top of base image '{2}'. - '{0}' için '{1}' çalışma zamanı tanımlayıcısı için '{2}' temel görüntüsünün üstünde görüntü oluşturuluyor. - - CONTAINER1007: Could not deserialize token from JSON. CONTAINER1007: Belirteç, JSON'dan seri durumdan çıkarılamadı. - {StrBegins="CONTAINER1007: "} + {StrBegin="CONTAINER1007: "} CONTAINER2012: Could not recognize registry '{0}'. CONTAINER2012: '{0}' kayıt defteri tanınamadı. - {StrBegins="CONTAINER2012: "} - - - local registry via '{0}' - '{0}' aracılığıyla yerel kayıt defteri - {0} is the command used + {StrBegin="CONTAINER2012: "} CONTAINER3002: Failed to get docker info({0})\n{1}\n{2} CONTAINER3002: Docker bilgileri alınamadı ({0})\n{1}\n{2} - {StrBegins="CONTAINER3002: "} + {StrBegin="CONTAINER3002: "} CONTAINER3002: Failed to get docker info: {0} CONTAINER3002: Docker bilgileri alınamadı: {0} - {StrBegins="CONTAINER3002: "} + {StrBegin="CONTAINER3002: "} - + CONTAINER3001: Failed creating {0} process. - CONTAINER3001: {0} işlemi oluşturulamadı. + CONTAINER3001: Failed creating {0} process. CONTAINER3001: {0} is the name of the command we failed to run, usually 'docker' or 'podman'. CONTAINER4006: Property '{0}' is empty or contains whitespace and will be ignored. CONTAINER4006: '{0}' özelliği boş veya boşluk içeriyor ve yoksayılacak. - {StrBegins="CONTAINER4006: "} + {StrBegin="CONTAINER4006: "} CONTAINER4004: Items '{0}' contain empty item(s) which will be ignored. CONTAINER4004: '{0}' öğeleri yoksayılacak boş öğeler içeriyor. - {StrBegins="CONTAINER4004: "} + {StrBegin="CONTAINER4004: "} CONTAINER2023: A ContainerEntrypoint and ContainerAppCommandArgs are provided. ContainerAppInstruction must be set to configure how the application is started. Valid instructions are {0}. - CONTAINER2023: ContainerEntrypoint ve ContainerAppCommandArgs sağlandı. Uygulamanın nasıl başlatılacağını yapılandırmak için ContainerAppInstruction komutu ayarlanmalıdır. Geçerli yönergeler {0}. - {StrBegins="CONTAINER2023: "} + CONTAINER2023: A ContainerEntrypoint and ContainerAppCommandArgs are provided. ContainerAppInstruction must be set to configure how the application is started. Valid instructions are {0}. + {StrBegin="CONTAINER2023: "} CONTAINER2027: A ContainerEntrypoint is provided. ContainerAppInstruction must be set to configure how the application is started. Valid instructions are {0}. - CONTAINER2027: ContainerEntrypoint sağlandı. Uygulamanın nasıl başlatılacağını yapılandırmak için ContainerAppInstruction komutu ayarlanmalıdır. Geçerli yönergeler {0}. - {StrBegins="CONTAINER2027: "} + CONTAINER2027: A ContainerEntrypoint is provided. ContainerAppInstruction must be set to configure how the application is started. Valid instructions are {0}. + {StrBegin="CONTAINER2027: "} CONTAINER2024: ContainerEntrypointArgs are provided without specifying a ContainerEntrypoint. - CONTAINER2024: ContainerEntrypointArgs bir ContainerEntrypoint belirtilmeden sağlanır. - {StrBegins="CONTAINER2024: "} + CONTAINER2024: ContainerEntrypointArgs are provided without specifying a ContainerEntrypoint. + {StrBegin="CONTAINER2024: "} CONTAINER2029: ContainerEntrypointArgsSet are provided. Change to use ContainerAppCommandArgs for arguments that must always be set, or ContainerDefaultArgs for arguments that can be overridden when the container is created. - CONTAINER2029: ContainerEntrypointArgsSet sağlandı. Her zaman ayarlanması gereken bağımsız değişkenler için ContainerAppCommandArgs komutu veya kapsayıcı oluşturulduğunda geçersiz kılınabilen bağımsız değişkenler için ContainerDefaultArgs komutu kullanılacak şekilde değiştirin. - {StrBegins="CONTAINER2029: "} + CONTAINER2029: ContainerEntrypointArgsSet are provided. Change to use ContainerAppCommandArgs for arguments that must always be set, or ContainerDefaultArgs for arguments that can be overridden when the container is created. + {StrBegin="CONTAINER2029: "} CONTAINER2028: ContainerEntrypoint can not be combined with ContainerAppCommandInstruction '{0}'. - CONTAINER2028: ContainerEntrypoint komutu ContainerAppCommandInstruction '{0}' ile birleştirilemez. - {StrBegins="CONTAINER2028: "} + CONTAINER2028: ContainerEntrypoint can not be combined with ContainerAppCommandInstruction '{0}'. + {StrBegin="CONTAINER2028: "} CONTAINER1008: Failed retrieving credentials for "{0}": {1} CONTAINER1008: "{0}" için kimlik bilgileri alınamadı: {1} - {StrBegins="CONTAINER1008: "} - - - CONTAINER2030: GenerateLabels was disabled but GenerateDigestLabel was enabled - no digest label will be created. - CONTAINER2030: GenerateLabels devre dışı bırakıldı ancak GenerateDigestLabel etkinleştirildi; özet etiketi oluşturulmayacak. - {StrBegins="CONTAINER2030: "} + {StrBegin="CONTAINER1008: "} No host object detected. Ana bilgisayar nesnesi algılanmadı. - - Cannot create image index because at least one of the provided images' config is missing 'architecture'. - Sağlanan görüntülerden en az biri 'architecture' eksik olduğundan görüntü dizini oluşturulamıyor. - - - - Cannot create image index because at least one of the provided images' config is missing 'os'. - Sağlanan görüntülerden en az biri 'os' eksik olduğundan görüntü dizini oluşturulamıyor. - - - - Pushed image index '{0}' to registry '{1}'. - '{0}' görüntü dizini '{1}' kayıt defterine gönderildi. - - - - Image index creation for Podman is not supported. - Podman için görüntü dizini oluşturma desteklenmiyor. - - CONTAINER1009: Failed to load image from local registry. stdout: {0} - CONTAINER1009: Görüntü yerel kayıt defterinden yüklenemedi. stdout: {0} - {StrBegins="CONTAINER1009: "} - - - CONTAINER1020: Failed to load image because containerd image store is not enabled for Docker. Tip: You can enable it by checking 'Use containerd for pulling and storing images' in Docker Desktop settings. - CONTAINER1020: Kapsayıcılı görüntü deposu Docker için etkinleştirilmediğinden görüntü yüklenemedi. İpucu: Docker Desktop ayarlarında 'Görüntüleri çekmek ve depolamak için kapsayıcılı kullan' ayarını kontrol edip etkinleştirebilirsiniz. - {StrBegin="CONTAINER1020: "} + CONTAINER1009: Görüntü yerel Docker daemon'a yüklenemedi. stdout: {0} + {StrBegin="CONTAINER1009: "} CONTAINER1010: Pulling images from local registry is not supported. - CONTAINER1010: Yerel kayıt defterinden görüntü çekme desteklenmiyor. - {StrBegins="CONTAINER1010: "} + CONTAINER1010: Pulling images from local registry is not supported. + {StrBegin="CONTAINER1010: "} - - Cannot create image index because no images were provided. - Görüntü dizini oluşturulamıyor çünkü görüntü sağlanmadı. - - - - CONTAINER2031: The container image format '{0}' is not supported. Supported formats are '{1}'. - CONTAINER2031: Kapsayıcı görüntü '{0}' biçimi desteklenmiyor. Desteklenen biçimler '{1}'. - {StrBegins="CONTAINER2031: "} + + CONTAINER2014: Invalid {0}: {1}. + CONTAINER2014: Geçersiz {0}: {1}. + {StrBegin="CONTAINER2014: "} CONTAINER2015: {0}: '{1}' was not a valid Environment Variable. Ignoring. CONTAINER2015: {0}: '{1}' geçerli bir Ortam Değişkeni değildi. Görmezden geliniyor. - {StrBegins="CONTAINER2015: "} + {StrBegin="CONTAINER2015: "} - - Cannot create image index because at least one of the provided images' config is invalid. - Sağlanan görüntülerin yapılandırmasından en az biri geçersiz olduğundan görüntü dizini oluşturulamıyor. - - - - Cannot create image index because at least one of the provided images' manifest is invalid. - Sağlanan görüntülerin bildiriminden en az biri geçersiz olduğundan görüntü dizini oluşturulamıyor. - - - - Cannot create image index because provided images are invalid. Items must have 'Config', 'Manifest', 'ManifestMediaType' and 'ManifestDigest' metadata. - Sağlanan görüntüler geçersiz olduğundan görüntü dizini oluşturulamıyor. Öğeler 'Config', 'Manifest', 'ManifestMediaType' ve 'ManifestDigest' meta verilerine sahip olmalıdır. - - - - CONTAINER2005: The inferred image name '{0}' contains entirely invalid characters. The valid characters for an image name are alphanumeric characters, -, /, or _, and the image name must start with an alphanumeric character. - CONTAINER2005: Çıkarsanan '{0}' görüntü adı tamamen geçersiz karakterler içeriyor. Bir görüntü adında geçerli karakterler şunlardan oluşur: alfasayısal karakterler, -, /, veya _. Görüntü adı alfasayısal karakterle başlamalıdır. - {StrBegins="CONTAINER2005: "} - - - CONTAINER2005: The first character of the image name '{0}' must be a lowercase letter or a digit and all characters in the name must be an alphanumeric character, -, /, or _. - CONTAINER2005: '{0}' görüntü adındaki ilk karakter bir küçük harf veya rakam olmalıdır ve addaki tüm karakterler şunlardan oluşmalıdır: alfasayısal karakter, -, /, veya _. - {StrBegins="CONTAINER2005: "} + + CONTAINER2005: The first character of the image name must be a lowercase letter or a digit. + CONTAINER2005: Görüntü adının ilk karakteri küçük harf veya rakam olmalıdır. + {StrBegin="CONTAINER2005: "} CONTAINER2017: A ContainerPort item was provided with an invalid port number '{0}'. ContainerPort items must have an Include value that is an integer, and a Type value that is either 'tcp' or 'udp'. CONTAINER2017: Geçersiz bağlantı noktası numarasına ('{0}') sahip bir ContainerPort öğesi sağlandı. ContainerPort öğeleri, tamsayı olan bir Include değerine ve 'tcp' veya 'udp' olan bir Type değerine sahip olmalıdır. - {StrBegins="CONTAINER2017: "} + {StrBegin="CONTAINER2017: "} CONTAINER2017: A ContainerPort item was provided with an invalid port number '{0}' and an invalid port type '{1}'. ContainerPort items must have an Include value that is an integer, and a Type value that is either 'tcp' or 'udp'. CONTAINER2017: Geçersiz bağlantı noktası numarasına ('{0}') ve geçersiz bağlantı noktası türüne ('{1}') sahip olan bir ContainerPort öğesi sağlandı. ContainerPort öğeleri, tamsayı olan bir Include değerine ve 'tcp' veya 'udp' olan bir Type değerine sahip olmalıdır. - {StrBegins="CONTAINER2017: "} + {StrBegin="CONTAINER2017: "} CONTAINER2017: A ContainerPort item was provided with an invalid port type '{0}'. ContainerPort items must have an Include value that is an integer, and a Type value that is either 'tcp' or 'udp'. CONTAINER2017: Geçersiz bağlantı noktası türüne ('{0}') sahip bir ContainerPort öğesi sağlandı. ContainerPort öğeleri, tamsayı olan bir Include değerine ve 'tcp' veya 'udp' olan bir Type değerine sahip olmalıdır. - {StrBegins="CONTAINER2017: "} + {StrBegin="CONTAINER2017: "} CONTAINER2018: Invalid SDK prerelease version '{0}' - only 'rc' and 'preview' are supported. CONTAINER2018: Geçersiz SDK ön sürümü ('{0}') : yalnızca 'rc' ve 'preview' destekleniyor. - {StrBegins="CONTAINER2018: "} + {StrBegin="CONTAINER2018: "} CONTAINER2019: Invalid SDK semantic version '{0}'. CONTAINER2019: Geçersiz SDK anlamsal sürümü '{0}'. - {StrBegins="CONTAINER2019: "} + {StrBegin="CONTAINER2019: "} CONTAINER2007: Invalid {0} provided: {1}. Image tags must be alphanumeric, underscore, hyphen, or period. CONTAINER2007: Geçersiz {0} sağlandı: {1}. Görüntü etiketleri alfasayısal, alt çizgi, kısa çizgi veya nokta olmalıdır. - {StrBegins="CONTAINER2007: "} + {StrBegin="CONTAINER2007: "} CONTAINER2010: Invalid {0} provided: {1}. {0} must be a semicolon-delimited list of valid image tags. Image tags must be alphanumeric, underscore, hyphen, or period. CONTAINER2010: Geçersiz {0} sağlandı: {1}. {0}, geçerli resim etiketlerinin noktalı virgülle ayrılmış listesi olmalıdır. Resim etiketleri alfasayısal, alt çizgi, kısa çizgi veya nokta olmalıdır. - {StrBegins="CONTAINER2010: "} - - - Invalid string[] TargetRuntimeIdentifiers. Either all should be 'linux-musl' or none. - Geçersiz string[] TargetRuntimeIdentifiers. Tümü 'linux-musl' veya hiçbiri olmalıdır. - + {StrBegin="CONTAINER2010: "} CONTAINER1003: Token response had neither token nor access_token. CONTAINER1003: Belirteç yanıtında belirteç veya access_token yok. - {StrBegins="CONTAINER1003: "} + {StrBegin="CONTAINER1003: "} CONTAINER4005: Item '{0}' contains items without metadata 'Value', and they will be ignored. CONTAINER4005: '{0}' öğesi 'Value' meta verileri olmayan öğeler içeriyor ve yoksayılacak. - {StrBegins="CONTAINER4005: "} + {StrBegin="CONTAINER4005: "} CONTAINER1012: The local registry is not available, but pushing to a local registry was requested. - CONTAINER1012: Yerel kayıt defteri kullanılamıyor, ancak yerel kayıt defterine gönderim isteniyor. - {StrBegins="CONTAINER1012: "} + CONTAINER1012: The local registry is not available, but pushing to a local registry was requested. + {StrBegin="CONTAINER1012: "} Error while reading daemon config: {0} @@ -307,22 +225,17 @@ CONTAINER2004: Unable to download layer with descriptor '{0}' from registry '{1}' because it does not exist. CONTAINER2004: '{0}' tanımlayıcısına sahip katman mevcut olmadığından '{1}' kayıt defterinden indirilemedi. - {StrBegins="CONTAINER2004: "} + {StrBegin="CONTAINER2004: "} CONTAINER2016: ContainerPort item '{0}' does not specify the port number. Please ensure the item's Include is a port number, for example '<ContainerPort Include="80" />' CONTAINER2016: ContainerPort öğesi ('{0}'), bağlantı noktası numarasını belirtmiyor. Lütfen öğenin Include değerinin bir bağlantı noktası numarası olduğundan emin olun, örneğin '<ContainerPort Include="80" />' - {StrBegins="CONTAINER2016: "} - - - 'mediaType' of manifests should be the same in image index. - Bildirimlerin 'mediaType' değeri görüntü dizininde aynı olmalıdır. - + {StrBegin="CONTAINER2016: "} CONTAINER1004: No RequestUri specified. CONTAINER1004: RequestUri belirtilmedi. - {StrBegins="CONTAINER1004: "} + {StrBegin="CONTAINER1004: "} '{0}' was not a valid container image name, it was normalized to '{1}' @@ -332,27 +245,17 @@ CONTAINER2011: {0} '{1}' does not exist CONTAINER2011: {0} '{1}' yok - {StrBegins="CONTAINER2011: "} - - - CONTAINER1017: Unable to communicate with the registry '{0}'. - CONTAINER1017: '{0}' kayıt defteriyle iletişim kurulamıyor. - {StrBegins="CONTAINER1017:" } + {StrBegin="CONTAINER2011: "} CONTAINER1013: Failed to push to the output registry: {0} CONTAINER1013: Çıkış kayıt defterine gönderilemedi: {0} - {StrBegins="CONTAINER1013: "} - - - CONTAINER1014: Manifest pull failed. - CONTAINER1014: Bildirim çekme başarısız oldu. - {StrBegins="CONTAINER1014: "} + {StrBegin="CONTAINER1013: "} - CONTAINER1005: Registry push failed; received status code '{0}'. - CONTAINER1005: Kayıt defteri gönderilemedi; '{0}' durum kodu alındı. - {StrBegins="CONTAINER1005: "} + CONTAINER1005: Registry push failed. + CONTAINER1005: Kayıt defteri gönderilemedi. + {StrBegin="CONTAINER1005: "} Uploading config to registry at blob '{0}', @@ -399,70 +302,45 @@ '{0}' etiketi, '{1}' konumuna karşıya yüklendi. {1} is the registry name - - CONTAINER1015: Unable to access the repository '{0}' at tag '{1}' in the registry '{2}'. Please confirm that this name and tag are present in the registry. - CONTAINER1015: '{2}' kayıt defterindeki '{1}' etiketinde bulunan '{0}' deposuna erişilemiyor. Lütfen bu ad ve etiketin kayıt defterinde mevcut olduğunu onaylayın. - {StrBegins="CONTAINER1015: "} - CONTAINER4003: Required '{0}' items contain empty items. CONTAINER4003: Gerekli '{0}' öğeleri boş öğeler içeriyor. - {StrBegins="CONTAINER4003: "} + {StrBegin="CONTAINER4003: "} CONTAINER4002: Required '{0}' items were not set. CONTAINER4002: Gerekli '{0}' öğe ayarlanmadı. - {StrBegins="CONTAINER4002: "} + {StrBegin="CONTAINER4002: "} CONTAINER4001: Required property '{0}' was not set or empty. CONTAINER4001: Gerekli '{0}' özelliği ayarlanmadı veya boş değil. - {StrBegins="CONTAINER4001: "} + {StrBegin="CONTAINER4001: "} CONTAINER1006: Too many retries, stopping. CONTAINER1006: Çok fazla yeniden deneme, durduruluyor. - {StrBegins="CONTAINER1006: "} - - - CONTAINER1016: Unable to access the repository '{0}' in the registry '{1}'. Please confirm your credentials are correct and that you have access to this repository and registry. - CONTAINER1016: '{1}' kayıt defterindeki '{0}' deposuna erişilemedi. Lütfen kimlik bilgilerinizin doğru olduğunu ve bu depoya ve kayıt defterine erişiminiz olduğunu onaylayın. - {StrBegins="CONTAINER1016:" } - - - CONTAINER1018: Unable to download image from the repository '{0}'. - CONTAINER1018: '{0}' deposundan görüntü indirilemedi. - {StrBegins="CONTAINER1018:" } + {StrBegin="CONTAINER1006: "} CONTAINER2021: Unknown AppCommandInstruction '{0}'. Valid instructions are {1}. - CONTAINER2021: AppCommandInstruction '{0}' bilinmiyor. Geçerli yönergeler {1}. - {StrBegins="CONTAINER2021: "} + CONTAINER2021: Unknown AppCommandInstruction '{0}'. Valid instructions are {1}. + {StrBegin="CONTAINER2021: "} CONTAINER2002: Unknown local registry type '{0}'. Valid local container registry types are {1}. - CONTAINER2002: '{0}' yerel kayıt defteri türü bilinmiyor. Geçerli yerel kayıt defteri türleri {1}. - {StrBegins="CONTAINER2002: "} + CONTAINER2002: Unknown local registry type '{0}'. Valid local container registry types are {1}. + {StrBegin="CONTAINER2002: "} CONTAINER2003: The manifest for {0}:{1} from registry {2} was an unknown type: {3}. Please raise an issue at https://github.com/dotnet/sdk-container-builds/issues with this message. CONTAINER2003: {2} kayıt defterindeki {0} : {1} bildirimi bilinmeyen bir türdü: {3}. Lütfen bu iletiyle https://github.com/dotnet/sdk-container-builds/issues adresinde bir sorun oluşturun. - {StrBegins="CONTAINER2003: "} + {StrBegin="CONTAINER2003: "} CONTAINER2001: Unrecognized mediaType '{0}'. CONTAINER2001: Tanınmayan mediaType ('{0}'). - {StrBegins="CONTAINER2001: "} - - - Cannot create image index for the provided 'mediaType' = '{0}'. - Sağlanan 'mediaType' = '{0}' için görüntü dizini oluşturulamıyor. - - - - Unable to create tarball for mediaType '{0}'. - mediaType '{0}' için tarball oluşturulamıyor. - + {StrBegin="CONTAINER2001: "} CONTAINER0000: Value for unit test {0} diff --git a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.zh-Hans.xlf b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.zh-Hans.xlf index f75697ff279f..bc70588b8f77 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.zh-Hans.xlf +++ b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.zh-Hans.xlf @@ -5,73 +5,61 @@ CONTAINER1002: Request to Amazon Elastic Container Registry failed prematurely. This is often caused when the target repository does not exist in the registry. CONTAINER1002: 对 Amazon 弹性容器注册表的请求过早失败。当注册表中不存在目标存储库时,通常会导致这种情况。 - {StrBegins="CONTAINER1002: "} + {StrBegin="CONTAINER1002: "} CONTAINER2008: Both {0} and {1} were provided, but only one or the other is allowed. CONTAINER2008: 同时提供了 {0} 和 {1} ,但只允许一个或另一个。 - {StrBegins="CONTAINER2008: "} + {StrBegin="CONTAINER2008: "} CONTAINER2025: ContainerAppCommandArgs are provided without specifying a ContainerAppCommand. - CONTAINER2025: 提供了 ContainerAppCommandArgs,但未指定 ContainerAppCommand。 - {StrBegins="CONTAINER2025: "} + CONTAINER2025: ContainerAppCommandArgs are provided without specifying a ContainerAppCommand. + {StrBegin="CONTAINER2025: "} CONTAINER2026: ContainerAppCommand and ContainerAppCommandArgs must be empty when ContainerAppCommandInstruction is '{0}'. - CONTAINER2026: 当 ContainerAppCommandInstruction 为“{0}”时,ContainerAppCommand 和 ContainerAppCommandArgs 必须为空。 - {StrBegins="CONTAINER2026: "} - - - local archive at '{0}' - 本地存档在“{0}” - {0} is the path to the file written + CONTAINER2026: ContainerAppCommand and ContainerAppCommandArgs must be empty when ContainerAppCommandInstruction is '{0}'. + {StrBegin="CONTAINER2026: "} CONTAINER2022: The base image has an entrypoint that will be overwritten to start the application. Set ContainerAppCommandInstruction to 'Entrypoint' if this is desired. To preserve the base image entrypoint, set ContainerAppCommandInstruction to 'DefaultArgs'. - CONTAINER2022: 基础映像具有一个入口点,该入口点将被覆盖以启动应用程序。如果需要,请将 ContainerAppCommandInstruction 设置为 "Entrypoint"。若要保留基础映像入口点,请将 ContainerAppCommandInstruction 设置为 "DefaultArgs"。 - {StrBegins="CONTAINER2022: "} + CONTAINER2022: The base image has an entrypoint that will be overwritten to start the application. Set ContainerAppCommandInstruction to 'Entrypoint' if this is desired. To preserve the base image entrypoint, set ContainerAppCommandInstruction to 'DefaultArgs'. + {StrBegin="CONTAINER2022: "} CONTAINER2009: Could not parse {0}: {1} CONTAINER2009: 无法分析 {0}: {1} - {StrBegins="CONTAINER2009: "} + {StrBegin="CONTAINER2009: "} CONTAINER2020: {0} does not specify a registry and will be pulled from Docker Hub. Please prefix the name with the image registry, for example: '{1}/<image>'. - CONTAINER2020: {0} 未指定注册表,将从 Docker Hub 中拉取。请在名称前面添加映像注册表,例如: "{1}/<image>"。 - {StrBegins="CONTAINER2020: "} + CONTAINER2020: {0} does not specify a registry and will be pulled from Docker Hub. Please prefix the name with the image registry, for example: '{1}/<image>'. + {StrBegin="CONTAINER2020: "} CONTAINER2013: {0} had spaces in it, replacing with dashes. CONTAINER2013: {0} 中包含空格,替换为短划线。 - {StrBegins="CONTAINER2013: "} + {StrBegin="CONTAINER2013: "} CONTAINER1011: Couldn't find matching base image for {0} that matches RuntimeIdentifier {1}. CONTAINER1011: 找不到与 RuntimeIdentifier 匹配的 {0} 匹配基映像 {1}。 - {StrBegins="CONTAINER1011: "} + {StrBegin="CONTAINER1011: "} CONTAINER1001: Failed to upload blob using {0}; received status code '{1}'. CONTAINER1001: 无法使用 {0} 上传 blob;已收到状态代码“{1}”。 - {StrBegins="CONTAINER1001: "} - - - Building image index '{0}' on top of manifests {1}. - 在清单 {1} 顶部生成图像索引 '{0}'。 - - {0} is the name of the image index and its tag, {1} is the list of manifests digests - + {StrBegin="CONTAINER1001: "} - Pushed image '{0}' to {1}. - 已将图像“{0}”推送到 {1}。 + Pushed container '{0}' to Docker daemon. + 已将容器“{0}”推送到 Docker 守护程序。 - Pushed image '{0}' to registry '{1}'. - 已将映像“{0}”推送到注册表“{1}”。 + Pushed container '{0}' to registry '{1}'. + 已将容器“{0}”推送到注册表“{1}”。 @@ -79,220 +67,150 @@ 在基本映像“{2}”顶部构建标记为“{1}”的映像“{0}”。 - - Building image '{0}' for runtime identifier '{1}' on top of base image '{2}'. - 根据基础映像“{2}”生成运行时标识符“{1}”的映像“{0}”。 - - CONTAINER1007: Could not deserialize token from JSON. CONTAINER1007: 无法从 JSON 反序列化令牌。 - {StrBegins="CONTAINER1007: "} + {StrBegin="CONTAINER1007: "} CONTAINER2012: Could not recognize registry '{0}'. CONTAINER2012: 无法识别注册表“{0}”。 - {StrBegins="CONTAINER2012: "} - - - local registry via '{0}' - 使用“{0}”的本地注册表 - {0} is the command used + {StrBegin="CONTAINER2012: "} CONTAINER3002: Failed to get docker info({0})\n{1}\n{2} CONTAINER3002: 无法获取 docker 信息({0})\n{1}\n{2} - {StrBegins="CONTAINER3002: "} + {StrBegin="CONTAINER3002: "} CONTAINER3002: Failed to get docker info: {0} CONTAINER3002: 无法获取 docker 信息: {0} - {StrBegins="CONTAINER3002: "} + {StrBegin="CONTAINER3002: "} - + CONTAINER3001: Failed creating {0} process. - CONTAINER3001: 未能创建 {0} 进程。 + CONTAINER3001: Failed creating {0} process. CONTAINER3001: {0} is the name of the command we failed to run, usually 'docker' or 'podman'. CONTAINER4006: Property '{0}' is empty or contains whitespace and will be ignored. CONTAINER4006: 属性“{0}”为空或包含空格,将被忽略。 - {StrBegins="CONTAINER4006: "} + {StrBegin="CONTAINER4006: "} CONTAINER4004: Items '{0}' contain empty item(s) which will be ignored. CONTAINER4004: 项“{0}”包含将忽略的空项。 - {StrBegins="CONTAINER4004: "} + {StrBegin="CONTAINER4004: "} CONTAINER2023: A ContainerEntrypoint and ContainerAppCommandArgs are provided. ContainerAppInstruction must be set to configure how the application is started. Valid instructions are {0}. - CONTAINER2023: 提供了 ContainerEntrypoint 和 ContainerAppCommandArgs。必须设置 ContainerAppInstruction 以配置应用程序的启动方式。有效说明为 {0}。 - {StrBegins="CONTAINER2023: "} + CONTAINER2023: A ContainerEntrypoint and ContainerAppCommandArgs are provided. ContainerAppInstruction must be set to configure how the application is started. Valid instructions are {0}. + {StrBegin="CONTAINER2023: "} CONTAINER2027: A ContainerEntrypoint is provided. ContainerAppInstruction must be set to configure how the application is started. Valid instructions are {0}. - CONTAINER2027: 提供了 ContainerEntrypoint。必须设置 ContainerAppInstruction 以配置应用程序的启动方式。有效说明为 {0}。 - {StrBegins="CONTAINER2027: "} + CONTAINER2027: A ContainerEntrypoint is provided. ContainerAppInstruction must be set to configure how the application is started. Valid instructions are {0}. + {StrBegin="CONTAINER2027: "} CONTAINER2024: ContainerEntrypointArgs are provided without specifying a ContainerEntrypoint. - CONTAINER2024: 提供了 ContainerEntrypointArgs,但未指定 ContainerEntrypoint。 - {StrBegins="CONTAINER2024: "} + CONTAINER2024: ContainerEntrypointArgs are provided without specifying a ContainerEntrypoint. + {StrBegin="CONTAINER2024: "} CONTAINER2029: ContainerEntrypointArgsSet are provided. Change to use ContainerAppCommandArgs for arguments that must always be set, or ContainerDefaultArgs for arguments that can be overridden when the container is created. - CONTAINER2029: 提供了 ContainerEntrypointArgsSet。对于必须始终设置的参数,请更改为使用 ContainerAppCommandArgs;对于可在创建容器时替代的参数,请更改为使用 ContainerDefaultArgs。 - {StrBegins="CONTAINER2029: "} + CONTAINER2029: ContainerEntrypointArgsSet are provided. Change to use ContainerAppCommandArgs for arguments that must always be set, or ContainerDefaultArgs for arguments that can be overridden when the container is created. + {StrBegin="CONTAINER2029: "} CONTAINER2028: ContainerEntrypoint can not be combined with ContainerAppCommandInstruction '{0}'. - CONTAINER2028: ContainerEntrypoint 不能与 ContainerAppCommandInstruction“{0}”一起使用。 - {StrBegins="CONTAINER2028: "} + CONTAINER2028: ContainerEntrypoint can not be combined with ContainerAppCommandInstruction '{0}'. + {StrBegin="CONTAINER2028: "} CONTAINER1008: Failed retrieving credentials for "{0}": {1} CONTAINER1008: 检索“{0}”的凭据失败: {1} - {StrBegins="CONTAINER1008: "} - - - CONTAINER2030: GenerateLabels was disabled but GenerateDigestLabel was enabled - no digest label will be created. - CONTAINER2030: 已禁用 GenerateLabels,但已启用 GenerateDigestLabel - 不会创建任何摘要标签。 - {StrBegins="CONTAINER2030: "} + {StrBegin="CONTAINER1008: "} No host object detected. 未检测到主机对象。 - - Cannot create image index because at least one of the provided images' config is missing 'architecture'. - 无法创建映像索引,因为提供的映像配置中至少有一个缺少 “architecture”。 - - - - Cannot create image index because at least one of the provided images' config is missing 'os'. - 无法创建映像索引,因为提供的映像配置中至少有一个缺少 “os”。 - - - - Pushed image index '{0}' to registry '{1}'. - 已将映像索引“{0}”推送到注册表“{1}”。 - - - - Image index creation for Podman is not supported. - 不支持为 Podman 创建映像索引。 - - CONTAINER1009: Failed to load image from local registry. stdout: {0} - CONTAINER1009: 未能从本地注册表加载映像。stdout: {0} - {StrBegins="CONTAINER1009: "} - - - CONTAINER1020: Failed to load image because containerd image store is not enabled for Docker. Tip: You can enable it by checking 'Use containerd for pulling and storing images' in Docker Desktop settings. - CONTAINER1020: 未能加载映像,因为没有为 Docker 启用容器映像存储。提示: 可以通过在 Docker Desktop 设置中检查“使用容器拉取和存储映像”来启用它。 - {StrBegin="CONTAINER1020: "} + CONTAINER1009: 未能将映像加载到本地 Docker 守护程序。stdout: {0} + {StrBegin="CONTAINER1009: "} CONTAINER1010: Pulling images from local registry is not supported. - CONTAINER1010: 不支持从本地注册表拉取映像。 - {StrBegins="CONTAINER1010: "} + CONTAINER1010: Pulling images from local registry is not supported. + {StrBegin="CONTAINER1010: "} - - Cannot create image index because no images were provided. - 无法创建映像索引,因为未提供映像。 - - - - CONTAINER2031: The container image format '{0}' is not supported. Supported formats are '{1}'. - CONTAINER2031: 不支持容器映像格式“{0}”。支持的格式为“{1}”。 - {StrBegins="CONTAINER2031: "} + + CONTAINER2014: Invalid {0}: {1}. + CONTAINER2014: 无效 {0}: {1}。 + {StrBegin="CONTAINER2014: "} CONTAINER2015: {0}: '{1}' was not a valid Environment Variable. Ignoring. CONTAINER2015: {0}: "{1}" 不是有效的环境变量。忽略。 - {StrBegins="CONTAINER2015: "} + {StrBegin="CONTAINER2015: "} - - Cannot create image index because at least one of the provided images' config is invalid. - 无法创建映像索引,因为提供的映像配置中至少有一个无效。 - - - - Cannot create image index because at least one of the provided images' manifest is invalid. - 无法创建映像索引,因为提供的映像清单中至少有一个无效。 - - - - Cannot create image index because provided images are invalid. Items must have 'Config', 'Manifest', 'ManifestMediaType' and 'ManifestDigest' metadata. - 无法创建映像索引,因为提供的映像无效。项必须具有 “Config”、“Manifest”、“ManifestMediaType” 和 “ManifestDigest” 元数据。 - - - - CONTAINER2005: The inferred image name '{0}' contains entirely invalid characters. The valid characters for an image name are alphanumeric characters, -, /, or _, and the image name must start with an alphanumeric character. - CONTAINER2005: 推断的图像名称“{0}”包含完全无效的字符。图像名称的有效字符包括字母数字字符、-、/ 或 _,图像名称必须以字母数字字符开头。 - {StrBegins="CONTAINER2005: "} - - - CONTAINER2005: The first character of the image name '{0}' must be a lowercase letter or a digit and all characters in the name must be an alphanumeric character, -, /, or _. - CONTAINER2005: 图像名称“{0}”的第一个字符必须是小写字母或数字,并且名称中的所有字符都必须是字母数字字符、-、/ 或 _。 - {StrBegins="CONTAINER2005: "} + + CONTAINER2005: The first character of the image name must be a lowercase letter or a digit. + CONTAINER2005: 图像名称的第一个字符必须是小写字母或数字。 + {StrBegin="CONTAINER2005: "} CONTAINER2017: A ContainerPort item was provided with an invalid port number '{0}'. ContainerPort items must have an Include value that is an integer, and a Type value that is either 'tcp' or 'udp'. CONTAINER2017: 为 ContainerPort 项提供了无效的端口数字 "{0}"。ContainerPort 项必须具有作为整数的 Include 值,以及 "tcp" 或 "udp" 的 Type 值。 - {StrBegins="CONTAINER2017: "} + {StrBegin="CONTAINER2017: "} CONTAINER2017: A ContainerPort item was provided with an invalid port number '{0}' and an invalid port type '{1}'. ContainerPort items must have an Include value that is an integer, and a Type value that is either 'tcp' or 'udp'. CONTAINER2017: 为 ContainerPort 项提供了无效的端口号 {0} 和无效的端口类型 "{1}"。ContainerPort 项必须具有作为整数的 Include 值,以及“tcp”或“udp”的 Type 值。 - {StrBegins="CONTAINER2017: "} + {StrBegin="CONTAINER2017: "} CONTAINER2017: A ContainerPort item was provided with an invalid port type '{0}'. ContainerPort items must have an Include value that is an integer, and a Type value that is either 'tcp' or 'udp'. CONTAINER2017: 为 ContainerPort 项提供了无效的端口类型 "{0}"。ContainerPort 项必须具有作为整数的 Include 值,以及 "tcp" 或 "udp" 的 Type 值。 - {StrBegins="CONTAINER2017: "} + {StrBegin="CONTAINER2017: "} CONTAINER2018: Invalid SDK prerelease version '{0}' - only 'rc' and 'preview' are supported. CONTAINER2018: SDK 预发行版本“{0}”无效 - 仅支持“rc”和“preview”。 - {StrBegins="CONTAINER2018: "} + {StrBegin="CONTAINER2018: "} CONTAINER2019: Invalid SDK semantic version '{0}'. CONTAINER2019: SDK 语义版本“{0}”无效。 - {StrBegins="CONTAINER2019: "} + {StrBegin="CONTAINER2019: "} CONTAINER2007: Invalid {0} provided: {1}. Image tags must be alphanumeric, underscore, hyphen, or period. CONTAINER2007: 提供的 {0} 无效: {1}。图像标记必须是字母数字、下划线、连字符或句点。 - {StrBegins="CONTAINER2007: "} + {StrBegin="CONTAINER2007: "} CONTAINER2010: Invalid {0} provided: {1}. {0} must be a semicolon-delimited list of valid image tags. Image tags must be alphanumeric, underscore, hyphen, or period. CONTAINER2010: 提供的 {0} 无效: {1}。{0} 必须是有效图像标记的分号分隔列表。图像标记必须是字母数字、下划线、连字符或句点。 - {StrBegins="CONTAINER2010: "} - - - Invalid string[] TargetRuntimeIdentifiers. Either all should be 'linux-musl' or none. - 无效的 string[] TargetRuntimeIdentifiers。全部应为 “linux-musl” 或无。 - + {StrBegin="CONTAINER2010: "} CONTAINER1003: Token response had neither token nor access_token. CONTAINER1003: 令牌响应既没有令牌,也没有access_token。 - {StrBegins="CONTAINER1003: "} + {StrBegin="CONTAINER1003: "} CONTAINER4005: Item '{0}' contains items without metadata 'Value', and they will be ignored. CONTAINER4005: 项 "{0}" 包含没有元数据 "Value" 的项,它们将被忽略。 - {StrBegins="CONTAINER4005: "} + {StrBegin="CONTAINER4005: "} CONTAINER1012: The local registry is not available, but pushing to a local registry was requested. - CONTAINER1012: 本地注册表不可用,但请求推送到本地注册表。 - {StrBegins="CONTAINER1012: "} + CONTAINER1012: The local registry is not available, but pushing to a local registry was requested. + {StrBegin="CONTAINER1012: "} Error while reading daemon config: {0} @@ -307,22 +225,17 @@ CONTAINER2004: Unable to download layer with descriptor '{0}' from registry '{1}' because it does not exist. CONTAINER2004: 无法从注册表“{1}”下载描述符为“{0}”的层,因为它不存在。 - {StrBegins="CONTAINER2004: "} + {StrBegin="CONTAINER2004: "} CONTAINER2016: ContainerPort item '{0}' does not specify the port number. Please ensure the item's Include is a port number, for example '<ContainerPort Include="80" />' CONTAINER2016: ContainerPort 项“{0}”未指定端口号。请确保项的 Include 是端口号,例如 "<ContainerPort Include="80" />" - {StrBegins="CONTAINER2016: "} - - - 'mediaType' of manifests should be the same in image index. - 清单的 “mediaType” 在映像索引中应相同。 - + {StrBegin="CONTAINER2016: "} CONTAINER1004: No RequestUri specified. CONTAINER1004: 未指定 RequestUri。 - {StrBegins="CONTAINER1004: "} + {StrBegin="CONTAINER1004: "} '{0}' was not a valid container image name, it was normalized to '{1}' @@ -332,27 +245,17 @@ CONTAINER2011: {0} '{1}' does not exist CONTAINER2011: {0} {1} 不存在 - {StrBegins="CONTAINER2011: "} - - - CONTAINER1017: Unable to communicate with the registry '{0}'. - CONTAINER1017: 无法与注册表“{0}”通信。 - {StrBegins="CONTAINER1017:" } + {StrBegin="CONTAINER2011: "} CONTAINER1013: Failed to push to the output registry: {0} CONTAINER1013: 未能推送到输出注册表: {0} - {StrBegins="CONTAINER1013: "} - - - CONTAINER1014: Manifest pull failed. - CONTAINER1014: 清单拉取失败。 - {StrBegins="CONTAINER1014: "} + {StrBegin="CONTAINER1013: "} - CONTAINER1005: Registry push failed; received status code '{0}'. - CONTAINER1005: 注册表推送失败,已收到状态代码“{0}”。 - {StrBegins="CONTAINER1005: "} + CONTAINER1005: Registry push failed. + CONTAINER1005: 注册表推送失败。 + {StrBegin="CONTAINER1005: "} Uploading config to registry at blob '{0}', @@ -399,70 +302,45 @@ 已将标记“{0}”上传到“{1}”。 {1} is the registry name - - CONTAINER1015: Unable to access the repository '{0}' at tag '{1}' in the registry '{2}'. Please confirm that this name and tag are present in the registry. - CONTAINER1015: 无法访问注册表“{2}”中标记“{1}”处的存储库“{0}”。请确认注册表中存在此名称和标记。 - {StrBegins="CONTAINER1015: "} - CONTAINER4003: Required '{0}' items contain empty items. CONTAINER4003: 必需的“{0}”项包含空项。 - {StrBegins="CONTAINER4003: "} + {StrBegin="CONTAINER4003: "} CONTAINER4002: Required '{0}' items were not set. CONTAINER4002: 未设置必需的“{0}”项。 - {StrBegins="CONTAINER4002: "} + {StrBegin="CONTAINER4002: "} CONTAINER4001: Required property '{0}' was not set or empty. CONTAINER4001: 未设置必需属性 "{0}" 或为空。 - {StrBegins="CONTAINER4001: "} + {StrBegin="CONTAINER4001: "} CONTAINER1006: Too many retries, stopping. CONTAINER1006: 重试次数过多,正在停止。 - {StrBegins="CONTAINER1006: "} - - - CONTAINER1016: Unable to access the repository '{0}' in the registry '{1}'. Please confirm your credentials are correct and that you have access to this repository and registry. - CONTAINER1016: 无法访问注册表“{1}”中的存储库“{0}”。请确认你的凭据正确无误,并且你有权访问此存储库和注册表。 - {StrBegins="CONTAINER1016:" } - - - CONTAINER1018: Unable to download image from the repository '{0}'. - CONTAINER1018: 无法从存储库 "{0}" 下载映像。 - {StrBegins="CONTAINER1018:" } + {StrBegin="CONTAINER1006: "} CONTAINER2021: Unknown AppCommandInstruction '{0}'. Valid instructions are {1}. - CONTAINER2021: AppCommandInstruction“{0}”未知。有效的说明为 {1}。 - {StrBegins="CONTAINER2021: "} + CONTAINER2021: Unknown AppCommandInstruction '{0}'. Valid instructions are {1}. + {StrBegin="CONTAINER2021: "} CONTAINER2002: Unknown local registry type '{0}'. Valid local container registry types are {1}. - CONTAINER2002: 本地注册表类型“{0}”未知。有效的本地容器注册表类型为 {1}。 - {StrBegins="CONTAINER2002: "} + CONTAINER2002: Unknown local registry type '{0}'. Valid local container registry types are {1}. + {StrBegin="CONTAINER2002: "} CONTAINER2003: The manifest for {0}:{1} from registry {2} was an unknown type: {3}. Please raise an issue at https://github.com/dotnet/sdk-container-builds/issues with this message. CONTAINER2003: 注册表 {2} 中 {0} {1} 的清单是未知类型: {3}。请在 https://github.com/dotnet/sdk-container-builds/issues 上提出问题,并附上此消息。 - {StrBegins="CONTAINER2003: "} + {StrBegin="CONTAINER2003: "} CONTAINER2001: Unrecognized mediaType '{0}'. CONTAINER2001: 无法识别 mediaType“{0}”。 - {StrBegins="CONTAINER2001: "} - - - Cannot create image index for the provided 'mediaType' = '{0}'. - 无法为提供的 “mediaType” = '{0}' 创建映像索引。 - - - - Unable to create tarball for mediaType '{0}'. - 无法为 mediaType“{0}”创建 tarball。 - + {StrBegin="CONTAINER2001: "} CONTAINER0000: Value for unit test {0} diff --git a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.zh-Hant.xlf b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.zh-Hant.xlf index f9bdce96c863..07fb1de1b126 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.zh-Hant.xlf +++ b/src/Containers/Microsoft.NET.Build.Containers/Resources/xlf/Strings.zh-Hant.xlf @@ -5,73 +5,61 @@ CONTAINER1002: Request to Amazon Elastic Container Registry failed prematurely. This is often caused when the target repository does not exist in the registry. CONTAINER1002: 對 Amazon 彈性容器登錄的要求提前失敗。這通常是在目標存放庫不存在於登錄中時所導致。 - {StrBegins="CONTAINER1002: "} + {StrBegin="CONTAINER1002: "} CONTAINER2008: Both {0} and {1} were provided, but only one or the other is allowed. CONTAINER2008: 同時提供了 {0} 和 {1},但只允許兩者之一。 - {StrBegins="CONTAINER2008: "} + {StrBegin="CONTAINER2008: "} CONTAINER2025: ContainerAppCommandArgs are provided without specifying a ContainerAppCommand. - CONTAINER2025: 提供了 ContainerAppCommandArgs 但未指定 ContainerAppCommand。 - {StrBegins="CONTAINER2025: "} + CONTAINER2025: ContainerAppCommandArgs are provided without specifying a ContainerAppCommand. + {StrBegin="CONTAINER2025: "} CONTAINER2026: ContainerAppCommand and ContainerAppCommandArgs must be empty when ContainerAppCommandInstruction is '{0}'. - CONTAINER2026: 當 ContainerAppCommandInstruction 為 '{0}' 時,ContainerAppCommand 和 ContainerAppCommandArgs 必須是空白。 - {StrBegins="CONTAINER2026: "} - - - local archive at '{0}' - 位於 '{0}' 的本機封存 - {0} is the path to the file written + CONTAINER2026: ContainerAppCommand and ContainerAppCommandArgs must be empty when ContainerAppCommandInstruction is '{0}'. + {StrBegin="CONTAINER2026: "} CONTAINER2022: The base image has an entrypoint that will be overwritten to start the application. Set ContainerAppCommandInstruction to 'Entrypoint' if this is desired. To preserve the base image entrypoint, set ContainerAppCommandInstruction to 'DefaultArgs'. - CONTAINER2022: 基礎映像有一個進入點,將被覆寫以啟動應用程式。如果這是預期的行為,請將 ContainerAppCommandInstruction 設定為 'Entrypoint'。若要保留基礎映像進入點,請將 ContainerAppCommandInstruction 設定為 'DefaultArgs'。 - {StrBegins="CONTAINER2022: "} + CONTAINER2022: The base image has an entrypoint that will be overwritten to start the application. Set ContainerAppCommandInstruction to 'Entrypoint' if this is desired. To preserve the base image entrypoint, set ContainerAppCommandInstruction to 'DefaultArgs'. + {StrBegin="CONTAINER2022: "} CONTAINER2009: Could not parse {0}: {1} CONTAINER2009: 無法剖析 {0}: {1} - {StrBegins="CONTAINER2009: "} + {StrBegin="CONTAINER2009: "} CONTAINER2020: {0} does not specify a registry and will be pulled from Docker Hub. Please prefix the name with the image registry, for example: '{1}/<image>'. - CONTAINER2020: {0} 未指定登錄,並將從 Docker Hub 提取。請在名稱前面加上映像登錄,例如: '{1}/<image>'。 - {StrBegins="CONTAINER2020: "} + CONTAINER2020: {0} does not specify a registry and will be pulled from Docker Hub. Please prefix the name with the image registry, for example: '{1}/<image>'. + {StrBegin="CONTAINER2020: "} CONTAINER2013: {0} had spaces in it, replacing with dashes. CONTAINER2013: {0} 有空格,正在以虛線取代。 - {StrBegins="CONTAINER2013: "} + {StrBegin="CONTAINER2013: "} CONTAINER1011: Couldn't find matching base image for {0} that matches RuntimeIdentifier {1}. CONTAINER1011: 找不到 {0} 符合 RuntimeIdentifier 的 {1} 相符基本映像。 - {StrBegins="CONTAINER1011: "} + {StrBegin="CONTAINER1011: "} CONTAINER1001: Failed to upload blob using {0}; received status code '{1}'. CONTAINER1001: 無法使用 {0} 上傳 blob; 收到狀態碼 '{1}'。 - {StrBegins="CONTAINER1001: "} - - - Building image index '{0}' on top of manifests {1}. - 正在將映像索引 '{0}' 在指令清單頂端 {1}。 - - {0} is the name of the image index and its tag, {1} is the list of manifests digests - + {StrBegin="CONTAINER1001: "} - Pushed image '{0}' to {1}. - 已將影像 '{0}' 推送至 {1}。 + Pushed container '{0}' to Docker daemon. + 已推送容器 '{0}' 至 Docker 精靈。 - Pushed image '{0}' to registry '{1}'. - 已將影像 '{0}' 推送至登錄 '{1}'。 + Pushed container '{0}' to registry '{1}'. + 已將容器 '{0}' 推送至登錄 '{1}'。 @@ -79,220 +67,150 @@ 在基礎映像 '{2}' 上建立具有標記 '{1}' 的映像 '{0}'。 - - Building image '{0}' for runtime identifier '{1}' on top of base image '{2}'. - 在基礎映像 '{2}' 上建立具有執行階段識別碼 '{1}' 的映像 '{0}'。 - - CONTAINER1007: Could not deserialize token from JSON. CONTAINER1007: 無法從 JSON 還原序列化權杖。 - {StrBegins="CONTAINER1007: "} + {StrBegin="CONTAINER1007: "} CONTAINER2012: Could not recognize registry '{0}'. CONTAINER2012: 無法識別登錄 '{0}'。 - {StrBegins="CONTAINER2012: "} - - - local registry via '{0}' - 透過 '{0}' 的本機登錄 - {0} is the command used + {StrBegin="CONTAINER2012: "} CONTAINER3002: Failed to get docker info({0})\n{1}\n{2} CONTAINER3002: 無法取得 Docker 資訊({0})\n{1}\n{2} - {StrBegins="CONTAINER3002: "} + {StrBegin="CONTAINER3002: "} CONTAINER3002: Failed to get docker info: {0} CONTAINER3002: 無法取得 Docker 資訊: {0} - {StrBegins="CONTAINER3002: "} + {StrBegin="CONTAINER3002: "} - + CONTAINER3001: Failed creating {0} process. - CONTAINER3001: 無法建立 {0} 程序。 + CONTAINER3001: Failed creating {0} process. CONTAINER3001: {0} is the name of the command we failed to run, usually 'docker' or 'podman'. CONTAINER4006: Property '{0}' is empty or contains whitespace and will be ignored. CONTAINER4006: 屬性 '{0}' 空的或包含空白字元,將被忽略。 - {StrBegins="CONTAINER4006: "} + {StrBegin="CONTAINER4006: "} CONTAINER4004: Items '{0}' contain empty item(s) which will be ignored. CONTAINER4004: 項目 '{0}' 包含將被忽略的空項目。 - {StrBegins="CONTAINER4004: "} + {StrBegin="CONTAINER4004: "} CONTAINER2023: A ContainerEntrypoint and ContainerAppCommandArgs are provided. ContainerAppInstruction must be set to configure how the application is started. Valid instructions are {0}. - CONTAINER2023: 提供了 ContainerEntrypoint 和 ContainerAppCommandArgs。必須設定 ContainerAppInstruction 以設定應用程式的啟動方式。有效的指示為 {0}。 - {StrBegins="CONTAINER2023: "} + CONTAINER2023: A ContainerEntrypoint and ContainerAppCommandArgs are provided. ContainerAppInstruction must be set to configure how the application is started. Valid instructions are {0}. + {StrBegin="CONTAINER2023: "} CONTAINER2027: A ContainerEntrypoint is provided. ContainerAppInstruction must be set to configure how the application is started. Valid instructions are {0}. - CONTAINER2027: 提供了 ContainerEntrypoint。必須設定 ContainerAppInstruction 以設定應用程式的啟動方式。有效的指示為 {0}。 - {StrBegins="CONTAINER2027: "} + CONTAINER2027: A ContainerEntrypoint is provided. ContainerAppInstruction must be set to configure how the application is started. Valid instructions are {0}. + {StrBegin="CONTAINER2027: "} CONTAINER2024: ContainerEntrypointArgs are provided without specifying a ContainerEntrypoint. - CONTAINER2024: 提供了 ContainerEntrypointArgs 但未指定 ContainerEntrypoint。 - {StrBegins="CONTAINER2024: "} + CONTAINER2024: ContainerEntrypointArgs are provided without specifying a ContainerEntrypoint. + {StrBegin="CONTAINER2024: "} CONTAINER2029: ContainerEntrypointArgsSet are provided. Change to use ContainerAppCommandArgs for arguments that must always be set, or ContainerDefaultArgs for arguments that can be overridden when the container is created. - CONTAINER2029: 提供了 ContainerEntrypointArgsSet。請針對一律必須設定的引數變更為使用 ContainerAppCommandArgs,或針對建立容器時可覆寫的引數使用 ContainerDefaultArgs。 - {StrBegins="CONTAINER2029: "} + CONTAINER2029: ContainerEntrypointArgsSet are provided. Change to use ContainerAppCommandArgs for arguments that must always be set, or ContainerDefaultArgs for arguments that can be overridden when the container is created. + {StrBegin="CONTAINER2029: "} CONTAINER2028: ContainerEntrypoint can not be combined with ContainerAppCommandInstruction '{0}'. - CONTAINER2028: ContainerEntrypoint 無法與 ContainerAppCommandInstruction '{0}' 結合。 - {StrBegins="CONTAINER2028: "} + CONTAINER2028: ContainerEntrypoint can not be combined with ContainerAppCommandInstruction '{0}'. + {StrBegin="CONTAINER2028: "} CONTAINER1008: Failed retrieving credentials for "{0}": {1} CONTAINER1008: 無法擷取 "{0}" 的認證: {1} - {StrBegins="CONTAINER1008: "} - - - CONTAINER2030: GenerateLabels was disabled but GenerateDigestLabel was enabled - no digest label will be created. - CONTAINER2030: GenerateLabels 已停用,但 GenerateDigestLabel 已啟用 - 將不會建立摘要標籤。 - {StrBegins="CONTAINER2030: "} + {StrBegin="CONTAINER1008: "} No host object detected. 未偵測到主機物件。 - - Cannot create image index because at least one of the provided images' config is missing 'architecture'. - 無法建立映射索引,因為至少一個提供的映像設定遺漏 'architecture'。 - - - - Cannot create image index because at least one of the provided images' config is missing 'os'. - 無法建立映射索引,因為至少一個提供的映像設定遺漏 'os'。 - - - - Pushed image index '{0}' to registry '{1}'. - 已將影像 '{0}' 推送至登錄 '{1}'。 - - - - Image index creation for Podman is not supported. - 不支援建立 Podman 的映射索引。 - - CONTAINER1009: Failed to load image from local registry. stdout: {0} - CONTAINER1009: 無法從本機登錄載入映像。stdout: {0} - {StrBegins="CONTAINER1009: "} - - - CONTAINER1020: Failed to load image because containerd image store is not enabled for Docker. Tip: You can enable it by checking 'Use containerd for pulling and storing images' in Docker Desktop settings. - CONTAINER1020: 因為 Docker 未啟用容器映像存放區,所以無法載入映像。提示: 您可以在 [Docker 桌面] 設定中勾選 [使用容器化來提取和儲存映像] 來啟用此功能。 - {StrBegin="CONTAINER1020: "} + CONTAINER1009: 無法將映像載入至本機 Docker 精靈。stdout: {0} + {StrBegin="CONTAINER1009: "} CONTAINER1010: Pulling images from local registry is not supported. - CONTAINER1010: 不支援從本機登錄提取映像。 - {StrBegins="CONTAINER1010: "} + CONTAINER1010: 不支援從本機 Docker 精靈提取映像。 + {StrBegin="CONTAINER1010: "} - - Cannot create image index because no images were provided. - 無法建立映射索引,因為未提供影像。 - - - - CONTAINER2031: The container image format '{0}' is not supported. Supported formats are '{1}'. - CONTAINER2031: 不支援容器映像格式 '{0}'。支援的格式為 '{1}'。 - {StrBegins="CONTAINER2031: "} + + CONTAINER2014: Invalid {0}: {1}. + CONTAINER2014: 無效的 {0}: {1}。 + {StrBegin="CONTAINER2014: "} CONTAINER2015: {0}: '{1}' was not a valid Environment Variable. Ignoring. CONTAINER2015: {0}: '{1}' 不是有效的環境變數。正在忽略。 - {StrBegins="CONTAINER2015: "} + {StrBegin="CONTAINER2015: "} - - Cannot create image index because at least one of the provided images' config is invalid. - 無法建立映射索引,因為至少一個提供的映像設定無效。 - - - - Cannot create image index because at least one of the provided images' manifest is invalid. - 無法建立映射索引,因為至少一個提供的映像指令清單無效。 - - - - Cannot create image index because provided images are invalid. Items must have 'Config', 'Manifest', 'ManifestMediaType' and 'ManifestDigest' metadata. - 無法建立映射索引,因為提供的映像無效。項目必須有 'Config'、'Manifest'、'ManifestMediaType' 和 'ManifestDigest' 元數據。 - - - - CONTAINER2005: The inferred image name '{0}' contains entirely invalid characters. The valid characters for an image name are alphanumeric characters, -, /, or _, and the image name must start with an alphanumeric character. - CONTAINER2005: 推斷的映像名稱 '{0}' 包含完全無效字元。映像名稱的有效字元是英數字元、-、/ 或 _,並且映像名稱必須以英數字元開頭。 - {StrBegins="CONTAINER2005: "} - - - CONTAINER2005: The first character of the image name '{0}' must be a lowercase letter or a digit and all characters in the name must be an alphanumeric character, -, /, or _. - CONTAINER2005: 映像名稱 '{0}' 的首字元必須是小寫字母或數字,並且名稱中的所有字元都必須是英數字元、-、/ 或 _。 - {StrBegins="CONTAINER2005: "} + + CONTAINER2005: The first character of the image name must be a lowercase letter or a digit. + CONTAINER2005: 映像名稱的第一個字元必須是小寫字母或數字。 + {StrBegin="CONTAINER2005: "} CONTAINER2017: A ContainerPort item was provided with an invalid port number '{0}'. ContainerPort items must have an Include value that is an integer, and a Type value that is either 'tcp' or 'udp'. CONTAINER2017: 提供的 ContainerPort 項目具有無效的連接埠號碼 '{0}'。ContainerPort 項目必須具有為整數的 Include 值,以及 'tcp' 或 'udp' 的 Type 值。 - {StrBegins="CONTAINER2017: "} + {StrBegin="CONTAINER2017: "} CONTAINER2017: A ContainerPort item was provided with an invalid port number '{0}' and an invalid port type '{1}'. ContainerPort items must have an Include value that is an integer, and a Type value that is either 'tcp' or 'udp'. CONTAINER2017: 提供的 ContainerPort 項目具有無效的連接埠號碼 '{0}' 和無效的連接埠類型 '{1}'。ContainerPort 項目必須具有為整數的 Include 值,以及 'tcp' 或 'udp' 的 Type 值。 - {StrBegins="CONTAINER2017: "} + {StrBegin="CONTAINER2017: "} CONTAINER2017: A ContainerPort item was provided with an invalid port type '{0}'. ContainerPort items must have an Include value that is an integer, and a Type value that is either 'tcp' or 'udp'. CONTAINER2017: 提供的 ContainerPort 項目具有無效的連接埠類型 '{0}'。ContainerPort 項目必須具有為整數的 Include 值,以及 'tcp' 或 'udp' 的 Type 值。 - {StrBegins="CONTAINER2017: "} + {StrBegin="CONTAINER2017: "} CONTAINER2018: Invalid SDK prerelease version '{0}' - only 'rc' and 'preview' are supported. CONTAINER2018: 無效的 SDK 發行前版本 '{0}' - 只支援 'rc' 和 'preview'。 - {StrBegins="CONTAINER2018: "} + {StrBegin="CONTAINER2018: "} CONTAINER2019: Invalid SDK semantic version '{0}'. CONTAINER2019: 無效的 SDK 語義版本 '{0}'。 - {StrBegins="CONTAINER2019: "} + {StrBegin="CONTAINER2019: "} CONTAINER2007: Invalid {0} provided: {1}. Image tags must be alphanumeric, underscore, hyphen, or period. CONTAINER2007: 提供的 {0} 無效: {1}。映像標記必須是英數字元、底線、連字號或句號。 - {StrBegins="CONTAINER2007: "} + {StrBegin="CONTAINER2007: "} CONTAINER2010: Invalid {0} provided: {1}. {0} must be a semicolon-delimited list of valid image tags. Image tags must be alphanumeric, underscore, hyphen, or period. CONTAINER2010: 提供的 {0} 無效: {1}。{0} 必須是有效映像標記的分號分隔清單。映像標記必須是英數字元、底線、連字號或句號。 - {StrBegins="CONTAINER2010: "} - - - Invalid string[] TargetRuntimeIdentifiers. Either all should be 'linux-musl' or none. - 無效的字串[] TargetRuntimeIdentifiers。全部應為 『linux-musl』 或無。 - + {StrBegin="CONTAINER2010: "} CONTAINER1003: Token response had neither token nor access_token. CONTAINER1003: 權杖回應沒有權杖,也沒有access_token。 - {StrBegins="CONTAINER1003: "} + {StrBegin="CONTAINER1003: "} CONTAINER4005: Item '{0}' contains items without metadata 'Value', and they will be ignored. CONTAINER4005: 項目 '{0}' 包含沒有中繼資料 'Value' 的項目,將忽略這些項目。 - {StrBegins="CONTAINER4005: "} + {StrBegin="CONTAINER4005: "} CONTAINER1012: The local registry is not available, but pushing to a local registry was requested. - CONTAINER1012: 本機登錄無法使用,但已要求推送至本機登錄。 - {StrBegins="CONTAINER1012: "} + CONTAINER1012: The local registry is not available, but pushing to a local registry was requested. + {StrBegin="CONTAINER1012: "} Error while reading daemon config: {0} @@ -307,22 +225,17 @@ CONTAINER2004: Unable to download layer with descriptor '{0}' from registry '{1}' because it does not exist. CONTAINER2004: 無法從登錄 '{1}' 下載描述元為 '{0}' 的層,因為它不存在。 - {StrBegins="CONTAINER2004: "} + {StrBegin="CONTAINER2004: "} CONTAINER2016: ContainerPort item '{0}' does not specify the port number. Please ensure the item's Include is a port number, for example '<ContainerPort Include="80" />' CONTAINER2016: ContainerPort 項目 '{0}' 未指定連接埠號碼。請確保項目的 Include 是連接埠號碼,例如 '<ContainerPort Include="80" />' - {StrBegins="CONTAINER2016: "} - - - 'mediaType' of manifests should be the same in image index. - 指令清單的 'mediaType' 在影像索引中應該相同。 - + {StrBegin="CONTAINER2016: "} CONTAINER1004: No RequestUri specified. CONTAINER1004: 未指定 RequestUri。 - {StrBegins="CONTAINER1004: "} + {StrBegin="CONTAINER1004: "} '{0}' was not a valid container image name, it was normalized to '{1}' @@ -332,27 +245,17 @@ CONTAINER2011: {0} '{1}' does not exist CONTAINER2011: {0} '{1}' 不存在 - {StrBegins="CONTAINER2011: "} - - - CONTAINER1017: Unable to communicate with the registry '{0}'. - CONTAINER1017: 無法與登錄 '{0}' 通訊。 - {StrBegins="CONTAINER1017:" } + {StrBegin="CONTAINER2011: "} CONTAINER1013: Failed to push to the output registry: {0} CONTAINER1013: 無法推送至輸出登錄: {0} - {StrBegins="CONTAINER1013: "} - - - CONTAINER1014: Manifest pull failed. - CONTAINER1014: 資訊清單提取失敗。 - {StrBegins="CONTAINER1014: "} + {StrBegin="CONTAINER1013: "} - CONTAINER1005: Registry push failed; received status code '{0}'. - CONTAINER1005: 登錄推送失敗; 已收到狀態碼 '{0}'。 - {StrBegins="CONTAINER1005: "} + CONTAINER1005: Registry push failed. + CONTAINER1005: 登錄推送失敗。 + {StrBegin="CONTAINER1005: "} Uploading config to registry at blob '{0}', @@ -399,70 +302,45 @@ 已上傳標記 '{0}' 至 '{1}'。 {1} is the registry name - - CONTAINER1015: Unable to access the repository '{0}' at tag '{1}' in the registry '{2}'. Please confirm that this name and tag are present in the registry. - CONTAINER1015: 無法存取登錄 '{2}' 中標籤 '{1}' 的存放庫 '{0}'。請確認此名稱和標籤存在於登錄中。 - {StrBegins="CONTAINER1015: "} - CONTAINER4003: Required '{0}' items contain empty items. CONTAINER4003: 必要的 '{0}' 項目包含空項目。 - {StrBegins="CONTAINER4003: "} + {StrBegin="CONTAINER4003: "} CONTAINER4002: Required '{0}' items were not set. CONTAINER4002: 必要的 '{0}' 項目未設定。 - {StrBegins="CONTAINER4002: "} + {StrBegin="CONTAINER4002: "} CONTAINER4001: Required property '{0}' was not set or empty. CONTAINER4001: 必要的屬性 '{0}' 未設定或是空的。 - {StrBegins="CONTAINER4001: "} + {StrBegin="CONTAINER4001: "} CONTAINER1006: Too many retries, stopping. CONTAINER1006: 重試太多次,正在停止。 - {StrBegins="CONTAINER1006: "} - - - CONTAINER1016: Unable to access the repository '{0}' in the registry '{1}'. Please confirm your credentials are correct and that you have access to this repository and registry. - CONTAINER1016: 無法存取登錄 '{1}' 中的存放庫 '{0}'。請確認您的認證正確,且您擁有此存放庫和登錄的存取權。 - {StrBegins="CONTAINER1016:" } - - - CONTAINER1018: Unable to download image from the repository '{0}'. - CONTAINER1018: 無法從存放庫 '{0}' 下載影像。 - {StrBegins="CONTAINER1018:" } + {StrBegin="CONTAINER1006: "} CONTAINER2021: Unknown AppCommandInstruction '{0}'. Valid instructions are {1}. - CONTAINER2021: 未知的 AppCommandInstruction '{0}'。有效的指示為 {1}。 - {StrBegins="CONTAINER2021: "} + CONTAINER2021: Unknown AppCommandInstruction '{0}'. Valid instructions are {1}. + {StrBegin="CONTAINER2021: "} CONTAINER2002: Unknown local registry type '{0}'. Valid local container registry types are {1}. - CONTAINER2002: 不明的本機登錄類型 '{0}'。有效的本機容器註冊類型為 {1}。 - {StrBegins="CONTAINER2002: "} + CONTAINER2002: Unknown local registry type '{0}'. Valid local container registry types are {1}. + {StrBegin="CONTAINER2002: "} CONTAINER2003: The manifest for {0}:{1} from registry {2} was an unknown type: {3}. Please raise an issue at https://github.com/dotnet/sdk-container-builds/issues with this message. CONTAINER2003: 來自登錄 {2} 用於 {0}:{1} 的資訊清單是未知的類型: {3}。請使用此訊息在 https://github.com/dotnet/sdk-container-builds/issues 提出問題。 - {StrBegins="CONTAINER2003: "} + {StrBegin="CONTAINER2003: "} CONTAINER2001: Unrecognized mediaType '{0}'. CONTAINER2001: 無法辨識的 mediaType '{0}'。 - {StrBegins="CONTAINER2001: "} - - - Cannot create image index for the provided 'mediaType' = '{0}'. - 無法為提供的 'mediaType' = '{0}' 建立映射索引。 - - - - Unable to create tarball for mediaType '{0}'. - 無法為 mediaType '{0}' 建立 tarball。 - + {StrBegin="CONTAINER2001: "} CONTAINER0000: Value for unit test {0} diff --git a/src/Containers/Microsoft.NET.Build.Containers/Tasks/ComputeDotnetBaseImageTag.cs b/src/Containers/Microsoft.NET.Build.Containers/Tasks/ComputeDotnetBaseImageTag.cs new file mode 100644 index 000000000000..d0fdde5b930d --- /dev/null +++ b/src/Containers/Microsoft.NET.Build.Containers/Tasks/ComputeDotnetBaseImageTag.cs @@ -0,0 +1,113 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Build.Framework; +using NuGet.Versioning; +#if NETFRAMEWORK +using System.Linq; +#endif + +namespace Microsoft.NET.Build.Containers.Tasks; + +/// +/// Computes the base image Tag for a Microsoft-authored container image based on the tagging scheme from various SDK versions. +/// +public sealed class ComputeDotnetBaseImageTag : Microsoft.Build.Utilities.Task +{ + // starting in .NET 8, the container tagging scheme started incorporating the + // 'channel' (rc/preview) and the channel increment (the numeric value after the channel name) + // into the container tags. + private const int FirstVersionWithNewTaggingScheme = 8; + + [Required] + public string SdkVersion { get; set; } + + [Required] + public string TargetFrameworkVersion { get; set; } + + [Output] + public string? ComputedBaseImageTag { get; private set; } + + public ComputeDotnetBaseImageTag() + { + SdkVersion = ""; + TargetFrameworkVersion = ""; + } + + public override bool Execute() + { + if (SemanticVersion.TryParse(TargetFrameworkVersion, out var tfm) && tfm.Major < FirstVersionWithNewTaggingScheme) + { + ComputedBaseImageTag = $"{tfm.Major}.{tfm.Minor}"; + return true; + } + + if (SemanticVersion.TryParse(SdkVersion, out var version)) + { + ComputedBaseImageTag = ComputeVersionInternal(version, tfm); + return true; + } + else + { + Log.LogError(Resources.Strings.InvalidSdkVersion, SdkVersion); + return false; + } + } + + + private string? ComputeVersionInternal(SemanticVersion version, SemanticVersion? tfm) + { + if (tfm != null && (tfm.Major < version.Major || tfm.Minor < version.Minor)) + { + // in this case the TFM is earlier, so we are assumed to be in a stable scenario + return $"{tfm.Major}.{tfm.Minor}"; + } + // otherwise if we're in a scenario where we're using the TFM for the given SDK version, + // and that SDK version may be a prerelease, so we need to handle + var baseImageTag = (version) switch + { + // all stable versions or prereleases with majors before the switch get major/minor tags + { IsPrerelease: false } or { Major: < FirstVersionWithNewTaggingScheme } => $"{version.Major}.{version.Minor}", + // prereleases after the switch for the first SDK version get major/minor-channel.bump tags + { IsPrerelease: true, Major: >= FirstVersionWithNewTaggingScheme, Patch: 100 } => DetermineLabelBasedOnChannel(version.Major, version.Minor, version.ReleaseLabels.ToArray()), + // prereleases of subsequent SDK versions still get to use the stable tags + { IsPrerelease: true, Major: >= FirstVersionWithNewTaggingScheme } => $"{version.Major}.{version.Minor}", + }; + return baseImageTag; + } + + private string? DetermineLabelBasedOnChannel(int major, int minor, string[] releaseLabels) { + // this would be a switch, but we have to support net47x where Range and Index aren't available + if (releaseLabels.Length == 0) + { + return $"{major}.{minor}"; + } + else + { + var channel = releaseLabels[0]; + if (channel == "rc" || channel == "preview") + { + if (releaseLabels.Length > 1) + { + // Per the dotnet-docker team, the major.minor preview tag format is a fluke and the major.minor.0 form + // should be used for all previews going forward. + return $"{major}.{minor}.0-{channel}.{releaseLabels[1]}"; + } + else + { + Log.LogError(Resources.Strings.InvalidSdkPrereleaseVersion, channel); + return null; + } + } + else if (channel == "alpha" || channel == "dev" || channel == "ci") + { + return $"{major}.{minor}-preview"; + } + else + { + Log.LogError(Resources.Strings.InvalidSdkPrereleaseVersion, channel); + return null; + } + } + } +} diff --git a/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.Interface.cs b/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.Interface.cs index ebbd1de29901..d0a26a8be2f8 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.Interface.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.Interface.cs @@ -1,6 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; +using System.Reflection; +using System.Resources; using Microsoft.Build.Framework; using Microsoft.NET.Build.Containers.Resources; @@ -20,6 +23,7 @@ partial class CreateNewImage /// The base registry to pull from. /// Ex: mcr.microsoft.com /// + [Required] public string BaseRegistry { get; set; } /// @@ -33,24 +37,14 @@ partial class CreateNewImage /// The base image tag. /// Ex: 6.0 /// + [Required] public string BaseImageTag { get; set; } - /// - /// The base image digest. - /// Ex: sha256:12345... - /// - public string BaseImageDigest { get; set; } - /// /// The registry to push to. /// public string OutputRegistry { get; set; } - /// - /// The file path to which to write a tar.gz archive of the container image. - /// - public string ArchiveOutputPath { get; set; } - /// /// The kind of local registry to use, if any. /// @@ -150,51 +144,12 @@ partial class CreateNewImage /// public string ContainerUser { get; set; } - /// - /// If true, the tooling may create labels on the generated images. - /// - [Required] - public bool GenerateLabels { get; set; } - - /// - /// If true, the tooling will generate an org.opencontainers.image.base.digest label on the generated images containing the digest of the chosen base image. - /// - /// - /// Normally this would have been handled in the container targets, but we do not currently _fetch_ the digest of the base image in pure MSBuild, so we do it during generation-time. - /// - [Required] - public bool GenerateDigestLabel { get; set; } - - /// - /// Set to either 'OCI', 'Docker', or null. If unset, the generated images' mediaType will be that of the base image. If set, the generated image will be given the specified media type. - /// - public string? ImageFormat { get; set; } - - /// If true, the tooling will skip the publishing step. - /// - public bool SkipPublishing { get; set; } - [Output] public string GeneratedContainerManifest { get; set; } [Output] public string GeneratedContainerConfiguration { get; set; } - [Output] - public string GeneratedContainerDigest { get; set; } - - [Output] - public string GeneratedArchiveOutputPath { get; set; } - - [Output] - public string GeneratedContainerMediaType { get; set; } - - [Output] - public ITaskItem[] GeneratedContainerNames { get; set; } - - [Output] - public ITaskItem? GeneratedDigestLabel { get; set; } - public CreateNewImage() { ContainerizeDirectory = ""; @@ -203,9 +158,7 @@ public CreateNewImage() BaseRegistry = ""; BaseImageName = ""; BaseImageTag = ""; - BaseImageDigest = ""; OutputRegistry = ""; - ArchiveOutputPath = ""; Repository = ""; ImageTags = Array.Empty(); PublishDirectory = ""; @@ -226,14 +179,6 @@ public CreateNewImage() GeneratedContainerConfiguration = ""; GeneratedContainerManifest = ""; - GeneratedContainerDigest = ""; - GeneratedArchiveOutputPath = ""; - GeneratedContainerMediaType = ""; - GeneratedContainerNames = Array.Empty(); - GeneratedDigestLabel = null; - - GenerateLabels = false; - GenerateDigestLabel = false; TaskResources = Resource.Manager; } diff --git a/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.cs b/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.cs index 124ee9749f45..c3cd150a3cea 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.cs @@ -1,9 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Text.Json; using Microsoft.Build.Framework; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.MSBuild; +using Microsoft.NET.Build.Containers.Logging; using Microsoft.NET.Build.Containers.Resources; using ILogger = Microsoft.Extensions.Logging.ILogger; @@ -23,25 +24,15 @@ public sealed partial class CreateNewImage : Microsoft.Build.Utilities.Task, ICa /// public string ToolPath { get; set; } - private bool IsLocalPull => string.IsNullOrWhiteSpace(BaseRegistry); + private bool IsLocalPush => string.IsNullOrEmpty(OutputRegistry); + + private bool IsLocalPull => string.IsNullOrEmpty(BaseRegistry); public void Cancel() => _cancellationTokenSource.Cancel(); public override bool Execute() { - try - { - Task.Run(() => ExecuteAsync(_cancellationTokenSource.Token)).GetAwaiter().GetResult(); - } - catch (TaskCanceledException ex) - { - Log.LogWarningFromException(ex); - } - catch (OperationCanceledException ex) - { - Log.LogWarningFromException(ex); - } - return !Log.HasLoggedErrors; + return Task.Run(() => ExecuteAsync(_cancellationTokenSource.Token)).GetAwaiter().GetResult(); } internal async Task ExecuteAsync(CancellationToken cancellationToken) @@ -58,88 +49,21 @@ internal async Task ExecuteAsync(CancellationToken cancellationToken) return !Log.HasLoggedErrors; } - bool credentialsSet = false; - VSHostObject hostObj = new(HostObject, Log); - if (hostObj.TryGetCredentials() is (string userName, string pass)) - { - // Set credentials for the duration of this operation. - // These will be cleared in the finally block to minimize exposure. - Environment.SetEnvironmentVariable(ContainerHelpers.HostObjectUser, userName); - Environment.SetEnvironmentVariable(ContainerHelpers.HostObjectPass, pass); - credentialsSet = true; - } - else - { - Log.LogMessage(MessageImportance.Low, Resource.GetString(nameof(Strings.HostObjectNotDetected))); - } - - try - { - return await ExecuteAsyncCore(logger, msbuildLoggerFactory, cancellationToken).ConfigureAwait(false); - } - finally - { - // Clear credentials from environment to minimize exposure window. - if (credentialsSet) - { - Environment.SetEnvironmentVariable(ContainerHelpers.HostObjectUser, null); - Environment.SetEnvironmentVariable(ContainerHelpers.HostObjectPass, null); - } - } - } - - private async Task ExecuteAsyncCore(ILogger logger, ILoggerFactory msbuildLoggerFactory, CancellationToken cancellationToken) - { - RegistryMode sourceRegistryMode = BaseRegistry.Equals(OutputRegistry, StringComparison.InvariantCultureIgnoreCase) ? RegistryMode.PullFromOutput : RegistryMode.Pull; - Registry? sourceRegistry = IsLocalPull ? null : new Registry(BaseRegistry, logger, sourceRegistryMode); - SourceImageReference sourceImageReference = new(sourceRegistry, BaseImageName, BaseImageTag, BaseImageDigest); - - DestinationImageReference destinationImageReference = DestinationImageReference.CreateFromSettings( - Repository, - ImageTags, - msbuildLoggerFactory, - ArchiveOutputPath, - OutputRegistry, - LocalRegistry); + Registry? sourceRegistry = IsLocalPull ? null : new Registry(ContainerHelpers.TryExpandRegistryToUri(BaseRegistry), logger); + ImageReference sourceImageReference = new(sourceRegistry, BaseImageName, BaseImageTag); - var telemetry = new Telemetry(sourceImageReference, destinationImageReference, Log); + Registry? destinationRegistry = IsLocalPush ? null : new Registry(ContainerHelpers.TryExpandRegistryToUri(OutputRegistry), logger); + IEnumerable destinationImageReferences = ImageTags.Select(t => new ImageReference(destinationRegistry, Repository, t)); ImageBuilder? imageBuilder; if (sourceRegistry is { } registry) { - try - { - var picker = new RidGraphManifestPicker(RuntimeIdentifierGraphPath); - imageBuilder = await registry.GetImageManifestAsync( - BaseImageName, - sourceImageReference.Reference, - ContainerRuntimeIdentifier, - picker, - cancellationToken).ConfigureAwait(false); - } - catch (RepositoryNotFoundException) - { - telemetry.LogUnknownRepository(); - Log.LogErrorWithCodeFromResources(nameof(Strings.RepositoryNotFound), BaseImageName, BaseImageTag, BaseImageDigest, registry.RegistryName); - return !Log.HasLoggedErrors; - } - catch (UnableToAccessRepositoryException) - { - telemetry.LogCredentialFailure(sourceImageReference); - Log.LogErrorWithCodeFromResources(nameof(Strings.UnableToAccessRepository), BaseImageName, registry.RegistryName); - return !Log.HasLoggedErrors; - } - catch (ContainerHttpException e) - { - Log.LogErrorFromException(e, showStackTrace: false, showDetail: true, file: null); - return !Log.HasLoggedErrors; - } - catch (BaseImageNotFoundException e) - { - telemetry.LogRidMismatch(e.RequestedRuntimeIdentifier, e.AvailableRuntimeIdentifiers.ToArray()); - Log.LogErrorFromException(e, showStackTrace: false, showDetail: true, file: null); - return !Log.HasLoggedErrors; - } + imageBuilder = await registry.GetImageManifestAsync( + BaseImageName, + BaseImageTag, + ContainerRuntimeIdentifier, + RuntimeIdentifierGraphPath, + cancellationToken).ConfigureAwait(false); } else { @@ -152,74 +76,18 @@ private async Task ExecuteAsyncCore(ILogger logger, ILoggerFactory msbuild return !Log.HasLoggedErrors; } - (string message, object[] parameters) = SkipPublishing ? - (Strings.ContainerBuilder_StartBuildingImageForRid, new object[] { Repository, ContainerRuntimeIdentifier, sourceImageReference }) : - (Strings.ContainerBuilder_StartBuildingImage, new object[] { Repository, String.Join(",", ImageTags), sourceImageReference }); - Log.LogMessage(MessageImportance.High, message, parameters); - - // forcibly change the media type if required - if (ImageFormat is not null) - { - if (Enum.TryParse(ImageFormat, out var imageFormat)) - { - imageBuilder.ManifestMediaType = imageFormat switch - { - KnownImageFormats.Docker => SchemaTypes.DockerManifestV2, - KnownImageFormats.OCI => SchemaTypes.OciManifestV1, - _ => imageBuilder.ManifestMediaType // should be impossible unless we add to the enum - }; - } - else - { - Log.LogErrorWithCodeFromResources(nameof(Strings.InvalidContainerImageFormat), ImageFormat, string.Join(",", Enum.GetValues())); - } - } + SafeLog("Building image '{0}' with tags {1} on top of base image {2}", Repository, String.Join(",", ImageTags), sourceImageReference); - // forcibly change the media type if required - if (ImageFormat is not null) - { - if (Enum.TryParse(ImageFormat, out var imageFormat)) - { - imageBuilder.ManifestMediaType = imageFormat switch - { - KnownImageFormats.Docker => SchemaTypes.DockerManifestV2, - KnownImageFormats.OCI => SchemaTypes.OciManifestV1, - _ => imageBuilder.ManifestMediaType // should be impossible unless we add to the enum - }; - } - else - { - Log.LogErrorWithCodeFromResources(nameof(Strings.InvalidContainerImageFormat), ImageFormat, string.Join(",", Enum.GetValues())); - } - } - var userId = imageBuilder.IsWindows ? null : ContainerBuilder.TryParseUserId(ContainerUser); - Layer newLayer = Layer.FromDirectory(PublishDirectory, WorkingDirectory, imageBuilder.IsWindows, imageBuilder.ManifestMediaType, userId); + Layer newLayer = Layer.FromDirectory(PublishDirectory, WorkingDirectory, imageBuilder.IsWindows); imageBuilder.AddLayer(newLayer); imageBuilder.SetWorkingDirectory(WorkingDirectory); (string[] entrypoint, string[] cmd) = DetermineEntrypointAndCmd(baseImageEntrypoint: imageBuilder.BaseImageConfig.GetEntrypoint()); imageBuilder.SetEntrypointAndCmd(entrypoint, cmd); - string? baseImageLabel = null; - string? baseImageDigest = null; - if (GenerateLabels) + foreach (ITaskItem label in Labels) { - foreach (ITaskItem label in Labels) - { - imageBuilder.AddLabel(label.ItemSpec, label.GetMetadata("Value")); - } - - if (GenerateDigestLabel) - { - (baseImageLabel, baseImageDigest) = imageBuilder.AddBaseImageDigestLabel(); - } - } - else - { - if (GenerateDigestLabel) - { - Log.LogMessageFromResources(nameof(Strings.GenerateDigestLabelWithoutGenerateLabels)); - } + imageBuilder.AddLabel(label.ItemSpec, label.GetMetadata("Value")); } SetEnvironmentVariables(imageBuilder, ContainerEnvironmentVariables); @@ -241,23 +109,59 @@ private async Task ExecuteAsyncCore(ILogger logger, ILoggerFactory msbuild cancellationToken.ThrowIfCancellationRequested(); // at this point we're done with modifications and are just pushing the data other places - GeneratedContainerManifest = builtImage.Manifest; + GeneratedContainerManifest = JsonSerializer.Serialize(builtImage.Manifest); GeneratedContainerConfiguration = builtImage.Config; - GeneratedContainerDigest = builtImage.ManifestDigest; - GeneratedArchiveOutputPath = ArchiveOutputPath; - GeneratedContainerMediaType = builtImage.ManifestMediaType; - GeneratedContainerNames = destinationImageReference.FullyQualifiedImageNames().Select(name => new Microsoft.Build.Utilities.TaskItem(name)).ToArray(); - if (baseImageLabel is not null && baseImageDigest is not null) - { - var labelItem = new Microsoft.Build.Utilities.TaskItem(baseImageLabel); - labelItem.SetMetadata("Value", baseImageDigest); - GeneratedDigestLabel = labelItem; - } - if (!SkipPublishing) + foreach (ImageReference destinationImageReference in destinationImageReferences) { - await ImagePublisher.PublishImageAsync(builtImage, sourceImageReference, destinationImageReference, Log, telemetry, cancellationToken) - .ConfigureAwait(false); + if (IsLocalPush) + { + ILocalRegistry localRegistry = KnownLocalRegistryTypes.CreateLocalRegistry(LocalRegistry, msbuildLoggerFactory); + if (!(await localRegistry.IsAvailableAsync(cancellationToken).ConfigureAwait(false))) + { + Log.LogErrorWithCodeFromResources(nameof(Strings.LocalRegistryNotAvailable)); + return false; + } + try + { + await localRegistry.LoadAsync(builtImage, sourceImageReference, destinationImageReference, cancellationToken).ConfigureAwait(false); + SafeLog("Pushed image '{0}' to local registry", destinationImageReference.RepositoryAndTag); + } + catch (AggregateException ex) when (ex.InnerException is DockerLoadException dle) + { + Log.LogErrorFromException(dle, showStackTrace: false); + } + } + else + { + try + { + if (destinationImageReference.Registry is not null) + { + await destinationImageReference.Registry.PushAsync( + builtImage, + sourceImageReference, + destinationImageReference, + cancellationToken).ConfigureAwait(false); + SafeLog("Pushed image '{0}' to registry '{1}'", destinationImageReference, OutputRegistry); + } + } + catch (ContainerHttpException e) + { + if (BuildEngine != null) + { + Log.LogErrorFromException(e, true); + } + } + catch (Exception e) + { + if (BuildEngine != null) + { + Log.LogErrorWithCodeFromResources(nameof(Strings.RegistryOutputPushFailed), e.Message); + Log.LogMessage(MessageImportance.Low, "Details: {0}", e); + } + } + } } return !Log.HasLoggedErrors; @@ -300,15 +204,18 @@ private void SetPorts(ImageBuilder image, ITaskItem[] exposedPorts) } } - private void SetEnvironmentVariables(ImageBuilder img, ITaskItem[] envVars) + private static void SetEnvironmentVariables(ImageBuilder img, ITaskItem[] envVars) { foreach (ITaskItem envVar in envVars) { - var value = envVar.GetMetadata("Value"); - img.AddEnvironmentVariable(envVar.ItemSpec, value); + img.AddEnvironmentVariable(envVar.ItemSpec, envVar.GetMetadata("Value")); } } + private void SafeLog(string message, params object[] formatParams) { + if(BuildEngine != null) Log.LogMessage(MessageImportance.High, message, formatParams); + } + public void Dispose() { _cancellationTokenSource.Dispose(); @@ -323,8 +230,98 @@ public void Dispose() string[] appCommandArgs = AppCommandArgs.Select(i => i.ItemSpec).ToArray(); string appCommandInstruction = AppCommandInstruction; - return ImageBuilder.DetermineEntrypointAndCmd(entrypoint, entrypointArgs, cmd, appCommand, appCommandArgs, appCommandInstruction, baseImageEntrypoint, - logWarning: s => Log.LogWarningWithCodeFromResources(s), - logError: (s, a) => { if (a is null) Log.LogErrorWithCodeFromResources(s); else Log.LogErrorWithCodeFromResources(s, a); }); + bool setsEntrypoint = entrypoint.Length > 0 || entrypointArgs.Length > 0; + bool setsCmd = cmd.Length > 0; + + baseImageEntrypoint ??= Array.Empty(); + // Some (Microsoft) base images set 'dotnet' as the ENTRYPOINT. We mustn't use it. + if (baseImageEntrypoint.Length == 1 && (baseImageEntrypoint[0] == "dotnet" || baseImageEntrypoint[0] == "/usr/bin/dotnet")) + { + baseImageEntrypoint = Array.Empty(); + } + + if (string.IsNullOrEmpty(appCommandInstruction)) + { + if (setsEntrypoint) + { + // Backwards-compatibility: before 'AppCommand'/'Cmd' was added, only 'Entrypoint' was available. + if (!setsCmd && appCommandArgs.Length == 0 && entrypoint.Length == 0) + { + // Copy over the values for starting the application from AppCommand. + entrypoint = appCommand; + appCommand = Array.Empty(); + + // Use EntrypointArgs as cmd. + cmd = entrypointArgs; + entrypointArgs = Array.Empty(); + + if (entrypointArgs.Length > 0) + { + // Log warning: Instead of ContainerEntrypointArgs, use ContainerAppCommandArgs for arguments that must always be set, or ContainerDefaultArgs for default arguments that the user override when creating the container. + Log.LogWarningWithCodeFromResources(nameof(Strings.EntrypointArgsSetPreferAppCommandArgs)); + } + + appCommandInstruction = KnownAppCommandInstructions.None; + } + else + { + // There's an Entrypoint. Use DefaultArgs for the AppCommand. + appCommandInstruction = KnownAppCommandInstructions.DefaultArgs; + } + } + else + { + // Default to use an Entrypoint. + // If the base image defines an ENTRYPOINT, print a warning. + if (baseImageEntrypoint.Length > 0) + { + Log.LogWarningWithCodeFromResources(nameof(Strings.BaseEntrypointOverwritten)); + } + appCommandInstruction = KnownAppCommandInstructions.Entrypoint; + } + } + + if (entrypointArgs.Length > 0 && entrypoint.Length == 0) + { + Log.LogErrorWithCodeFromResources(nameof(Strings.EntrypointArgsSetNoEntrypoint)); + return (Array.Empty(), Array.Empty()); + } + + if (appCommandArgs.Length > 0 && appCommand.Length == 0) + { + Log.LogErrorWithCodeFromResources(nameof(Strings.AppCommandArgsSetNoAppCommand)); + return (Array.Empty(), Array.Empty()); + } + + switch (appCommandInstruction) + { + case KnownAppCommandInstructions.None: + if (appCommand.Length > 0 || appCommandArgs.Length > 0) + { + Log.LogErrorWithCodeFromResources(nameof(Strings.AppCommandSetNotUsed), appCommandInstruction); + return (Array.Empty(), Array.Empty()); + } + break; + case KnownAppCommandInstructions.DefaultArgs: + cmd = appCommand.Concat(appCommandArgs).Concat(cmd).ToArray(); + break; + case KnownAppCommandInstructions.Entrypoint: + if (setsEntrypoint) + { + Log.LogErrorWithCodeFromResources(nameof(Strings.EntrypointConflictAppCommand), appCommandInstruction); + return (Array.Empty(), Array.Empty()); + } + entrypoint = appCommand; + entrypointArgs = appCommandArgs; + break; + default: + throw new NotSupportedException( + Resource.FormatString( + nameof(Strings.UnknownAppCommandInstruction), + appCommandInstruction, + string.Join(",", KnownAppCommandInstructions.SupportedAppCommandInstructions))); + } + + return (entrypoint.Length > 0 ? entrypoint.Concat(entrypointArgs).ToArray() : baseImageEntrypoint, cmd); } } diff --git a/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImageToolTask.cs b/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImageToolTask.cs index 8bcec281ff38..042807e381e1 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImageToolTask.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImageToolTask.cs @@ -1,7 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.Diagnostics; +using System.Linq; +using System.IO; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; using Microsoft.NET.Build.Containers.Resources; @@ -22,37 +25,17 @@ private string DotNetPath { get { - // DOTNET_HOST_PATH, if set, is the full path to the dotnet host executable. - // However, not all environments/scenarios set it correctly - some set it to just the directory. - - // this is the expected correct case - DOTNET_HOST_PATH is set to the full path of the dotnet host executable string path = Environment.GetEnvironmentVariable("DOTNET_HOST_PATH") ?? ""; - if (Path.IsPathRooted(path) && File.Exists(path)) - { - return path; - } - // some environments set it to just the directory, so we need to check that too - if (Path.IsPathRooted(path) && Directory.Exists(path)) - { - path = Path.Combine(path, ToolExe); - if (File.Exists(path)) - { - return path; - } - } - // last-chance fallback - use the ToolPath and ToolExe properties to try to synthesize the path if (string.IsNullOrEmpty(path)) { - // no path = string.IsNullOrEmpty(ToolPath) ? "" : ToolPath; - path = Path.Combine(path, ToolExe); } return path; } } - protected override string GenerateFullPathToTool() => DotNetPath; + protected override string GenerateFullPathToTool() => Path.Combine(DotNetPath, ToolExe); /// /// Workaround to avoid storing user/pass into the EnvironmentVariables property, which gets logged by the task. @@ -63,8 +46,8 @@ private string DotNetPath /// protected override ProcessStartInfo GetProcessStartInfo(string pathToTool, string commandLineCommands, string responseFileSwitch) { - VSHostObject hostObj = new(HostObject, Log); - if (hostObj.TryGetCredentials() is (string user, string pass)) + VSHostObject hostObj = new VSHostObject(HostObject as System.Collections.Generic.IEnumerable); + if (hostObj.ExtractCredentials(out string user, out string pass, (string s) => Log.LogWarning(s))) { extractionInfo = (true, user, pass); } @@ -111,6 +94,14 @@ internal string GenerateCommandLineCommandsInt() { throw new InvalidOperationException(Resource.FormatString(nameof(Strings.RequiredPropertyNotSetOrEmpty), nameof(WorkingDirectory))); } + if (Entrypoint.Length == 0) + { + throw new InvalidOperationException(Resource.FormatString(nameof(Strings.RequiredItemsNotSet), nameof(Entrypoint))); + } + if (Entrypoint.Any(e => string.IsNullOrWhiteSpace(e.ItemSpec))) + { + throw new InvalidOperationException(Resource.FormatString(nameof(Strings.RequiredItemsContainsEmptyItems), nameof(Entrypoint))); + } CommandLineBuilder builder = new(); @@ -121,16 +112,14 @@ internal string GenerateCommandLineCommandsInt() builder.AppendSwitchIfNotNull("--baseimagename ", BaseImageName); builder.AppendSwitchIfNotNull("--repository ", Repository); builder.AppendSwitchIfNotNull("--workingdirectory ", WorkingDirectory); + ITaskItem[] sanitizedEntryPoints = Entrypoint.Where(e => !string.IsNullOrWhiteSpace(e.ItemSpec)).ToArray(); + builder.AppendSwitchIfNotNull("--entrypoint ", sanitizedEntryPoints, delimiter: " "); //optional options if (!string.IsNullOrWhiteSpace(BaseImageTag)) { builder.AppendSwitchIfNotNull("--baseimagetag ", BaseImageTag); } - if (!string.IsNullOrWhiteSpace(BaseImageDigest)) - { - builder.AppendSwitchIfNotNull("--baseimagedigest ", BaseImageDigest); - } if (!string.IsNullOrWhiteSpace(OutputRegistry)) { builder.AppendSwitchIfNotNull("--outputregistry ", OutputRegistry); @@ -139,20 +128,13 @@ internal string GenerateCommandLineCommandsInt() { builder.AppendSwitchIfNotNull("--localregistry ", LocalRegistry); } - if (!string.IsNullOrWhiteSpace(AppCommandInstruction)) - { - builder.AppendSwitchIfNotNull("--appcommandinstruction ", AppCommandInstruction); - } - if (!string.IsNullOrWhiteSpace(ImageFormat)) + + if (EntrypointArgs.Any(e => string.IsNullOrWhiteSpace(e.ItemSpec))) { - builder.AppendSwitchIfNotNull("--image-format ", ImageFormat); + Log.LogWarningWithCodeFromResources(nameof(Strings.EmptyValuesIgnored), nameof(EntrypointArgs)); } - - AppendSwitchIfNotNullSanitized(builder, "--entrypoint ", nameof(Entrypoint), Entrypoint); - AppendSwitchIfNotNullSanitized(builder, "--entrypointargs ", nameof(EntrypointArgs), EntrypointArgs); - AppendSwitchIfNotNullSanitized(builder, "--defaultargs ", nameof(DefaultArgs), DefaultArgs); - AppendSwitchIfNotNullSanitized(builder, "--appcommand ", nameof(AppCommand), AppCommand); - AppendSwitchIfNotNullSanitized(builder, "--appcommandargs ", nameof(AppCommandArgs), AppCommandArgs); + ITaskItem[] sanitizedEntryPointArgs = EntrypointArgs.Where(e => !string.IsNullOrWhiteSpace(e.ItemSpec)).ToArray(); + builder.AppendSwitchIfNotNull("--entrypointargs ", sanitizedEntryPointArgs, delimiter: " "); if (Labels.Any(e => string.IsNullOrWhiteSpace(e.ItemSpec))) { @@ -215,32 +197,7 @@ internal string GenerateCommandLineCommandsInt() builder.AppendSwitchIfNotNull("--container-user ", ContainerUser); } - if (!string.IsNullOrWhiteSpace(ArchiveOutputPath)) - { - builder.AppendSwitchIfNotNull("--archiveoutputpath ", ArchiveOutputPath); - } - - if (GenerateLabels) - { - builder.AppendSwitch("--generate-labels"); - } - - if (GenerateDigestLabel) - { - builder.AppendSwitch("--generate-digest-label"); - } - return builder.ToString(); - - void AppendSwitchIfNotNullSanitized(CommandLineBuilder builder, string commandArgName, string propertyName, ITaskItem[] value) - { - ITaskItem[] santized = value.Where(e => !string.IsNullOrWhiteSpace(e.ItemSpec)).ToArray(); - if (santized.Length != value.Length) - { - Log.LogWarningWithCodeFromResources(nameof(Strings.EmptyValuesIgnored), propertyName); - } - builder.AppendSwitchIfNotNull(commandArgName, santized, delimiter: " "); - } } } diff --git a/src/Containers/Microsoft.NET.Build.Containers/Tasks/ParseContainerProperties.cs b/src/Containers/Microsoft.NET.Build.Containers/Tasks/ParseContainerProperties.cs index bab45e1c1ce1..018d4ad04e9a 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Tasks/ParseContainerProperties.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/Tasks/ParseContainerProperties.cs @@ -1,6 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; +using System.Collections.Generic; +using System.Linq; using Microsoft.Build.Framework; using Microsoft.NET.Build.Containers.Resources; @@ -48,9 +51,6 @@ public sealed class ParseContainerProperties : Microsoft.Build.Utilities.Task [Output] public string ParsedContainerTag { get; private set; } - [Output] - public string ParsedContainerDigest { get; private set; } - [Output] public string NewContainerRegistry { get; private set; } @@ -74,7 +74,6 @@ public ParseContainerProperties() ParsedContainerRegistry = ""; ParsedContainerImage = ""; ParsedContainerTag = ""; - ParsedContainerDigest = ""; NewContainerRegistry = ""; NewContainerRepository = ""; NewContainerTags = Array.Empty(); @@ -86,13 +85,13 @@ public ParseContainerProperties() public override bool Execute() { string[] validTags; - if (!string.IsNullOrEmpty(ContainerImageTag) && ContainerImageTags.Length >= 1) + if (!String.IsNullOrEmpty(ContainerImageTag) && ContainerImageTags.Length >= 1) { Log.LogErrorWithCodeFromResources(nameof(Strings.AmbiguousTags), nameof(ContainerImageTag), nameof(ContainerImageTags)); return !Log.HasLoggedErrors; } - if (!string.IsNullOrEmpty(ContainerImageTag)) + if (!String.IsNullOrEmpty(ContainerImageTag)) { if (ContainerHelpers.IsValidImageTag(ContainerImageTag)) { @@ -104,12 +103,12 @@ public override bool Execute() Log.LogErrorWithCodeFromResources(nameof(Strings.InvalidTag), nameof(ContainerImageTag), ContainerImageTag); } } - else if (ContainerImageTags.Length != 0) + else if (ContainerImageTags.Length != 0 && TryValidateTags(ContainerImageTags, out var valids, out var invalids)) { - (validTags, var invalidTags) = TryValidateTags(ContainerImageTags); - if (invalidTags.Any()) + validTags = valids; + if (invalids.Any()) { - Log.LogErrorWithCodeFromResources(nameof(Strings.InvalidTags), nameof(ContainerImageTags), String.Join(",", invalidTags)); + Log.LogErrorWithCodeFromResources(nameof(Strings.InvalidTags), nameof(ContainerImageTags), String.Join(",", invalids)); return !Log.HasLoggedErrors; } } @@ -118,7 +117,7 @@ public override bool Execute() validTags = Array.Empty(); } - if (!string.IsNullOrEmpty(ContainerRegistry) && !ContainerHelpers.IsValidRegistry(ContainerRegistry)) + if (!String.IsNullOrEmpty(ContainerRegistry) && !ContainerHelpers.IsValidRegistry(ContainerRegistry)) { Log.LogErrorWithCodeFromResources(nameof(Strings.CouldntRecognizeRegistry), ContainerRegistry); return !Log.HasLoggedErrors; @@ -136,7 +135,7 @@ public override bool Execute() out string? outputReg, out string? outputImage, out string? outputTag, - out string? outputDigest, + out string? _outputDigest, out bool isRegistrySpecified)) { Log.LogErrorWithCodeFromResources(nameof(Strings.BaseImageNameParsingFailed), nameof(FullyQualifiedBaseImageName), FullyQualifiedBaseImageName); @@ -148,26 +147,28 @@ public override bool Execute() Log.LogWarningWithCodeFromResources(nameof(Strings.BaseImageNameRegistryFallback), nameof(FullyQualifiedBaseImageName), ContainerHelpers.DockerRegistryAlias); } - var (normalizedRepository, normalizationWarning, normalizationError) = ContainerHelpers.NormalizeRepository(ContainerRepository); - if (normalizedRepository is not null) + try { - NewContainerRepository = normalizedRepository; - } - if (normalizationWarning is (string warningMessageKey, object[] warningParams)) - { - Log.LogMessageFromResources(warningMessageKey, warningParams); + if (!ContainerHelpers.NormalizeRepository(ContainerRepository, out var normalizedRepository)) + { + Log.LogMessageFromResources(nameof(Strings.NormalizedContainerName), nameof(ContainerRepository), normalizedRepository); + NewContainerRepository = normalizedRepository!; // known to be not null due to output of NormalizeImageName + } + else + { + // name was valid already + NewContainerRepository = ContainerRepository; + } } - - if (normalizationError is (string errorMessageKey, object[] errorParams)) + catch (ArgumentException) { - Log.LogErrorWithCodeFromResources(errorMessageKey, errorParams); + Log.LogErrorWithCodeFromResources(nameof(Strings.InvalidContainerRepository), nameof(ContainerRepository), ContainerRepository); return !Log.HasLoggedErrors; } ParsedContainerRegistry = outputReg ?? ""; ParsedContainerImage = outputImage ?? ""; ParsedContainerTag = outputTag ?? ""; - ParsedContainerDigest = outputDigest ?? ""; NewContainerRegistry = ContainerRegistry; NewContainerTags = validTags; @@ -177,9 +178,8 @@ public override bool Execute() Log.LogMessage(MessageImportance.Low, "Host: {0}", ParsedContainerRegistry); Log.LogMessage(MessageImportance.Low, "Image: {0}", ParsedContainerImage); Log.LogMessage(MessageImportance.Low, "Tag: {0}", ParsedContainerTag); - Log.LogMessage(MessageImportance.Low, "Digest: {0}", ParsedContainerDigest); Log.LogMessage(MessageImportance.Low, "Image Name: {0}", NewContainerRepository); - Log.LogMessage(MessageImportance.Low, "Image Tags: {0}", string.Join(", ", NewContainerTags)); + Log.LogMessage(MessageImportance.Low, "Image Tags: {0}", String.Join(", ", NewContainerTags)); } return !Log.HasLoggedErrors; @@ -206,7 +206,7 @@ private void ValidateEnvironmentVariables() } } - private static (string[] validTags, string[] invalidTags) TryValidateTags(string[] inputTags) + private static bool TryValidateTags(string[] inputTags, out string[] validTags, out string[] invalidTags) { var v = new List(); var i = new List(); @@ -221,6 +221,8 @@ private static (string[] validTags, string[] invalidTags) TryValidateTags(string i.Add(tag); } } - return (v.ToArray(), i.ToArray()); + validTags = v.ToArray(); + invalidTags = i.ToArray(); + return invalidTags.Length == 0; } } diff --git a/src/Containers/Microsoft.NET.Build.Containers/VSHostObject.cs b/src/Containers/Microsoft.NET.Build.Containers/VSHostObject.cs index 89ea5b16ceae..9e793c7d2bf9 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/VSHostObject.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/VSHostObject.cs @@ -1,121 +1,47 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Reflection; -using System.Text.Json; +using System; +using System.Collections.Generic; +using System.Linq; using Microsoft.Build.Framework; -using Microsoft.Build.Utilities; namespace Microsoft.NET.Build.Containers.Tasks; -internal sealed class VSHostObject(ITaskHost? hostObject, TaskLoggingHelper log) +internal sealed class VSHostObject { private const string CredentialItemSpecName = "MsDeployCredential"; private const string UserMetaDataName = "UserName"; private const string PasswordMetaDataName = "Password"; + IEnumerable? _hostObject; - private readonly ITaskHost? _hostObject = hostObject; - private readonly TaskLoggingHelper _log = log; - - /// - /// Tries to extract credentials from the host object. - /// - /// A tuple of (username, password) if credentials were found with non-empty username, null otherwise. - public (string username, string password)? TryGetCredentials() + public VSHostObject(IEnumerable? hostObject) { - if (_hostObject is null) - { - return null; - } - - IEnumerable? taskItems = GetTaskItems(); - if (taskItems is null) - { - _log.LogMessage(MessageImportance.Low, "No task items found in host object."); - return null; - } - - ITaskItem? credentialItem = taskItems.FirstOrDefault(p => p.ItemSpec == CredentialItemSpecName); - if (credentialItem is null) - { - return null; - } - - string username = credentialItem.GetMetadata(UserMetaDataName); - if (string.IsNullOrEmpty(username)) - { - return null; - } - - string password = credentialItem.GetMetadata(PasswordMetaDataName); - return (username, password); + _hostObject = hostObject; } - private IEnumerable? GetTaskItems() + public bool ExtractCredentials(out string username, out string password, Action logMethod) { - try + bool retVal = false; + username = password = string.Empty; + if (_hostObject != null) { - // This call mirrors the behavior of Microsoft.WebTools.Publish.MSDeploy.VSMsDeployTaskHostObject.QueryAllTaskItems. - // Expected contract: - // - Instance method on the host object named "QueryAllTaskItems". - // - Signature: string QueryAllTaskItems(). - // - Returns a JSON array of objects with the shape: - // [{ "ItemSpec": "", "Metadata": { "": "", ... } }, ...] - // The JSON is deserialized into TaskItemDto records and converted to ITaskItem instances. - // Only UserName and Password metadata are extracted to avoid conflicts with reserved MSBuild metadata. - string? rawTaskItems = (string?)_hostObject!.GetType().InvokeMember( - "QueryAllTaskItems", - BindingFlags.InvokeMethod, - null, - _hostObject, - null); - - if (!string.IsNullOrEmpty(rawTaskItems)) + ITaskItem credentialItem = _hostObject.FirstOrDefault(p => p.ItemSpec == CredentialItemSpecName); + if (credentialItem != null) { - List? dtos = JsonSerializer.Deserialize>(rawTaskItems); - if (dtos is not null && dtos.Count > 0) + retVal = true; + username = credentialItem.GetMetadata(UserMetaDataName); + if (!string.IsNullOrEmpty(username)) { - _log.LogMessage(MessageImportance.Low, "Successfully retrieved task items via QueryAllTaskItems."); - return dtos.Select(ConvertToTaskItem).ToList(); + password = credentialItem.GetMetadata(PasswordMetaDataName); } - } - - _log.LogMessage(MessageImportance.Low, "QueryAllTaskItems returned null or empty result."); - } - catch (Exception ex) - { - _log.LogMessage(MessageImportance.Low, "Exception trying to call QueryAllTaskItems: {0}", ex.Message); - } - - // Fallback: try to use the host object directly as IEnumerable (legacy behavior). - if (_hostObject is IEnumerable enumerableHost) - { - _log.LogMessage(MessageImportance.Low, "Falling back to IEnumerable host object."); - return enumerableHost; - } - - return null; - - static TaskItem ConvertToTaskItem(TaskItemDto dto) - { - TaskItem taskItem = new(dto.ItemSpec ?? string.Empty); - if (dto.Metadata is not null) - { - if (dto.Metadata.TryGetValue(UserMetaDataName, out string? userName)) + else { - taskItem.SetMetadata(UserMetaDataName, userName); - } - - if (dto.Metadata.TryGetValue(PasswordMetaDataName, out string? password)) - { - taskItem.SetMetadata(PasswordMetaDataName, password); + logMethod("HostObject credentials not detected. Falling back to Docker credential retrieval."); } } - - return taskItem; } + return retVal; } - - private readonly record struct TaskItemDto(string? ItemSpec, Dictionary? Metadata); } diff --git a/src/Containers/Microsoft.NET.Build.Containers/net472Definitions.cs b/src/Containers/Microsoft.NET.Build.Containers/net472Definitions.cs new file mode 100644 index 000000000000..0574671472c2 --- /dev/null +++ b/src/Containers/Microsoft.NET.Build.Containers/net472Definitions.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace System.Runtime.CompilerServices +{ + internal static class IsExternalInit { } +} + +namespace Microsoft.NET.Build.Containers +{ + /// + /// Specifies that when a method returns , the parameter will not be null even if the corresponding type allows it. + /// + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] + sealed class NotNullWhenAttribute : Attribute + { + /// Initializes the attribute with the specified return value condition. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + public NotNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; + + /// Gets the return value condition. + public bool ReturnValue { get; } + } +} \ No newline at end of file diff --git a/src/Containers/containerize/ContainerizeCommand.cs b/src/Containers/containerize/ContainerizeCommand.cs index 8da04309a921..3c33cf18fff2 100644 --- a/src/Containers/containerize/ContainerizeCommand.cs +++ b/src/Containers/containerize/ContainerizeCommand.cs @@ -3,6 +3,7 @@ using System.CommandLine; using System.CommandLine.Parsing; +using System.Text; using Microsoft.DotNet.Cli.Utils; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Console; @@ -10,115 +11,87 @@ namespace containerize; -internal class ContainerizeCommand : RootCommand +internal class ContainerizeCommand : CliRootCommand { - internal Argument PublishDirectoryArgument { get; } = new Argument("PublishDirectory") + internal CliArgument PublishDirectoryArgument { get; } = new CliArgument("PublishDirectory") { Description = "The directory for the build outputs to be published." }.AcceptExistingOnly(); - internal Option BaseRegistryOption { get; } = new("--baseregistry") + internal CliOption BaseRegistryOption { get; } = new("--baseregistry") { Description = "The registry to use for the base image.", Required = true }; - internal Option BaseImageNameOption { get; } = new("--baseimagename") + internal CliOption BaseImageNameOption { get; } = new("--baseimagename") { Description = "The base image to pull.", Required = true }; - internal Option BaseImageTagOption { get; } = new("--baseimagetag") + internal CliOption BaseImageTagOption { get; } = new("--baseimagetag") { Description = "The base image tag. Ex: 6.0", DefaultValueFactory = (_) => "latest" }; - internal Option BaseImageDigestOption { get; } = new("--baseimagedigest") - { - Description = "The base image digest. Ex: sha256:6cec3641...", - Required = false - }; - - internal Option OutputRegistryOption { get; } = new("--outputregistry") + internal CliOption OutputRegistryOption { get; } = new("--outputregistry") { Description = "The registry to push to.", Required = false }; - internal Option ArchiveOutputPathOption { get; } = new("--archiveoutputpath") - { - Description = "The file path to which to write a tar.gz archive of the container image.", - Required = false - }; - - internal Option RepositoryOption { get; } = new("--repository") + internal CliOption RepositoryOption { get; } = new("--repository") { Description = "The name of the output container repository that will be pushed to the registry.", Required = true }; - internal Option ImageTagsOption { get; } = new("--imagetags") + internal CliOption ImageTagsOption { get; } = new("--imagetags") { Description = "The tags to associate with the new image.", AllowMultipleArgumentsPerToken = true }; - internal Option WorkingDirectoryOption { get; } = new("--workingdirectory") + internal CliOption WorkingDirectoryOption { get; } = new("--workingdirectory") { Description = "The working directory of the container.", Required = true }; - internal Option EntrypointOption { get; } = new("--entrypoint") + internal CliOption EntrypointOption { get; } = new("--entrypoint") { Description = "The entrypoint application of the container.", + Required = true, AllowMultipleArgumentsPerToken = true }; - internal Option EntrypointArgsOption { get; } = new("--entrypointargs") + internal CliOption EntrypointArgsOption { get; } = new("--entrypointargs") { Description = "Arguments to pass alongside Entrypoint.", AllowMultipleArgumentsPerToken = true }; - internal Option DefaultArgsOption { get; } = new Option("--defaultargs") + internal CliOption LocalRegistryOption { get; } = new CliOption("--localregistry") { - Description = "Default arguments passed. These can be overridden by the user when the container is created.", - AllowMultipleArgumentsPerToken = true - }; - - internal Option AppCommandOption { get; } = new("--appcommand") - { - Description = "The file name and arguments that launch the application. For example: ['dotnet', 'app.dll'].", - AllowMultipleArgumentsPerToken = true + Description = "The local registry to push to" }; - internal Option AppCommandArgsOption { get; } = new("--appcommandargs") + internal CliOption> LabelsOption { get; } = new("--labels") { - Description = "Arguments always passed to the application.", + Description = "Labels that the image configuration will include in metadata.", + CustomParser = result => ParseDictionary(result, errorMessage: "Incorrectly formatted labels: "), AllowMultipleArgumentsPerToken = true }; - internal Option AppCommandInstructionOption { get; } = new Option("--appcommandinstruction") - { - Description = "The Dockerfile instruction used for AppCommand. Can be set to 'DefaultArgs', 'Entrypoint', 'None', '' (default)." - }; - - internal Option LocalRegistryOption { get; } = new Option("--localregistry") - { - Description = "The local registry to push to." - }; - - internal Option> LabelsOption { get; } = new("--labels") + internal CliOption CmdOption { get; } = new CliOption("--cmd") { - Description = "Labels that the image configuration will include in metadata.", - CustomParser = result => ParseDictionary(result, errorMessage: "Incorrectly formatted labels: "), + Description = "The Cmd of the container image.", AllowMultipleArgumentsPerToken = true }; - internal Option PortsOption { get; } = new("--ports") + internal CliOption PortsOption { get; } = new("--ports") { Description = "Ports that the application declares that it will use. Note that this means nothing to container hosts, by default - it's mostly documentation. Ports should be of the form {number}/{type}, where {type} is tcp or udp", AllowMultipleArgumentsPerToken = true, @@ -178,85 +151,55 @@ internal class ContainerizeCommand : RootCommand } }; - internal Option> EnvVarsOption { get; } = new("--environmentvariables") + internal CliOption> EnvVarsOption { get; } = new("--environmentvariables") { Description = "Container environment variables to set.", CustomParser = result => ParseDictionary(result, errorMessage: "Incorrectly formatted environment variables: "), AllowMultipleArgumentsPerToken = true }; - internal Option RidOption { get; } = new("--rid") { Description = "Runtime Identifier of the generated container." }; - - internal Option RidGraphPathOption { get; } = new("--ridgraphpath") { Description = "Path to the RID graph file." }; + internal CliOption RidOption { get; } = new("--rid") { Description = "Runtime Identifier of the generated container." }; - internal Option ContainerUserOption { get; } = new("--container-user") { Description = "User to run the container as." }; + internal CliOption RidGraphPathOption { get; } = new("--ridgraphpath") { Description = "Path to the RID graph file." }; - internal Option GenerateLabelsOption { get; } = new("--generate-labels") - { - Description = "If true, the tooling may create labels on the generated images.", - Arity = ArgumentArity.Zero - }; - - internal Option GenerateDigestLabelOption { get; } = new("--generate-digest-label") - { - Description = "If true, the tooling will generate an 'org.opencontainers.image.base.digest' label on the generated images containing the digest of the chosen base image.", - Arity = ArgumentArity.Zero - }; - - internal Option ImageFormatOption { get; } = new("--image-format") - { - Description = "If set to OCI or Docker will force the generated image to be that format. If unset, the base images format will be used." - }; + internal CliOption ContainerUserOption { get; } = new("--container-user") { Description = "User to run the container as." }; internal ContainerizeCommand() : base("Containerize an application without Docker.") { PublishDirectoryArgument.AcceptLegalFilePathsOnly(); - Arguments.Add(PublishDirectoryArgument); - Options.Add(BaseRegistryOption); - Options.Add(BaseImageNameOption); - Options.Add(BaseImageTagOption); - Options.Add(BaseImageDigestOption); - Options.Add(OutputRegistryOption); - Options.Add(ArchiveOutputPathOption); - Options.Add(RepositoryOption); - Options.Add(ImageTagsOption); - Options.Add(WorkingDirectoryOption); - Options.Add(EntrypointOption); - Options.Add(EntrypointArgsOption); - Options.Add(DefaultArgsOption); - Options.Add(AppCommandOption); - Options.Add(AppCommandArgsOption); - Options.Add(AppCommandInstructionOption); - Options.Add(LabelsOption); - Options.Add(PortsOption); - Options.Add(EnvVarsOption); - Options.Add(RidOption); - Options.Add(RidGraphPathOption); + this.Arguments.Add(PublishDirectoryArgument); + this.Options.Add(BaseRegistryOption); + this.Options.Add(BaseImageNameOption); + this.Options.Add(BaseImageTagOption); + this.Options.Add(OutputRegistryOption); + this.Options.Add(RepositoryOption); + this.Options.Add(ImageTagsOption); + this.Options.Add(WorkingDirectoryOption); + this.Options.Add(EntrypointOption); + this.Options.Add(EntrypointArgsOption); + this.Options.Add(CmdOption); + this.Options.Add(LabelsOption); + this.Options.Add(PortsOption); + this.Options.Add(EnvVarsOption); + this.Options.Add(RidOption); + this.Options.Add(RidGraphPathOption); LocalRegistryOption.AcceptOnlyFromAmong(KnownLocalRegistryTypes.SupportedLocalRegistryTypes); - Options.Add(LocalRegistryOption); - Options.Add(ContainerUserOption); - Options.Add(GenerateLabelsOption); - Options.Add(GenerateDigestLabelOption); - Options.Add(ImageFormatOption); + this.Options.Add(LocalRegistryOption); + this.Options.Add(ContainerUserOption); - SetAction(async (parseResult, cancellationToken) => + this.SetAction(async (parseResult, cancellationToken) => { DirectoryInfo _publishDir = parseResult.GetValue(PublishDirectoryArgument)!; string _baseReg = parseResult.GetValue(BaseRegistryOption)!; string _baseName = parseResult.GetValue(BaseImageNameOption)!; string _baseTag = parseResult.GetValue(BaseImageTagOption)!; - string? _baseDigest = parseResult.GetValue(BaseImageDigestOption); string? _outputReg = parseResult.GetValue(OutputRegistryOption); - string? _archiveOutputPath = parseResult.GetValue(ArchiveOutputPathOption); string _name = parseResult.GetValue(RepositoryOption)!; string[] _tags = parseResult.GetValue(ImageTagsOption)!; string _workingDir = parseResult.GetValue(WorkingDirectoryOption)!; - string[] _entrypoint = parseResult.GetValue(EntrypointOption) ?? Array.Empty(); - string[] _entrypointArgs = parseResult.GetValue(EntrypointArgsOption) ?? Array.Empty(); - string[] _defaultArgs = parseResult.GetValue(DefaultArgsOption) ?? Array.Empty(); - string[] _appCommand = parseResult.GetValue(AppCommandOption) ?? Array.Empty(); - string[] _appCommandArgs = parseResult.GetValue(AppCommandArgsOption) ?? Array.Empty(); - string _appCommandInstruction = parseResult.GetValue(AppCommandInstructionOption) ?? ""; + string[] _entrypoint = parseResult.GetValue(EntrypointOption)!; + string[]? _cmdArgs = parseResult.GetValue(CmdOption); + string[]? _entrypointArgs = parseResult.GetValue(EntrypointArgsOption); Dictionary _labels = parseResult.GetValue(LabelsOption) ?? new Dictionary(); Port[]? _ports = parseResult.GetValue(PortsOption); Dictionary _envVars = parseResult.GetValue(EnvVarsOption) ?? new Dictionary(); @@ -264,9 +207,6 @@ internal ContainerizeCommand() : base("Containerize an application without Docke string _ridGraphPath = parseResult.GetValue(RidGraphPathOption)!; string _localContainerDaemon = parseResult.GetValue(LocalRegistryOption)!; string? _containerUser = parseResult.GetValue(ContainerUserOption); - bool _generateLabels = parseResult.GetValue(GenerateLabelsOption); - bool _generateDigestLabel = parseResult.GetValue(GenerateDigestLabelOption); - KnownImageFormats? _imageFormat = parseResult.GetValue(ImageFormatOption); //setup basic logging bool traceEnabled = Env.GetEnvironmentVariableAsBool("CONTAINERIZE_TRACE_LOGGING_ENABLED"); @@ -279,13 +219,8 @@ await ContainerBuilder.ContainerizeAsync( _baseReg, _baseName, _baseTag, - _baseDigest, _entrypoint, - _entrypointArgs, - _defaultArgs, - _appCommand, - _appCommandArgs, - _appCommandInstruction, + _cmdArgs, _name, _tags, _outputReg, @@ -296,10 +231,6 @@ await ContainerBuilder.ContainerizeAsync( _ridGraphPath, _localContainerDaemon, _containerUser, - _archiveOutputPath, - _generateLabels, - _generateDigestLabel, - _imageFormat, loggerFactory, cancellationToken).ConfigureAwait(false); }); diff --git a/src/Containers/containerize/Program.cs b/src/Containers/containerize/Program.cs index abd5b7ce9483..a2c36b2acebf 100644 --- a/src/Containers/containerize/Program.cs +++ b/src/Containers/containerize/Program.cs @@ -1,6 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.CommandLine; +using System.CommandLine.Parsing; + namespace containerize; internal class Program diff --git a/src/Containers/containerize/containerize.csproj b/src/Containers/containerize/containerize.csproj index 11916b8c05ef..43841176b7db 100644 --- a/src/Containers/containerize/containerize.csproj +++ b/src/Containers/containerize/containerize.csproj @@ -1,24 +1,24 @@  - $(SdkTargetFramework) Exe + $(SdkTargetFramework) enable + enable MicrosoftShared true - true - false + true - - - + + + - - + + @@ -26,11 +26,11 @@ - + - + - + \ No newline at end of file diff --git a/src/Containers/docs/ReleaseNotes/v7.0.400.md b/src/Containers/docs/ReleaseNotes/v7.0.400.md index 0986e20ab196..94ae3054009b 100644 --- a/src/Containers/docs/ReleaseNotes/v7.0.400.md +++ b/src/Containers/docs/ReleaseNotes/v7.0.400.md @@ -11,13 +11,13 @@ This version brings the following new features and enhancements: In addition, we fixed some protocol bugs that blocked usage with registries like Harbor. * Several environment variables were added to allow more explicit control over the layer upload process: - * DOTNET_CONTAINER_REGISTRY_PARALLEL_UPLOAD + * SDK_CONTAINER_REGISTRY_PARALLEL_UPLOAD * determines if layers of the generated image can be uploaded in parallel or in series. * defaults to `true` for all registries except AWS ECR - * DOTNET_CONTAINER_DEBUG_REGISTRY_FORCE_CHUNKED_UPLOAD + * SDK_CONTAINER_DEBUG_REGISTRY_FORCE_CHUNKED_UPLOAD * if set to `true`, we will always try to upload layers in chunks instead of all in one upload. * defaults to `false`. - * DOTNET_CONTAINER_REGISTRY_CHUNKED_UPLOAD_SIZE_BYTES + * SDK_CONTAINER_REGISTRY_CHUNKED_UPLOAD_SIZE_BYTES * allows for explicit control over the size of the chunks uploaded when using chunked uploads. * note that by default we prefer atomic uploads, so setting this might not have any impact if your registry supports atomic uploads. * does not have a default, but the default chunk size is 64Kb. diff --git a/src/Containers/packaging/build/Microsoft.NET.Build.Containers.props b/src/Containers/packaging/build/Microsoft.NET.Build.Containers.props index 9f5a06e56fdf..669e742dc309 100644 --- a/src/Containers/packaging/build/Microsoft.NET.Build.Containers.props +++ b/src/Containers/packaging/build/Microsoft.NET.Build.Containers.props @@ -3,11 +3,9 @@ true tasks - net11.0 + net8.0 net472 containerize - - true $(MSBuildThisFileDirectory)..\$(ContainerTaskFolderName)\$(ContainerTaskFramework)\ $(MSBuildThisFileDirectory)..\$(ContainerizeFolderName)\ @@ -17,7 +15,6 @@ - - + diff --git a/src/Containers/packaging/build/Microsoft.NET.Build.Containers.targets b/src/Containers/packaging/build/Microsoft.NET.Build.Containers.targets index ae67c5c24fce..9c3d0b17e8d3 100644 --- a/src/Containers/packaging/build/Microsoft.NET.Build.Containers.targets +++ b/src/Containers/packaging/build/Microsoft.NET.Build.Containers.targets @@ -12,11 +12,7 @@ ) )">true <_ContainerIsTargetingNet8TFM>false - <_ContainerIsTargetingNet8TFM Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp' And $([MSBuild]::VersionGreaterThanOrEquals($(_TargetFrameworkVersionWithoutV), '8.0'))">true - <_ContainerIsSelfContained>false - <_ContainerIsSelfContained Condition="'$(SelfContained)' == 'true' or '$(PublishSelfContained)' == 'true'">true - - true + <_ContainerIsTargetingNet8TFM Condition="$([MSBuild]::IsTargetFrameworkCompatible($(TargetFramework), 'net8.0'))">true @@ -33,45 +29,37 @@ + + + + + + + Returns="$(ContainerBaseImage)" + DependsOnTargets="_ComputeContainerBaseImageTag"> - - $(RuntimeIdentifier) - $(RuntimeIdentifiers) - linux-$(NETCoreSdkPortableRuntimeIdentifier.Split('-')[1]) - - <_InitialContainerBaseImage>$(ContainerBaseImage) - + + <_IsSelfContained Condition="'$(SelfContained)' == 'true' or '$(PublishSelfContained)' == 'true'">true + <_IsAspNet Condition="@(FrameworkReference->Count()) > 0 and @(FrameworkReference->AnyHaveMetadataValue('Identity', 'Microsoft.AspnetCore.App'))">true - - <_TargetRuntimeIdentifiers Include="$(ContainerRuntimeIdentifier)" Condition="'$(ContainerRuntimeIdentifier)' != ''" /> - <_TargetRuntimeIdentifiers Include="$(ContainerRuntimeIdentifiers)" Condition="@(_TargetRuntimeIdentifiers->Count()) == 0" /> - + + <_ContainerBaseRegistry>mcr.microsoft.com + <_ContainerBaseImageName Condition="'$(_IsSelfContained)' == 'true'">dotnet/runtime-deps + <_ContainerBaseImageName Condition="'$(_ContainerBaseImageName)' == '' and '$(_IsAspNet)' == 'true'">dotnet/aspnet + <_ContainerBaseImageName Condition="'$(_ContainerBaseImageName)' == ''">dotnet/runtime - - - + + <_ContainerIsUsingMicrosoftDefaultImages Condition="'$(ContainerBaseImage)' != ''">false + <_ContainerIsUsingMicrosoftDefaultImages Condition="'$(ContainerBaseImage)' == ''">true - - <_TargetRuntimeIdentifiers Remove ="$(_TargetRuntimeIdentifiers)" /> - + $(_ContainerBaseRegistry)/$(_ContainerBaseImageName):$(_ContainerBaseImageTag) + - - + $(RegistryUrl) @@ -100,6 +88,32 @@ latest $([System.DateTime]::UtcNow.ToString('yyyyMMddhhmmss')) + + + $(RuntimeIdentifier) + linux-$(NETCoreSdkPortableRuntimeIdentifier.Split('-')[1]) + <_ContainerIsTargetingWindows>false + <_ContainerIsTargetingWindows Condition="$(ContainerRuntimeIdentifier.StartsWith('win'))">true + + + /app + C:\app + + + + + + + + + + + + + ContainerUser + + 64198 - @@ -134,7 +147,6 @@ true true true - true @@ -161,19 +173,12 @@ - - - - - <_TrimmedRepositoryUrl Condition="'$(RepositoryType)' == 'git' and '$(PrivateRepositoryUrl)' != '' and $(PrivateRepositoryUrl.EndsWith('.git'))">$(PrivateRepositoryUrl.Substring(0, $(PrivateRepositoryUrl.LastIndexOf('.git')))) - <_TrimmedRepositoryUrl Condition="'$(_TrimmedRepositoryUrl)' == '' and '$(PrivateRepositoryUrl)' != ''">$(PrivateRepositoryUrl) - + In addition, the 'nice' property names are currently set by NuGet Pack targets and so we have to use the private/generic names here. --> - + @@ -181,77 +186,12 @@ _ContainerVerifySDKVersion; - ComputeContainerConfig; - _CheckContainersPackage; + ComputeContainerConfig - - - - - <_ContainerIsTargetingWindows>false - <_ContainerIsTargetingWindows Condition="$(ContainerRuntimeIdentifier.StartsWith('win'))">true + - - /app/ - C:\app\ - - <_ContainerIsUsingMicrosoftDefaultImages Condition="'$(_InitialContainerBaseImage)' == ''">true - <_ContainerIsUsingMicrosoftDefaultImages Condition="'$(_InitialContainerBaseImage)' != ''">false - - - - - ContainerUser - - - - - - - - - - - - - - <_ContainersPackageIdentity>Microsoft.NET.Build.Containers - <_WebDefaultSdkVersion>7.0.300 - <_WorkerDefaultSdkVersion>8.0.100 - <_ConsoleDefaultSdkVersion>8.0.200 - - - <_SdkCanPublishWeb>$([MSBuild]::VersionGreaterThanOrEquals('$(NETCoreSdkVersion)', '$(_WebDefaultSdkVersion)')) - <_SdkCanPublishWorker>$([MSBuild]::VersionGreaterThanOrEquals('$(NETCoreSdkVersion)', '$(_WorkerDefaultSdkVersion)')) - <_SdkCanPublishConsole>$([MSBuild]::VersionGreaterThanOrEquals('$(NETCoreSdkVersion)', '$(_ConsoleDefaultSdkVersion)')) - - - <_ContainerPackageIsPresent>false - <_ContainerPackageIsPresent Condition="@(PackageReference->AnyHaveMetadataValue('Identity', '$(_ContainersPackageIdentity)'))">true - <_IsWebProject>false - <_IsWebProject Condition="@(ProjectCapability->AnyHaveMetadataValue('Identity', 'DotNetCoreWeb'))">true - <_IsWorkerProject>false - <_IsWorkerProject Condition="@(ProjectCapability->AnyHaveMetadataValue('Identity', 'DotNetCoreWorker'))">true - - - - - - - $(NetCoreRoot) dotnet @@ -264,11 +204,8 @@ BaseRegistry="$(ContainerBaseRegistry)" BaseImageName="$(ContainerBaseName)" BaseImageTag="$(ContainerBaseTag)" - BaseImageDigest="$(ContainerBaseDigest)" - ImageFormat="$(ContainerImageFormat)" LocalRegistry="$(LocalRegistry)" OutputRegistry="$(ContainerRegistry)" - ArchiveOutputPath="$(ContainerArchiveOutputPath)" Repository="$(ContainerRepository)" ImageTags="@(ContainerImageTags)" PublishDirectory="$(PublishDir)" @@ -284,161 +221,10 @@ ContainerEnvironmentVariables="@(ContainerEnvironmentVariables)" ContainerRuntimeIdentifier="$(ContainerRuntimeIdentifier)" ContainerUser="$(ContainerUser)" - RuntimeIdentifierGraphPath="$(RuntimeIdentifierGraphPath)" - SkipPublishing="$(_SkipContainerPublishing)" - GenerateLabels="$(ContainerGenerateLabels)" - GenerateDigestLabel="$(ContainerGenerateLabelsImageBaseDigest)"> + RuntimeIdentifierGraphPath="$(RuntimeIdentifierGraphPath)"> - - - - - - - - - $(GeneratedContainerManifest) - $(GeneratedContainerConfiguration) - $(GeneratedContainerDigest) - $(GeneratedContainerMediaType) - - - - - - - - - - <_SkipContainerPublishing>false - <_SkipContainerPublishing Condition="$(ContainerArchiveOutputPath) != '' or ( $(ContainerRegistry) == '' and ( $(LocalRegistry) == '' or $(LocalRegistry) == 'Docker' ) )">true - - - <_SkipCreateImageIndex>false - <_SkipCreateImageIndex Condition="$(ContainerArchiveOutputPath) == '' and $(ContainerRegistry) == '' and $(LocalRegistry) == 'Podman'">true - - - - <_SingleImageContainerFormat Condition="'$(ContainerImageFormat)' != ''">$(ContainerImageFormat) - - <_SingleImageContainerFormat Condition="$(_SkipContainerPublishing) == 'true' ">OCI - - - - <_rids Include="$(ContainerRuntimeIdentifiers)" Condition="'$(ContainerRuntimeIdentifiers)' != ''" /> - <_rids Include="$(RuntimeIdentifiers)" Condition="'$(ContainerRuntimeIdentifiers)' == '' and '$(RuntimeIdentifiers)' != ''" /> - <_InnerBuild - Include="$(MSBuildProjectFullPath)" - AdditionalProperties=" - ContainerRuntimeIdentifier=%(_rids.Identity); - RuntimeIdentifier=%(_rids.Identity); - ContainerBaseRegistry=$(ContainerBaseRegistry); - ContainerBaseName=$(ContainerBaseName); - ContainerBaseTag=$(ContainerBaseTag); - ContainerBaseDigest=$(ContainerBaseDigest); - ContainerRegistry=$(ContainerRegistry); - _ContainerImageTags=@(ContainerImageTags, ';'); - ContainerRepository=$(ContainerRepository); - _ContainerLabel=@(ContainerLabel->'%(Identity):%(Value)'); - _ContainerPort=@(ContainerPort->'%(Identity):%(Type)'); - _ContainerEnvironmentVariables=@(ContainerEnvironmentVariable->'%(Identity):%(Value)'); - ContainerGenerateLabels=$(ContainerGenerateLabels); - ContainerGenerateLabelsImageBaseDigest=$(ContainerGenerateLabelsImageBaseDigest); - _SkipContainerPublishing=$(_SkipContainerPublishing); - ContainerImageFormat=$(_SingleImageContainerFormat); - _IsMultiRIDBuild=false; - _IsSingleRIDBuild=true; - _InitialContainerBaseImage=$(_InitialContainerBaseImage) - "/> - <_rids Remove ="$(_rids)" /> - - - - - - - - - - - - - - - - - - <_ParsedContainerLabel - Condition="'$(_ContainerLabel)' != ':'" - Include="$(_ContainerLabel)"/> - - - <_ParsedContainerPort - Condition="'$(_ContainerPort)' != ':'" - Include="$(_ContainerPort)"/> - - - <_ParsedContainerEnvironmentVariables - Condition="'$(_ContainerEnvironmentVariables)' != ':'" - Include="$(_ContainerEnvironmentVariables)"/> - - - - - - - <_IsMultiTFMBuild Condition="'$(TargetFrameworks)' != '' and '$(TargetFramework)' == ''">true - - <_HasCRIDsAndNoCRID Condition="'$(ContainerRuntimeIdentifiers)' != '' and '$(ContainerRuntimeIdentifier)' == ''">true - <_HasRIDs Condition="'$(RuntimeIdentifiers)' != ''">true - <_NoCRIDsOrCRIDorRID Condition="'$(ContainerRuntimeIdentifiers)' == '' and '$(ContainerRuntimeIdentifier)' == '' and '$(RuntimeIdentifier)' == ''">true - - <_IsMultiRIDBuild Condition="'$(BuildingInsideVisualStudio)' != 'true' and ('$(_HasCRIDsAndNoCRID)' == true or ('$(_HasRIDs)' == 'true' and '$(_NoCRIDsOrCRIDorRID)' == 'true'))">true - <_IsSingleRIDBuild Condition="'$(_IsMultiRIDBuild)' == ''">true - - - - - - - - - - - - - diff --git a/src/Containers/packaging/package.csproj b/src/Containers/packaging/package.csproj index 81b757e7336a..63c1be214bf9 100644 --- a/src/Containers/packaging/package.csproj +++ b/src/Containers/packaging/package.csproj @@ -1,4 +1,4 @@ - + $(SdkTargetFramework) true @@ -6,9 +6,10 @@ true true NU5100;NU5128 + net472 - NU5100;NU5128;NU1507 + NU5100;NU5128 Microsoft Microsoft Microsoft.NET.Build.Containers @@ -25,42 +26,38 @@ + OutputItemType="ContainerLibraryOutput"/> + + + SetTargetFramework="TargetFramework=$(VSCompatTargetFramework)" + OutputItemType="ContainerLibraryOutputNet472" Condition="'$(DotNetBuildFromSource)' != 'true'" /> - + - ../docs/ReleaseNotes/v8.0.300.md + ../docs/ReleaseNotes/v7.0.400.md $([System.IO.File]::ReadAllText($(PackageReleaseNotesFile))) - - + + - - + - + %(_AllNet472ContainerTaskDependencies.NuGetIsFrameworkReference) != true" /> + @@ -72,11 +69,11 @@ - + - + - + @@ -89,7 +86,7 @@ $([MSBuild]::ValueOrDefault('%(_AllNetContainerTaskDependencies.NuGetPackageId)', '').Contains('Microsoft.Extensions')) ) and %(_AllNetContainerTaskDependencies.NuGetIsFrameworkReference) != true" /> - + @@ -97,6 +94,7 @@ + @@ -111,14 +109,14 @@ + DestinationFiles="$(ArtifactsTmpDir)Container/package/Microsoft.NET.Build.Containers.$(Version).nupkg" /> + DestinationFiles="$(ArtifactsTmpDir)Container/packaging/Microsoft.NET.Build.Containers.props" /> + DestinationFiles="$(ArtifactsTmpDir)Container/packaging/Microsoft.NET.Build.Containers.targets" /> diff --git a/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/CapturingLogger.cs b/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/CapturingLogger.cs new file mode 100644 index 000000000000..fbd26d787895 --- /dev/null +++ b/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/CapturingLogger.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Build.Framework; + +namespace Microsoft.NET.Build.Containers.IntegrationTests; + +public class CapturingLogger : ILogger +{ + public LoggerVerbosity Verbosity { get => LoggerVerbosity.Diagnostic; set { } } + public string Parameters { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + + private List _messages = new(); + public IReadOnlyList Messages {get { return _messages; } } + + private List _warnings = new(); + public IReadOnlyList Warnings {get { return _warnings; } } + + private List _errors = new(); + public IReadOnlyList Errors {get { return _errors; } } + + public List AllMessages => Errors.Select(e => e.Message!).Concat(Warnings.Select(w => w.Message!)).Concat(Messages.Select(m => m.Message!)).ToList(); + + public void Initialize(IEventSource eventSource) + { + eventSource.MessageRaised += (o, e) => _messages.Add(e); + eventSource.WarningRaised += (o, e) => _warnings.Add(e); + eventSource.ErrorRaised += (o, e) => _errors.Add(e); + } + + + public void Shutdown() + { + } +} diff --git a/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/ContainerCli.cs b/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/ContainerCli.cs new file mode 100644 index 000000000000..dbca93bc1817 --- /dev/null +++ b/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/ContainerCli.cs @@ -0,0 +1,57 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Linq; +using System.Diagnostics; +using Microsoft.NET.TestFramework.Commands; +using Xunit.Abstractions; +using Microsoft.NET.TestFramework; + +namespace Microsoft.NET.Build.Containers.IntegrationTests; + +static class ContainerCli +{ + public static bool IsPodman => _isPodman.Value; + + public static RunExeCommand PullCommand(ITestOutputHelper log, params string[] args) + => CreateCommand(log, "pull", args); + + public static RunExeCommand TagCommand(ITestOutputHelper log, params string[] args) + => CreateCommand(log, "tag", args); + + public static RunExeCommand PushCommand(ITestOutputHelper log, params string[] args) + => CreateCommand(log, "push", args); + + public static RunExeCommand StopCommand(ITestOutputHelper log, params string[] args) + => CreateCommand(log, "stop", args); + + public static RunExeCommand RunCommand(ITestOutputHelper log, params string[] args) + => CreateCommand(log, "run", args); + + public static RunExeCommand LogsCommand(ITestOutputHelper log, params string[] args) + => CreateCommand(log, "logs", args); + + private static RunExeCommand CreateCommand(ITestOutputHelper log, string command, string[] args) + { + string commandPath = IsPodman ? "podman" : "docker"; + + // The local registry is not accessible via https. + // Podman doesn't want to use it unless we set 'tls-verify' to 'false'. + if (IsPodman && (command == "push" || command == "pull")) + { + if (args.Length > 0) + { + string image = args[args.Length - 1]; + if (image.StartsWith($"{DockerRegistryManager.LocalRegistry}/")) + { + args = new[] { "--tls-verify=false" }.Concat(args).ToArray(); + } + } + } + + return new RunExeCommand(log, commandPath, new[] { command }.Concat(args).ToArray()); + } + + private static readonly Lazy _isPodman = + new(() => new DockerCli(loggerFactory: new TestLoggerFactory()).GetCommand() == DockerCli.PodmanCommand); +} diff --git a/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/CreateNewImageTests.cs b/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/CreateNewImageTests.cs new file mode 100644 index 000000000000..4e2443003f2e --- /dev/null +++ b/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/CreateNewImageTests.cs @@ -0,0 +1,308 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Build.Utilities; +using FluentAssertions; +using Xunit; +using Xunit.Abstractions; +using Microsoft.NET.Build.Containers.IntegrationTests; +using Microsoft.NET.Build.Containers.UnitTests; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Commands; +using Microsoft.NET.TestFramework; +using FakeItEasy; +using Microsoft.Build.Framework; +using System.Runtime.CompilerServices; +using Microsoft.Extensions.Logging; + +namespace Microsoft.NET.Build.Containers.Tasks.IntegrationTests; + +[Collection("Docker tests")] +public class CreateNewImageTests +{ + private ITestOutputHelper _testOutput; + + public CreateNewImageTests(ITestOutputHelper testOutput) + { + _testOutput = testOutput; + } + + [DockerAvailableFact] + public void CreateNewImage_Baseline() + { + DirectoryInfo newProjectDir = new(GetTestDirectoryName()); + if (newProjectDir.Exists) + { + newProjectDir.Delete(recursive: true); + } + + newProjectDir.Create(); + + new DotnetNewCommand(_testOutput, "console", "-f", ToolsetInfo.CurrentTargetFramework) + .WithVirtualHive() + .WithWorkingDirectory(newProjectDir.FullName) + .Execute() + .Should().Pass(); + + new DotnetCommand(_testOutput, "publish", "-c", "Release", "-r", "linux-arm64", "--no-self-contained") + .WithWorkingDirectory(newProjectDir.FullName) + .Execute() + .Should().Pass(); + + CreateNewImage task = new(); + + (IBuildEngine buildEngine, List errors) = SetupBuildEngine(); + task.BuildEngine = buildEngine; + + task.BaseRegistry = "mcr.microsoft.com"; + task.BaseImageName = "dotnet/runtime"; + task.BaseImageTag = "7.0"; + + task.OutputRegistry = "localhost:5010"; + task.LocalRegistry = "Docker"; + task.PublishDirectory = Path.Combine(newProjectDir.FullName, "bin", "Release", ToolsetInfo.CurrentTargetFramework, "linux-arm64", "publish"); + task.Repository = "dotnet/create-new-image-baseline"; + task.ImageTags = new[] { "latest" }; + task.WorkingDirectory = "app/"; + task.ContainerRuntimeIdentifier = "linux-arm64"; + task.Entrypoint = new TaskItem[] { new("dotnet"), new("build") }; + task.RuntimeIdentifierGraphPath = ToolsetUtils.GetRuntimeGraphFilePath(); + + Assert.True(task.Execute(), FormatBuildMessages(errors)); + newProjectDir.Delete(true); + } + + [DockerAvailableFact] + public void ParseContainerProperties_EndToEnd() + { + DirectoryInfo newProjectDir = new(GetTestDirectoryName()); + + if (newProjectDir.Exists) + { + newProjectDir.Delete(recursive: true); + } + + newProjectDir.Create(); + + new DotnetNewCommand(_testOutput, "console", "-f", ToolsetInfo.CurrentTargetFramework) + .WithVirtualHive() + .WithWorkingDirectory(newProjectDir.FullName) + .Execute() + .Should().Pass(); + + new DotnetCommand(_testOutput, "build", "--configuration", "release") + .WithWorkingDirectory(newProjectDir.FullName) + .Execute() + .Should().Pass(); + + ParseContainerProperties pcp = new(); + (IBuildEngine buildEngine, List errors) = SetupBuildEngine(); + pcp.BuildEngine = buildEngine; + + pcp.FullyQualifiedBaseImageName = "mcr.microsoft.com/dotnet/runtime:7.0"; + pcp.ContainerRegistry = "localhost:5010"; + pcp.ContainerRepository = "dotnet/testimage"; + pcp.ContainerImageTags = new[] { "5.0", "latest" }; + + Assert.True(pcp.Execute(), FormatBuildMessages(errors)); + Assert.Equal("mcr.microsoft.com", pcp.ParsedContainerRegistry); + Assert.Equal("dotnet/runtime", pcp.ParsedContainerImage); + Assert.Equal("7.0", pcp.ParsedContainerTag); + + Assert.Equal("dotnet/testimage", pcp.NewContainerRepository); + pcp.NewContainerTags.Should().BeEquivalentTo(new[] { "5.0", "latest" }); + + CreateNewImage cni = new(); + (buildEngine, errors) = SetupBuildEngine(); + cni.BuildEngine = buildEngine; + + cni.BaseRegistry = pcp.ParsedContainerRegistry; + cni.BaseImageName = pcp.ParsedContainerImage; + cni.BaseImageTag = pcp.ParsedContainerTag; + cni.Repository = pcp.NewContainerRepository; + cni.OutputRegistry = "localhost:5010"; + cni.PublishDirectory = Path.Combine(newProjectDir.FullName, "bin", "release", ToolsetInfo.CurrentTargetFramework); + cni.WorkingDirectory = "app/"; + cni.Entrypoint = new TaskItem[] { new(newProjectDir.Name) }; + cni.ImageTags = pcp.NewContainerTags; + cni.ContainerRuntimeIdentifier = "linux-x64"; + cni.RuntimeIdentifierGraphPath = ToolsetUtils.GetRuntimeGraphFilePath(); + + Assert.True(cni.Execute(), FormatBuildMessages(errors)); + newProjectDir.Delete(true); + } + + /// + /// Creates a console app that outputs the environment variable added to the image. + /// + [DockerAvailableFact] + public void Tasks_EndToEnd_With_EnvironmentVariable_Validation() + { + DirectoryInfo newProjectDir = new(GetTestDirectoryName()); + + if (newProjectDir.Exists) + { + newProjectDir.Delete(recursive: true); + } + + newProjectDir.Create(); + + new DotnetNewCommand(_testOutput, "console", "-f", ToolsetInfo.CurrentTargetFramework) + .WithVirtualHive() + .WithWorkingDirectory(newProjectDir.FullName) + .Execute() + .Should().Pass(); + + File.WriteAllText(Path.Combine(newProjectDir.FullName, "Program.cs"), $"Console.Write(Environment.GetEnvironmentVariable(\"GoodEnvVar\"));"); + + new DotnetCommand(_testOutput, "build", "--configuration", "release", "/p:runtimeidentifier=linux-x64", $"/p:RuntimeFrameworkVersion=8.0.0-preview.3.23174.8") + .WithWorkingDirectory(newProjectDir.FullName) + .Execute() + .Should().Pass(); + + ParseContainerProperties pcp = new(); + (IBuildEngine buildEngine, List errors) = SetupBuildEngine(); + pcp.BuildEngine = buildEngine; + + pcp.FullyQualifiedBaseImageName = $"mcr.microsoft.com/{DockerRegistryManager.RuntimeBaseImage}:{DockerRegistryManager.Net8PreviewImageTag}"; + pcp.ContainerRegistry = ""; + pcp.ContainerRepository = "dotnet/envvarvalidation"; + pcp.ContainerImageTag = "latest"; + + Dictionary dict = new Dictionary(); + dict.Add("Value", "Foo"); + + pcp.ContainerEnvironmentVariables = new[] { new TaskItem("B@dEnv.Var", dict), new TaskItem("GoodEnvVar", dict) }; + + Assert.True(pcp.Execute(), FormatBuildMessages(errors)); + Assert.Equal("mcr.microsoft.com", pcp.ParsedContainerRegistry); + Assert.Equal("dotnet/runtime", pcp.ParsedContainerImage); + Assert.Equal(DockerRegistryManager.Net8PreviewImageTag, pcp.ParsedContainerTag); + Assert.Single(pcp.NewContainerEnvironmentVariables); + Assert.Equal("Foo", pcp.NewContainerEnvironmentVariables[0].GetMetadata("Value")); + + Assert.Equal("dotnet/envvarvalidation", pcp.NewContainerRepository); + Assert.Equal("latest", pcp.NewContainerTags[0]); + + CreateNewImage cni = new(); + (buildEngine, errors) = SetupBuildEngine(); + cni.BuildEngine = buildEngine; + + cni.BaseRegistry = pcp.ParsedContainerRegistry; + cni.BaseImageName = pcp.ParsedContainerImage; + cni.BaseImageTag = pcp.ParsedContainerTag; + cni.Repository = pcp.NewContainerRepository; + cni.OutputRegistry = pcp.NewContainerRegistry; + cni.PublishDirectory = Path.Combine(newProjectDir.FullName, "bin", "release", ToolsetInfo.CurrentTargetFramework, "linux-x64"); + cni.WorkingDirectory = "/app"; + cni.Entrypoint = new TaskItem[] { new($"/app/{newProjectDir.Name}") }; + cni.ImageTags = pcp.NewContainerTags; + cni.ContainerEnvironmentVariables = pcp.NewContainerEnvironmentVariables; + cni.ContainerRuntimeIdentifier = "linux-x64"; + cni.RuntimeIdentifierGraphPath = ToolsetUtils.GetRuntimeGraphFilePath(); + cni.LocalRegistry = global::Microsoft.NET.Build.Containers.KnownLocalRegistryTypes.Docker; + + Assert.True(cni.Execute(), FormatBuildMessages(errors)); + + ContainerCli.RunCommand(_testOutput, "--rm", $"{pcp.NewContainerRepository}:latest") + .Execute() + .Should().Pass() + .And.HaveStdOut("Foo"); + } + + [DockerAvailableFact] + public async System.Threading.Tasks.Task CreateNewImage_RootlessBaseImage() + { + const string RootlessBase ="dotnet/rootlessbase"; + const string AppImage = "dotnet/testimagerootless"; + const string RootlessUser = "101"; + var loggerFactory = new TestLoggerFactory(_testOutput); + var logger = loggerFactory.CreateLogger(nameof(CreateNewImage_RootlessBaseImage)); + + // Build a rootless base runtime image. + Registry registry = new Registry(ContainerHelpers.TryExpandRegistryToUri(DockerRegistryManager.LocalRegistry), logger); + + ImageBuilder imageBuilder = await registry.GetImageManifestAsync( + DockerRegistryManager.RuntimeBaseImage, + DockerRegistryManager.Net8PreviewImageTag, + "linux-x64", + ToolsetUtils.GetRuntimeGraphFilePath(), + cancellationToken: default).ConfigureAwait(false); + + Assert.NotNull(imageBuilder); + + imageBuilder.SetUser(RootlessUser); + + BuiltImage builtImage = imageBuilder.Build(); + + var sourceReference = new ImageReference(registry, DockerRegistryManager.RuntimeBaseImage, DockerRegistryManager.Net8PreviewImageTag); + var destinationReference = new ImageReference(registry, RootlessBase, "latest"); + + await registry.PushAsync(builtImage, sourceReference, destinationReference, cancellationToken: default).ConfigureAwait(false); + + // Build an application image on top of the rootless base runtime image. + DirectoryInfo newProjectDir = new DirectoryInfo(Path.Combine(TestSettings.TestArtifactsDirectory, nameof(CreateNewImage_RootlessBaseImage))); + + if (newProjectDir.Exists) + { + newProjectDir.Delete(recursive: true); + } + + newProjectDir.Create(); + + new DotnetNewCommand(_testOutput, "console", "-f", ToolsetInfo.CurrentTargetFramework) + .WithVirtualHive() + .WithWorkingDirectory(newProjectDir.FullName) + .Execute() + .Should().Pass(); + + new DotnetCommand(_testOutput, "publish", "-c", "Release", "-r", "linux-x64", "--no-self-contained") + .WithWorkingDirectory(newProjectDir.FullName) + .Execute() + .Should().Pass(); + + CreateNewImage task = new CreateNewImage(); + var (buildEngine, errors) = SetupBuildEngine(); + task.BuildEngine = buildEngine; + task.BaseRegistry = "localhost:5010"; + task.BaseImageName = RootlessBase; + task.BaseImageTag = "latest"; + + task.OutputRegistry = "localhost:5010"; + task.PublishDirectory = Path.Combine(newProjectDir.FullName, "bin", "Release", ToolsetInfo.CurrentTargetFramework, "linux-x64", "publish"); + task.Repository = AppImage; + task.ImageTags = new[] { "latest" }; + task.WorkingDirectory = "app/"; + task.ContainerRuntimeIdentifier = "linux-x64"; + task.Entrypoint = new TaskItem[] { new("dotnet"), new("build") }; + task.RuntimeIdentifierGraphPath = ToolsetUtils.GetRuntimeGraphFilePath(); + + Assert.True(task.Execute()); + newProjectDir.Delete(true); + + // Verify the application image uses the non-root user from the base image. + imageBuilder = await registry.GetImageManifestAsync( + AppImage, + "latest", + "linux-x64", + ToolsetUtils.GetRuntimeGraphFilePath(), + cancellationToken: default).ConfigureAwait(false); + + Assert.Equal(RootlessUser, imageBuilder.BaseImageConfig.GetUser()); + } + + private static (IBuildEngine buildEngine, List errors) SetupBuildEngine() + { + List errors = new(); + IBuildEngine buildEngine = A.Fake(); + A.CallTo(() => buildEngine.LogWarningEvent(A.Ignored)).Invokes((BuildWarningEventArgs e) => errors.Add(e.Message)); + A.CallTo(() => buildEngine.LogErrorEvent(A.Ignored)).Invokes((BuildErrorEventArgs e) => errors.Add(e.Message)); + A.CallTo(() => buildEngine.LogMessageEvent(A.Ignored)).Invokes((BuildMessageEventArgs e) => errors.Add(e.Message)); + + return (buildEngine, errors); + } + + private static string GetTestDirectoryName([CallerMemberName]string testName = "DefaultTest") => Path.Combine(TestSettings.TestArtifactsDirectory, testName + "_" + DateTime.Now.ToString("yyyyMMddHHmmss")); + + private static string FormatBuildMessages(List messages) => string.Join("\r\n", messages); +} diff --git a/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/CurrentFile.cs b/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/CurrentFile.cs new file mode 100644 index 000000000000..46f304b0c57b --- /dev/null +++ b/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/CurrentFile.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.CompilerServices; + +namespace Microsoft.NET.Build.Containers.IntegrationTests; + +public static class CurrentFile +{ + public static string Path([CallerFilePath] string file = "") => file; + + public static string Relative(string relative, [CallerFilePath] string file = "") { + return global::System.IO.Path.Combine(global::System.IO.Path.GetDirectoryName(file)!, relative); // file known to be not-null due to the mechanics of CallerFilePath + } +} diff --git a/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/DockerRegistryManager.cs b/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/DockerRegistryManager.cs new file mode 100644 index 000000000000..76199cfabf4e --- /dev/null +++ b/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/DockerRegistryManager.cs @@ -0,0 +1,66 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.DotNet.Cli.Utils; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.NET.TestFramework; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Commands; +using Xunit.Abstractions; + +namespace Microsoft.NET.Build.Containers.IntegrationTests; + +public class DockerRegistryManager +{ + public const string RuntimeBaseImage = "dotnet/runtime"; + public const string AspNetBaseImage = "dotnet/aspnet"; + public const string BaseImageSource = "mcr.microsoft.com/"; + public const string Net6ImageTag = "6.0"; + public const string Net7ImageTag = "7.0"; + public const string Net8PreviewImageTag = "8.0-preview"; + public const string Net8PreviewWindowsSpecificImageTag = $"{Net8PreviewImageTag}-nanoserver-ltsc2022"; + public const string LocalRegistry = "localhost:5010"; + public const string FullyQualifiedBaseImageDefault = $"{BaseImageSource}{RuntimeBaseImage}:{Net8PreviewImageTag}"; + public const string FullyQualifiedBaseImageAspNet = $"{BaseImageSource}{AspNetBaseImage}:{Net8PreviewImageTag}"; + private static string? s_registryContainerId; + + public static void StartAndPopulateDockerRegistry(ITestOutputHelper testOutput) + { + using TestLoggerFactory loggerFactory = new(testOutput); + + testOutput.WriteLine("Spawning local registry"); + if (!new DockerCli(loggerFactory).IsAvailable()) { + throw new InvalidOperationException("Docker is not available, tests cannot run"); + } + CommandResult processResult = ContainerCli.RunCommand(testOutput, "--rm", "--publish", "5010:5000", "--detach", "docker.io/library/registry:2").Execute(); + processResult.Should().Pass().And.HaveStdOut(); + using var reader = new StringReader(processResult.StdOut!); + s_registryContainerId = reader.ReadLine(); + + foreach (var tag in new[] { Net6ImageTag, Net7ImageTag, Net8PreviewImageTag }) + { + ContainerCli.PullCommand(testOutput, $"{BaseImageSource}{RuntimeBaseImage}:{tag}") + .Execute() + .Should().Pass(); + + ContainerCli.TagCommand(testOutput, $"{BaseImageSource}{RuntimeBaseImage}:{tag}", $"{LocalRegistry}/{RuntimeBaseImage}:{tag}") + .Execute() + .Should().Pass(); + + ContainerCli.PushCommand(testOutput, $"{LocalRegistry}/{RuntimeBaseImage}:{tag}") + .Execute() + .Should().Pass(); + } + } + + public static void ShutdownDockerRegistry(ITestOutputHelper testOutput) + { + if (s_registryContainerId != null) + { + ContainerCli.StopCommand(testOutput, s_registryContainerId) + .Execute() + .Should().Pass(); + } + } +} diff --git a/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/DockerRegistryTests.cs b/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/DockerRegistryTests.cs new file mode 100644 index 000000000000..ce133eec8095 --- /dev/null +++ b/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/DockerRegistryTests.cs @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.NET.Build.Containers.UnitTests; +using Microsoft.NET.TestFramework; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.NET.Build.Containers.IntegrationTests; + +[Collection("Docker tests")] +public class DockerRegistryTests +{ + private ITestOutputHelper _testOutput; + + public DockerRegistryTests(ITestOutputHelper output) + { + _testOutput = output; + } + + [DockerAvailableFact] + public async Task GetFromRegistry() + { + var loggerFactory = new TestLoggerFactory(_testOutput); + var logger = loggerFactory.CreateLogger(nameof(GetFromRegistry)); + Registry registry = new Registry(ContainerHelpers.TryExpandRegistryToUri(DockerRegistryManager.LocalRegistry), logger); + var ridgraphfile = ToolsetUtils.GetRuntimeGraphFilePath(); + + // Don't need rid graph for local registry image pulls - since we're only pushing single image manifests (not manifest lists) + // as part of our setup, we could put literally anything in here. The file at the passed-in path would only get read when parsing manifests lists. + ImageBuilder? downloadedImage = await registry.GetImageManifestAsync( + DockerRegistryManager.RuntimeBaseImage, + DockerRegistryManager.Net6ImageTag, + "linux-x64", + ridgraphfile, + cancellationToken: default).ConfigureAwait(false); + + Assert.NotNull(downloadedImage); + } +} diff --git a/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/DockerSupportsArchInlineData.cs b/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/DockerSupportsArchInlineData.cs new file mode 100644 index 000000000000..caa4b453ef1a --- /dev/null +++ b/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/DockerSupportsArchInlineData.cs @@ -0,0 +1,104 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Reflection; +using System.Text.Json; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Commands; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace Microsoft.NET.Build.Containers.IntegrationTests; + +public class DockerSupportsArchInlineData : DataAttribute +{ + // an optimization - this doesn't change over time so we can compute it once + private static string[] LinuxPlatforms = GetSupportedLinuxPlatforms(); + + // another optimization - daemons don't switch types easily or quickly, so this is as good as static + private static bool IsWindowsDaemon = GetIsWindowsDaemon(); + + private readonly string _arch; + private readonly object[] _data; + + public DockerSupportsArchInlineData(string arch, params object[] data) + { + _arch = arch; + _data = data; + } + + public override IEnumerable GetData(MethodInfo testMethod) + { + if (DaemonSupportsArch(_arch)) + { + return new object[][] { _data.Prepend(_arch).ToArray() }; + }; + return Array.Empty(); + } + + private bool DaemonSupportsArch(string arch) + { + if (LinuxPlatforms.Contains(arch)) + { + return true; + } + else + { + if (IsWindowsDaemon && arch.StartsWith("windows", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + base.Skip = $"Skipping test because Docker daemon does not support {arch}."; + return false; + } + } + + private static string[] GetSupportedLinuxPlatforms() + { + if (ContainerCli.IsPodman) + { + var inspectResult = new RunExeCommand(NullLogger.Instance, "podman", "info").Execute(); + inspectResult.Should().Pass(); + var platformsLine = inspectResult.StdOut!.Split(Environment.NewLine).First(x => x.Contains("OsArch:", StringComparison.OrdinalIgnoreCase)); + return new[] { platformsLine.Trim().Substring("OsArch: ".Length) }; + } + else + { + var inspectResult = new RunExeCommand(NullLogger.Instance, "docker", "buildx", "inspect", "default").Execute(); + inspectResult.Should().Pass(); + var platformsLine = inspectResult.StdOut!.Split(Environment.NewLine).First(x => x.StartsWith("Platforms:", StringComparison.OrdinalIgnoreCase)); + return platformsLine.Substring("Platforms: ".Length).Split(",", StringSplitOptions.TrimEntries); + } + } + + private static bool GetIsWindowsDaemon() + { + // the config json has an OSType property that is either "linux" or "windows" - + // we can't use this for linux arch detection because that isn't enough information. + var config = DockerCli.GetConfig(); + if (config.RootElement.TryGetProperty("OSType", out JsonElement osTypeProperty)) + { + return osTypeProperty.GetString() == "windows"; + } + else + { + return false; + } + } + + private class NullLogger : ITestOutputHelper + { + private NullLogger() { } + + public static NullLogger Instance { get; } = new NullLogger(); + + public void WriteLine(string message) + { + //do nothing + } + public void WriteLine(string format, params object[] args) + { + //do nothing + } + } +} diff --git a/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/DockerTestsCollection.cs b/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/DockerTestsCollection.cs new file mode 100644 index 000000000000..9e6ba9772e6e --- /dev/null +++ b/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/DockerTestsCollection.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace Microsoft.NET.Build.Containers.IntegrationTests; + +[CollectionDefinition("Docker tests")] +#pragma warning disable CA1711 // Identifiers should not have incorrect suffix +public class DockerTestsCollection : ICollectionFixture +#pragma warning restore CA1711 // Identifiers should not have incorrect suffix +{ + // This class has no code, and is never created. Its purpose is simply + // to be the place to apply [CollectionDefinition] and all the + // ICollectionFixture<> interfaces. +} diff --git a/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/DockerTestsFixture.cs b/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/DockerTestsFixture.cs new file mode 100644 index 000000000000..7256f0d6d597 --- /dev/null +++ b/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/DockerTestsFixture.cs @@ -0,0 +1,38 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.NET.TestFramework; +using Xunit.Abstractions; + +namespace Microsoft.NET.Build.Containers.IntegrationTests; + +public sealed class DockerTestsFixture : IDisposable +{ + private readonly SharedTestOutputHelper _diagnosticOutput; + + public DockerTestsFixture(IMessageSink messageSink) + { + _diagnosticOutput = new SharedTestOutputHelper(messageSink); + try + { + DockerRegistryManager.StartAndPopulateDockerRegistry(_diagnosticOutput); + } + catch + { + Dispose(); + throw; + } + } + + public void Dispose() + { + try + { + DockerRegistryManager.ShutdownDockerRegistry(_diagnosticOutput); + } + catch + { + _diagnosticOutput.WriteLine("Failed to shutdown docker registry, shut down it manually"); + } + } +} diff --git a/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/EndToEndTests.cs b/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/EndToEndTests.cs new file mode 100644 index 000000000000..d583fab4f4a7 --- /dev/null +++ b/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/EndToEndTests.cs @@ -0,0 +1,459 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.CompilerServices; +using System.Xml.Linq; +using Microsoft.DotNet.Cli.Utils; +using Microsoft.NET.Build.Containers.UnitTests; +using Microsoft.NET.TestFramework; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Commands; +using Xunit; +using Xunit.Abstractions; +using ILogger = Microsoft.Extensions.Logging.ILogger; + +namespace Microsoft.NET.Build.Containers.IntegrationTests; + +[Collection("Docker tests")] +public class EndToEndTests : IDisposable +{ + private ITestOutputHelper _testOutput; + private readonly TestLoggerFactory _loggerFactory; + + public EndToEndTests(ITestOutputHelper testOutput) + { + _testOutput = testOutput; + _loggerFactory = new TestLoggerFactory(testOutput); + } + + public static string NewImageName([CallerMemberName] string callerMemberName = "") + { + bool normalized = ContainerHelpers.NormalizeRepository(callerMemberName, out string? normalizedName); + if (!normalized) + { + return normalizedName!; + } + + return callerMemberName; + } + + public void Dispose() + { + _loggerFactory.Dispose(); + } + + [DockerAvailableFact] + public async Task ApiEndToEndWithRegistryPushAndPull() + { + ILogger logger = _loggerFactory.CreateLogger(nameof(ApiEndToEndWithRegistryPushAndPull)); + string publishDirectory = BuildLocalApp(); + + // Build the image + + Registry registry = new Registry(ContainerHelpers.TryExpandRegistryToUri(DockerRegistryManager.LocalRegistry), logger); + + ImageBuilder imageBuilder = await registry.GetImageManifestAsync( + DockerRegistryManager.RuntimeBaseImage, + DockerRegistryManager.Net8PreviewImageTag, + "linux-x64", + ToolsetUtils.GetRuntimeGraphFilePath(), + cancellationToken: default).ConfigureAwait(false); + + Assert.NotNull(imageBuilder); + + Layer l = Layer.FromDirectory(publishDirectory, "/app", false); + + imageBuilder.AddLayer(l); + + imageBuilder.SetEntrypointAndCmd(new[] { "/app/MinimalTestApp" }, Array.Empty()); + + BuiltImage builtImage = imageBuilder.Build(); + + // Push the image back to the local registry + var sourceReference = new ImageReference(registry, DockerRegistryManager.RuntimeBaseImage, DockerRegistryManager.Net8PreviewImageTag); + var destinationReference = new ImageReference(registry, NewImageName(), "latest"); + + await registry.PushAsync(builtImage, sourceReference, destinationReference, cancellationToken: default).ConfigureAwait(false); + + // pull it back locally + ContainerCli.PullCommand(_testOutput, $"{DockerRegistryManager.LocalRegistry}/{NewImageName()}:latest") + .Execute() + .Should().Pass(); + + // Run the image + ContainerCli.RunCommand(_testOutput, "--rm", "--tty", $"{DockerRegistryManager.LocalRegistry}/{NewImageName()}:latest") + .Execute() + .Should().Pass(); + } + + [DockerAvailableFact] + public async Task ApiEndToEndWithLocalLoad() + { + ILogger logger = _loggerFactory.CreateLogger(nameof(ApiEndToEndWithLocalLoad)); + string publishDirectory = BuildLocalApp(tfm: "net8.0"); + + // Build the image + + Registry registry = new Registry(ContainerHelpers.TryExpandRegistryToUri(DockerRegistryManager.LocalRegistry), logger); + + ImageBuilder imageBuilder = await registry.GetImageManifestAsync( + DockerRegistryManager.RuntimeBaseImage, + DockerRegistryManager.Net8PreviewImageTag, + "linux-x64", + ToolsetUtils.GetRuntimeGraphFilePath(), + cancellationToken: default).ConfigureAwait(false); + Assert.NotNull(imageBuilder); + + Layer l = Layer.FromDirectory(publishDirectory, "/app", false); + + imageBuilder.AddLayer(l); + + imageBuilder.SetEntrypointAndCmd(new[] { "/app/MinimalTestApp" }, Array.Empty()); + + BuiltImage builtImage = imageBuilder.Build(); + + // Load the image into the local registry + var sourceReference = new ImageReference(registry, DockerRegistryManager.RuntimeBaseImage, DockerRegistryManager.Net7ImageTag); + var destinationReference = new ImageReference(registry, NewImageName(), "latest"); + + await new DockerCli(_loggerFactory).LoadAsync(builtImage, sourceReference, destinationReference, default).ConfigureAwait(false); + + // Run the image + ContainerCli.RunCommand(_testOutput, "--rm", "--tty", $"{NewImageName()}:latest") + .Execute() + .Should().Pass(); + } + + private string BuildLocalApp([CallerMemberName] string testName = "TestName", string tfm = ToolsetInfo.CurrentTargetFramework, string rid = "linux-x64") + { + string workingDirectory = Path.Combine(TestSettings.TestArtifactsDirectory, testName); + + DirectoryInfo d = new DirectoryInfo(Path.Combine(workingDirectory, "MinimalTestApp")); + if (d.Exists) + { + d.Delete(recursive: true); + } + Directory.CreateDirectory(workingDirectory); + + new DotnetNewCommand(_testOutput, "console", "-f", tfm, "-o", "MinimalTestApp") + .WithVirtualHive() + .WithWorkingDirectory(workingDirectory) + .Execute() + .Should().Pass(); + + var publishCommand = + new DotnetCommand(_testOutput, "publish", "-bl", "MinimalTestApp", "-r", rid, "-f", tfm, "-c", "Debug") + .WithWorkingDirectory(workingDirectory); + + if (tfm == ToolsetInfo.CurrentTargetFramework) + { + publishCommand.Arguments.AddRange(new[] { "-p", $"RuntimeFrameworkVersion=8.0.0-preview.3.23174.8" }); + } + + publishCommand.Execute() + .Should().Pass(); + + string publishDirectory = Path.Join(workingDirectory, "MinimalTestApp", "bin", "Debug", tfm, rid, "publish"); + return publishDirectory; + } + + [DockerAvailableTheory] + [InlineData(false)] + [InlineData(true)] + public async Task EndToEnd_NoAPI_Web(bool addPackageReference) + { + DirectoryInfo newProjectDir = new DirectoryInfo(Path.Combine(TestSettings.TestArtifactsDirectory, $"CreateNewImageTest_{addPackageReference}")); + DirectoryInfo privateNuGetAssets = new DirectoryInfo(Path.Combine(TestSettings.TestArtifactsDirectory, "ContainerNuGet")); + + if (newProjectDir.Exists) + { + newProjectDir.Delete(recursive: true); + } + + if (privateNuGetAssets.Exists) + { + privateNuGetAssets.Delete(recursive: true); + } + + newProjectDir.Create(); + privateNuGetAssets.Create(); + + var packageDirPath = Path.Combine(TestContext.Current.TestExecutionDirectory, "Container", "package"); + var packagedir = new DirectoryInfo(packageDirPath); + + // do not pollute the primary/global NuGet package store with the private package(s) + FileInfo[] nupkgs = packagedir.GetFiles("*.nupkg"); + if (nupkgs == null || nupkgs.Length == 0) + { + // Build Microsoft.NET.Build.Containers.csproj & wait. + // for now, fail. + Assert.Fail("No nupkg found in expected package folder. You may need to rerun the build"); + } + + new DotnetNewCommand(_testOutput, "webapi", "-f", ToolsetInfo.CurrentTargetFramework) + .WithVirtualHive() + .WithWorkingDirectory(newProjectDir.FullName) + // do not pollute the primary/global NuGet package store with the private package(s) + .WithEnvironmentVariable("NUGET_PACKAGES", privateNuGetAssets.FullName) + .Execute() + .Should().Pass(); + + if (addPackageReference) + { + File.Copy(Path.Combine(TestContext.Current.TestExecutionDirectory, "NuGet.config"), Path.Combine(newProjectDir.FullName, "NuGet.config")); + + new DotnetCommand(_testOutput, "nuget", "add", "source", packagedir.FullName, "--name", "local-temp") + .WithEnvironmentVariable("NUGET_PACKAGES", privateNuGetAssets.FullName) + .WithWorkingDirectory(newProjectDir.FullName) + .Execute() + .Should().Pass(); + + // Add package to the project + new DotnetCommand(_testOutput, "add", "package", "Microsoft.NET.Build.Containers", "-f", ToolsetInfo.CurrentTargetFramework, "-v", TestContext.Current.ToolsetUnderTest.SdkVersion) + .WithEnvironmentVariable("NUGET_PACKAGES", privateNuGetAssets.FullName) + .WithWorkingDirectory(newProjectDir.FullName) + .Execute() + .Should().Pass(); + } + else + { + string projectPath = Path.Combine(newProjectDir.FullName, newProjectDir.Name + ".csproj"); + + var project = XDocument.Load(projectPath); + var ns = project.Root?.Name.Namespace ?? throw new InvalidOperationException("Project file is empty"); + + project.Root?.Add(new XElement("PropertyGroup", new XElement("EnableSDKContainerSupport", "true"))); + project.Save(projectPath); + } + + string imageName = NewImageName(); + string imageTag = "1.0"; + + // Build & publish the project + CommandResult commandResult = new DotnetCommand( + _testOutput, + "publish", + "/p:publishprofile=DefaultContainer", + "/p:runtimeidentifier=linux-x64", + $"/p:ContainerBaseImage={DockerRegistryManager.FullyQualifiedBaseImageAspNet}", + $"/p:ContainerRegistry={DockerRegistryManager.LocalRegistry}", + $"/p:ContainerRepository={imageName}", + $"/p:ContainerImageTag={imageTag}", + $"/p:RuntimeFrameworkVersion=8.0.0-preview.3.23174.8") + .WithEnvironmentVariable("NUGET_PACKAGES", privateNuGetAssets.FullName) + .WithWorkingDirectory(newProjectDir.FullName) + .Execute(); + + commandResult.Should().Pass(); + + if (addPackageReference) + { + commandResult.Should().HaveStdOutContaining("warning : Microsoft.NET.Build.Containers NuGet package is explicitly referenced. Consider removing the package reference to Microsoft.NET.Build.Containers as it is now part of .NET SDK."); + } + else + { + commandResult.Should().NotHaveStdOutContaining("warning"); + } + + ContainerCli.PullCommand(_testOutput, $"{DockerRegistryManager.LocalRegistry}/{imageName}:{imageTag}") + .Execute() + .Should().Pass(); + + var containerName = "test-container-1"; + CommandResult processResult = ContainerCli.RunCommand( + _testOutput, + "--rm", + "--name", + containerName, + "--publish", + "5017:8080", + "--detach", + $"{DockerRegistryManager.LocalRegistry}/{imageName}:{imageTag}") + .Execute(); + processResult.Should().Pass(); + Assert.NotNull(processResult.StdOut); + + string appContainerId = processResult.StdOut.Trim(); + + bool everSucceeded = false; + + HttpClient client = new(); + + // Give the server a moment to catch up, but no more than necessary. + for (int retry = 0; retry < 10; retry++) + { + try + { + var response = await client.GetAsync("http://localhost:5017/weatherforecast").ConfigureAwait(false); + + if (response.IsSuccessStatusCode) + { + everSucceeded = true; + break; + } + } + catch { } + + await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false); + } + + ContainerCli.LogsCommand(_testOutput, appContainerId) + .Execute() + .Should().Pass(); + + Assert.True(everSucceeded, "http://localhost:5017/weatherforecast never responded."); + + ContainerCli.StopCommand(_testOutput, appContainerId) + .Execute() + .Should().Pass(); + + newProjectDir.Delete(true); + privateNuGetAssets.Delete(true); + } + + [DockerAvailableFact] + public void EndToEnd_NoAPI_Console() + { + DirectoryInfo newProjectDir = new DirectoryInfo(Path.Combine(TestSettings.TestArtifactsDirectory, "CreateNewImageTest")); + DirectoryInfo privateNuGetAssets = new DirectoryInfo(Path.Combine(TestSettings.TestArtifactsDirectory, "ContainerNuGet")); + + if (newProjectDir.Exists) + { + newProjectDir.Delete(recursive: true); + } + + if (privateNuGetAssets.Exists) + { + privateNuGetAssets.Delete(recursive: true); + } + + newProjectDir.Create(); + privateNuGetAssets.Create(); + + var packageDirPath = Path.Combine(TestContext.Current.TestExecutionDirectory, "Container", "package"); + var packagedir = new DirectoryInfo(packageDirPath); + + FileInfo[] nupkgs = packagedir.GetFiles("*.nupkg"); + if (nupkgs == null || nupkgs.Length == 0) + { + // Build Microsoft.NET.Build.Containers.csproj & wait. + // for now, fail. + Assert.Fail("No nupkg found in expected package folder. You may need to rerun the build"); + } + + new DotnetNewCommand(_testOutput, "console", "-f", ToolsetInfo.CurrentTargetFramework) + .WithVirtualHive() + .WithWorkingDirectory(newProjectDir.FullName) + // do not pollute the primary/global NuGet package store with the private package(s) + .WithEnvironmentVariable("NUGET_PACKAGES", privateNuGetAssets.FullName) + .Execute() + .Should().Pass(); + + File.Copy(Path.Combine(TestContext.Current.TestExecutionDirectory, "NuGet.config"), Path.Combine(newProjectDir.FullName, "NuGet.config")); + + new DotnetCommand(_testOutput, "nuget", "add", "source", packagedir.FullName, "--name", "local-temp") + .WithEnvironmentVariable("NUGET_PACKAGES", privateNuGetAssets.FullName) + .WithWorkingDirectory(newProjectDir.FullName) + .Execute() + .Should().Pass(); + + // Add package to the project + new DotnetCommand(_testOutput, "add", "package", "Microsoft.NET.Build.Containers", "-f", ToolsetInfo.CurrentTargetFramework, "-v", TestContext.Current.ToolsetUnderTest.SdkVersion) + .WithEnvironmentVariable("NUGET_PACKAGES", privateNuGetAssets.FullName) + .WithWorkingDirectory(newProjectDir.FullName) + .Execute() + .Should().Pass(); + + string imageName = NewImageName(); + string imageTag = "1.0"; + + // Build & publish the project + new DotnetCommand( + _testOutput, + "publish", + "/t:PublishContainer", + "/p:runtimeidentifier=linux-x64", + $"/p:ContainerBaseImage={DockerRegistryManager.FullyQualifiedBaseImageAspNet}", + $"/p:ContainerRegistry={DockerRegistryManager.LocalRegistry}", + $"/p:ContainerRepository={imageName}", + $"/p:RuntimeFrameworkVersion=8.0.0-preview.3.23174.8", + $"/p:ContainerImageTag={imageTag}") + .WithEnvironmentVariable("NUGET_PACKAGES", privateNuGetAssets.FullName) + .WithWorkingDirectory(newProjectDir.FullName) + .Execute() + .Should().Pass(); + + ContainerCli.PullCommand(_testOutput, $"{DockerRegistryManager.LocalRegistry}/{imageName}:{imageTag}") + .Execute() + .Should().Pass(); + + var containerName = "test-container-2"; + CommandResult processResult = ContainerCli.RunCommand( + _testOutput, + "--rm", + "--name", + containerName, + $"{DockerRegistryManager.LocalRegistry}/{imageName}:{imageTag}") + .Execute(); + processResult.Should().Pass().And.HaveStdOut("Hello, World!"); + + newProjectDir.Delete(true); + privateNuGetAssets.Delete(true); + } + + [DockerSupportsArchInlineData("linux/arm/v7", "linux-arm", "/app")] + [DockerSupportsArchInlineData("linux/arm64/v8", "linux-arm64", "/app")] + [DockerSupportsArchInlineData("linux/386", "linux-x86", "/app", Skip = "There's no apphost for linux-x86 so we can't execute self-contained, and there's no .NET runtime base image for linux-x86 so we can't execute framework-dependent.")] + [DockerSupportsArchInlineData("windows/amd64", "win-x64", "C:\\app")] + [DockerSupportsArchInlineData("linux/amd64", "linux-x64", "/app")] + [DockerAvailableTheory] + public async Task CanPackageForAllSupportedContainerRIDs(string dockerPlatform, string rid, string workingDir) + { + ILogger logger = _loggerFactory.CreateLogger(nameof(CanPackageForAllSupportedContainerRIDs)); + string publishDirectory = BuildLocalApp(tfm: ToolsetInfo.CurrentTargetFramework, rid: rid); + + // Build the image + Registry registry = new(ContainerHelpers.TryExpandRegistryToUri(DockerRegistryManager.BaseImageSource), logger); + var isWin = rid.StartsWith("win"); + ImageBuilder? imageBuilder = await registry.GetImageManifestAsync( + DockerRegistryManager.RuntimeBaseImage, + isWin ? DockerRegistryManager.Net8PreviewWindowsSpecificImageTag : DockerRegistryManager.Net8PreviewImageTag, + rid, + ToolsetUtils.GetRuntimeGraphFilePath(), + cancellationToken: default).ConfigureAwait(false); + Assert.NotNull(imageBuilder); + + Layer l = Layer.FromDirectory(publishDirectory, isWin ? "C:\\app" : "/app", isWin); + + imageBuilder.AddLayer(l); + imageBuilder.SetWorkingDirectory(workingDir); + + string[] entryPoint = DecideEntrypoint(rid, "MinimalTestApp", workingDir); + imageBuilder.SetEntrypointAndCmd(entryPoint, Array.Empty()); + + BuiltImage builtImage = imageBuilder.Build(); + + // Load the image into the local registry + var sourceReference = new ImageReference(registry, DockerRegistryManager.RuntimeBaseImage, DockerRegistryManager.Net7ImageTag); + var destinationReference = new ImageReference(registry, NewImageName(), rid); + await new DockerCli(_loggerFactory).LoadAsync(builtImage, sourceReference, destinationReference, default).ConfigureAwait(false); + + // Run the image + ContainerCli.RunCommand( + _testOutput, + "--rm", + "--tty", + "--platform", + dockerPlatform, + $"{NewImageName()}:{rid}") + .Execute() + .Should() + .Pass(); + + static string[] DecideEntrypoint(string rid, string appName, string workingDir) + { + var binary = rid.StartsWith("win", StringComparison.Ordinal) ? $"{appName}.exe" : appName; + return new[] { $"{workingDir}/{binary}" }; + } + } +} diff --git a/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/FullFramework/CreateNewImageToolTaskTests.cs b/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/FullFramework/CreateNewImageToolTaskTests.cs new file mode 100644 index 000000000000..5470d8531fa6 --- /dev/null +++ b/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/FullFramework/CreateNewImageToolTaskTests.cs @@ -0,0 +1,473 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Reflection; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.NET.Build.Containers.Tasks; +using Microsoft.NET.TestFramework; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Commands; +using FakeItEasy; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.NET.Build.Containers.IntegrationTests.FullFramework; + +public class CreateNewImageToolTaskTests +{ + private ITestOutputHelper _testOutput; + + public CreateNewImageToolTaskTests(ITestOutputHelper testOutput) + { + _testOutput = testOutput; + } + + [Fact] + public void GenerateCommandLineCommands_ThrowsWhenRequiredPropertiesNotSet() + { + CreateNewImage task = new(); + + Exception e = Assert.Throws(() => task.GenerateCommandLineCommandsInt()); + Assert.Equal("CONTAINER4001: Required property 'PublishDirectory' was not set or empty.", e.Message); + + DirectoryInfo publishDir = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), DateTime.Now.ToString("yyyyMMddHHmmssfff"))); + + task.PublishDirectory = publishDir.FullName; + + e = Assert.Throws(() => task.GenerateCommandLineCommandsInt()); + Assert.Equal("CONTAINER4001: Required property 'BaseRegistry' was not set or empty.", e.Message); + + task.BaseRegistry = "MyBaseRegistry"; + + e = Assert.Throws(() => task.GenerateCommandLineCommandsInt()); + Assert.Equal("CONTAINER4001: Required property 'BaseImageName' was not set or empty.", e.Message); + + task.BaseImageName = "MyBaseImageName"; + + e = Assert.Throws(() => task.GenerateCommandLineCommandsInt()); + Assert.Equal("CONTAINER4001: Required property 'Repository' was not set or empty.", e.Message); + + task.Repository = "MyImageName"; + + e = Assert.Throws(() => task.GenerateCommandLineCommandsInt()); + Assert.Equal("CONTAINER4001: Required property 'WorkingDirectory' was not set or empty.", e.Message); + + task.WorkingDirectory = "MyWorkingDirectory"; + + e = Assert.Throws(() => task.GenerateCommandLineCommandsInt()); + Assert.Equal("CONTAINER4002: Required 'Entrypoint' items were not set.", e.Message); + + task.Entrypoint = new[] { new TaskItem("") }; + + e = Assert.Throws(() => task.GenerateCommandLineCommandsInt()); + Assert.Equal("CONTAINER4003: Required 'Entrypoint' items contain empty items.", e.Message); + + task.Entrypoint = new[] { new TaskItem("MyEntryPoint") }; + + string args = task.GenerateCommandLineCommandsInt(); + string workDir = GetPathToContainerize(); + + new DotnetCommand(_testOutput, args) + .WithRawArguments() + .WithWorkingDirectory(workDir) + .Execute().Should().Fail() + .And.NotHaveStdOutContaining("Description:"); //standard help output for parse error + + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + [InlineData("ValidTag", true)] + public void GenerateCommandLineCommands_BaseImageTag(string value, bool optionExpected = false) + { + CreateNewImage task = new(); + DirectoryInfo publishDir = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), DateTime.Now.ToString("yyyyMMddHHmmssfff"))); + task.PublishDirectory = publishDir.FullName; + task.BaseRegistry = "MyBaseRegistry"; + task.BaseImageName = "MyBaseImageName"; + task.Repository = "MyImageName"; + task.WorkingDirectory = "MyWorkingDirectory"; + task.Entrypoint = new[] { new TaskItem("MyEntryPoint") }; + + task.BaseImageTag = value; + + string args = task.GenerateCommandLineCommandsInt(); + + if (optionExpected) + { + Assert.Contains($"--baseimagetag {value}", args); + } + else + { + Assert.DoesNotContain("--baseimagetag", args); + } + } + + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + [InlineData("Valid", true)] + public void GenerateCommandLineCommands_OutputRegistry(string value, bool optionExpected = false) + { + CreateNewImage task = new(); + DirectoryInfo publishDir = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), DateTime.Now.ToString("yyyyMMddHHmmssfff"))); + task.PublishDirectory = publishDir.FullName; + task.BaseRegistry = "MyBaseRegistry"; + task.BaseImageName = "MyBaseImageName"; + task.Repository = "MyImageName"; + task.WorkingDirectory = "MyWorkingDirectory"; + task.Entrypoint = new[] { new TaskItem("MyEntryPoint") }; + + task.OutputRegistry = value; + + string args = task.GenerateCommandLineCommandsInt(); + + if (optionExpected) + { + Assert.Contains($"--outputregistry {value}", args); + } + else + { + Assert.DoesNotContain("--outputregistry", args); + } + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + [InlineData("Valid", true)] + public void GenerateCommandLineCommands_ContainerRuntimeIdentifier(string value, bool optionExpected = false) + { + CreateNewImage task = new(); + DirectoryInfo publishDir = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), DateTime.Now.ToString("yyyyMMddHHmmssfff"))); + task.PublishDirectory = publishDir.FullName; + task.BaseRegistry = "MyBaseRegistry"; + task.BaseImageName = "MyBaseImageName"; + task.Repository = "MyImageName"; + task.WorkingDirectory = "MyWorkingDirectory"; + task.Entrypoint = new[] { new TaskItem("MyEntryPoint") }; + + task.ContainerRuntimeIdentifier = value; + + string args = task.GenerateCommandLineCommandsInt(); + if (optionExpected) + { + Assert.Contains($"--rid {value}", args); + } + else + { + Assert.DoesNotContain("--rid", args); + } + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + [InlineData("Valid", true)] + public void GenerateCommandLineCommands_RuntimeIdentifierGraphPath(string value, bool optionExpected = false) + { + CreateNewImage task = new(); + DirectoryInfo publishDir = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), DateTime.Now.ToString("yyyyMMddHHmmssfff"))); + task.PublishDirectory = publishDir.FullName; + task.BaseRegistry = "MyBaseRegistry"; + task.BaseImageName = "MyBaseImageName"; + task.Repository = "MyImageName"; + task.WorkingDirectory = "MyWorkingDirectory"; + task.Entrypoint = new[] { new TaskItem("MyEntryPoint") }; + + task.RuntimeIdentifierGraphPath = value; + + string args = task.GenerateCommandLineCommandsInt(); + + if (optionExpected) + { + Assert.Contains($"--ridgraphpath {value}", args); + } + else + { + Assert.DoesNotContain("--ridgraphpath", args); + } + } + + [Fact] + public void GenerateCommandLineCommands_Labels() + { + CreateNewImage task = new(); + + List warnings = new(); + IBuildEngine buildEngine = A.Fake(); + A.CallTo(() => buildEngine.LogWarningEvent(A.Ignored)).Invokes((BuildWarningEventArgs e) => warnings.Add(e.Message)); + + task.BuildEngine = buildEngine; + + DirectoryInfo publishDir = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), DateTime.Now.ToString("yyyyMMddHHmmssfff"))); + task.PublishDirectory = publishDir.FullName; + task.BaseRegistry = "MyBaseRegistry"; + task.BaseImageName = "MyBaseImageName"; + task.Repository = "MyImageName"; + task.WorkingDirectory = "MyWorkingDirectory"; + task.Entrypoint = new[] { new TaskItem("MyEntryPoint") }; + + task.Labels = new[] + { + new TaskItem("NoValue"), + new TaskItem(" "), + new TaskItem("Valid1", new Dictionary() {{ "Value", "Val1" }}), + new TaskItem("Valid12", new Dictionary() {{ "Value", "Val2" }}), + new TaskItem("Valid12", new Dictionary() {{ "Value", "" }}), + new TaskItem("Valid3", new Dictionary() {{ "Value", "has space" }}), + new TaskItem("Valid4", new Dictionary() {{ "Value", "has\"quotes\"" }}) + }; + + string args = task.GenerateCommandLineCommandsInt(); + + Assert.Contains(""" + --labels NoValue= Valid1=Val1 Valid12=Val2 Valid12= "Valid3=has space" "Valid4=has\"quotes\"" + """, args); + Assert.Equal("Items 'Labels' contain empty item(s) which will be ignored.", Assert.Single(warnings)); + + string workDir = GetPathToContainerize(); + + new DotnetCommand(_testOutput, args) + .WithRawArguments() + .WithWorkingDirectory(workDir) + .Execute().Should().Fail() + .And.NotHaveStdOutContaining("Description:"); //standard help output for parse error + } + + [Fact] + public void GenerateCommandLineCommands_ContainerEnvironmentVariables() + { + CreateNewImage task = new(); + + List warnings = new(); + IBuildEngine buildEngine = A.Fake(); + A.CallTo(() => buildEngine.LogWarningEvent(A.Ignored)).Invokes((BuildWarningEventArgs e) => warnings.Add(e.Message)); + + task.BuildEngine = buildEngine; + + DirectoryInfo publishDir = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), DateTime.Now.ToString("yyyyMMddHHmmssfff"))); + task.PublishDirectory = publishDir.FullName; + task.BaseRegistry = "MyBaseRegistry"; + task.BaseImageName = "MyBaseImageName"; + task.Repository = "MyImageName"; + task.WorkingDirectory = "MyWorkingDirectory"; + task.Entrypoint = new[] { new TaskItem("MyEntryPoint") }; + + task.ContainerEnvironmentVariables = new[] + { + new TaskItem("NoValue"), + new TaskItem(" "), + new TaskItem("Valid1", new Dictionary() {{ "Value", "Val1" }}), + new TaskItem("Valid12", new Dictionary() {{ "Value", "Val2" }}), + new TaskItem("Valid12", new Dictionary() {{ "Value", "" }}), + new TaskItem("Valid3", new Dictionary() {{ "Value", "has space" }}), + new TaskItem("Valid4", new Dictionary() {{ "Value", "has\"quotes\"" }}) + }; + + string args = task.GenerateCommandLineCommandsInt(); + + Assert.Contains(""" + --environmentvariables NoValue= Valid1=Val1 Valid12=Val2 Valid12= "Valid3=has space" "Valid4=has\"quotes\"" + """, args); + Assert.Equal("Items 'ContainerEnvironmentVariables' contain empty item(s) which will be ignored.", Assert.Single(warnings)); + + string workDir = GetPathToContainerize(); + + new DotnetCommand(_testOutput, args) + .WithRawArguments() + .WithWorkingDirectory(workDir) + .Execute().Should().Fail() + .And.NotHaveStdOutContaining("Description:"); //standard help output for parse error + } + + + [Fact] + public void GenerateCommandLineCommands_EntryPointArgs() + { + CreateNewImage task = new(); + + List warnings = new(); + IBuildEngine buildEngine = A.Fake(); + A.CallTo(() => buildEngine.LogWarningEvent(A.Ignored)).Invokes((BuildWarningEventArgs e) => warnings.Add(e.Message)); + + task.BuildEngine = buildEngine; + + DirectoryInfo publishDir = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), DateTime.Now.ToString("yyyyMMddHHmmssfff"))); + task.PublishDirectory = publishDir.FullName; + task.BaseRegistry = "MyBaseRegistry"; + task.BaseImageName = "MyBaseImageName"; + task.Repository = "MyImageName"; + task.WorkingDirectory = "MyWorkingDirectory"; + task.Entrypoint = new[] { new TaskItem("MyEntryPoint") }; + + task.EntrypointArgs = new[] + { + new TaskItem(""), + new TaskItem(" "), + new TaskItem("Valid1"), + new TaskItem("Valid2"), + new TaskItem("Quoted item") + }; + + string args = task.GenerateCommandLineCommandsInt(); + + Assert.Contains(""" + --entrypointargs Valid1 Valid2 "Quoted item" + """, args); + Assert.Equal("Items 'EntrypointArgs' contain empty item(s) which will be ignored.", Assert.Single(warnings)); + + string workDir = GetPathToContainerize(); + + new DotnetCommand(_testOutput, args) + .WithRawArguments() + .WithWorkingDirectory(workDir) + .Execute().Should().Fail() + .And.NotHaveStdOutContaining("Description:"); //standard help output for parse error + } + + [Fact] + public void GenerateCommandLineCommands_ImageTags() + { + CreateNewImage task = new(); + + List warnings = new(); + IBuildEngine buildEngine = A.Fake(); + A.CallTo(() => buildEngine.LogWarningEvent(A.Ignored)).Invokes((BuildWarningEventArgs e) => warnings.Add(e.Message)); + + task.BuildEngine = buildEngine; + + DirectoryInfo publishDir = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), DateTime.Now.ToString("yyyyMMddHHmmssfff"))); + task.PublishDirectory = publishDir.FullName; + task.BaseRegistry = "MyBaseRegistry"; + task.BaseImageName = "MyBaseImageName"; + task.Repository = "MyImageName"; + task.WorkingDirectory = "MyWorkingDirectory"; + task.Entrypoint = new[] { new TaskItem("MyEntryPoint") }; + + task.ImageTags = new[] { "", " ", "Valid1", "To be quoted" }; + + string args = task.GenerateCommandLineCommandsInt(); + + Assert.Contains(""" + --imagetags Valid1 "To be quoted" + """, actualString: args); + Assert.Equal("Property 'ImageTags' is empty or contains whitespace and will be ignored.", Assert.Single(warnings)); + + string workDir = GetPathToContainerize(); + + new DotnetCommand(_testOutput, args) + .WithRawArguments() + .WithWorkingDirectory(workDir) + .Execute().Should().Fail() + .And.NotHaveStdOutContaining("Description:"); //standard help output for parse error + } + + [Fact] + public void GenerateCommandLineCommands_ExposedPorts() + { + CreateNewImage task = new(); + + List warnings = new(); + IBuildEngine buildEngine = A.Fake(); + A.CallTo(() => buildEngine.LogWarningEvent(A.Ignored)).Invokes((BuildWarningEventArgs e) => warnings.Add(e.Message)); + + task.BuildEngine = buildEngine; + + DirectoryInfo publishDir = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), DateTime.Now.ToString("yyyyMMddHHmmssfff"))); + task.PublishDirectory = publishDir.FullName; + task.BaseRegistry = "MyBaseRegistry"; + task.BaseImageName = "MyBaseImageName"; + task.Repository = "MyImageName"; + task.WorkingDirectory = "MyWorkingDirectory"; + task.Entrypoint = new[] { new TaskItem("MyEntryPoint") }; + + task.ExposedPorts = new[] + { + new TaskItem("1500"), + new TaskItem(" "), + new TaskItem("1501", new Dictionary() {{ "Type", "udp" }}), + new TaskItem("1501", new Dictionary() {{ "Type", "tcp" }}), + new TaskItem("1502", new Dictionary() {{ "Type", "tcp" }}), + new TaskItem("1503", new Dictionary() {{ "Type", "" }}) + }; + + string args = task.GenerateCommandLineCommandsInt(); + + Assert.Contains(""" + --ports 1500 1501/udp 1501/tcp 1502/tcp 1503 + """, args); + Assert.Equal("Items 'ExposedPorts' contain empty item(s) which will be ignored.", Assert.Single(warnings)); + + string workDir = GetPathToContainerize(); + + new DotnetCommand(_testOutput, args) + .WithRawArguments() + .WithWorkingDirectory(workDir) + .Execute().Should().Fail() + .And.NotHaveStdOutContaining("Description:"); //standard help output for parse error + } + + [Fact] + public void Logging_CanEnableTraceLogging() + { + CreateNewImage task = new(); + DirectoryInfo publishDir = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), DateTime.Now.ToString("yyyyMMddHHmmssfff"))); + + task.PublishDirectory = publishDir.FullName; + task.BaseRegistry = "MyBaseRegistry"; + task.BaseImageName = "MyBaseImageName"; + task.Repository = "MyImageName"; + task.WorkingDirectory = "MyWorkingDirectory"; + task.Entrypoint = new[] { new TaskItem("") }; + task.Entrypoint = new[] { new TaskItem("MyEntryPoint") }; + + string args = task.GenerateCommandLineCommandsInt(); + string workDir = GetPathToContainerize(); + + new DotnetCommand(_testOutput, args) + .WithRawArguments() + .WithWorkingDirectory(workDir) + .WithEnvironmentVariable("CONTAINERIZE_TRACE_LOGGING_ENABLED", "1") + .Execute().Should().Fail() + .And.NotHaveStdOutContaining("Description:") //standard help output for parse error + .And.HaveStdOutContaining("Trace logging: enabled."); + } + + [Fact] + public void Logging_TraceLoggingIsDisabledByDefault() + { + CreateNewImage task = new(); + DirectoryInfo publishDir = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), DateTime.Now.ToString("yyyyMMddHHmmssfff"))); + + task.PublishDirectory = publishDir.FullName; + task.BaseRegistry = "MyBaseRegistry"; + task.BaseImageName = "MyBaseImageName"; + task.Repository = "MyImageName"; + task.WorkingDirectory = "MyWorkingDirectory"; + task.Entrypoint = new[] { new TaskItem("") }; + task.Entrypoint = new[] { new TaskItem("MyEntryPoint") }; + + string args = task.GenerateCommandLineCommandsInt(); + string workDir = GetPathToContainerize(); + + new DotnetCommand(_testOutput, args) + .WithRawArguments() + .WithWorkingDirectory(workDir) + .Execute().Should().Fail() + .And.NotHaveStdOutContaining("Description:") //standard help output for parse error + .And.NotHaveStdOutContaining("Trace logging: enabled."); + } + + private static string GetPathToContainerize() + { + return Path.Combine(TestContext.Current.TestExecutionDirectory, "Container", "containerize"); + } +} diff --git a/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/LayerEndToEndTests.cs b/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/LayerEndToEndTests.cs new file mode 100644 index 000000000000..39178cee743c --- /dev/null +++ b/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/LayerEndToEndTests.cs @@ -0,0 +1,128 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Formats.Tar; +using System.IO.Compression; +using System.Security.Cryptography; +using Xunit.Abstractions; +using Xunit; + +namespace Microsoft.NET.Build.Containers.IntegrationTests; + +public sealed class LayerEndToEndTests : IDisposable +{ + private ITestOutputHelper _testOutput; + + public LayerEndToEndTests(ITestOutputHelper testOutput) + { + _testOutput = testOutput; + testSpecificArtifactRoot = new(); + priorArtifactRoot = ContentStore.ArtifactRoot; + ContentStore.ArtifactRoot = testSpecificArtifactRoot.Path; + } + + [Fact] + public void SingleFileInFolder() + { + using TransientTestFolder folder = new(); + + string testFilePath = Path.Join(folder.Path, "TestFile.txt"); + string testString = $"Test content for {nameof(SingleFileInFolder)}"; + + File.WriteAllText(testFilePath, testString); + + Layer l = Layer.FromDirectory(directory: folder.Path, containerPath: "/app", false); + + Console.WriteLine(l.Descriptor); + + //Assert.AreEqual("application/vnd.oci.image.layer.v1.tar", l.Descriptor.MediaType); // TODO: configurability + Assert.True(l.Descriptor.Size is >= 135 and <= 500, $"'l.Descriptor.Size' should be between 135 and 500, but is {l.Descriptor.Size}"); // TODO: determinism! + //Assert.AreEqual("sha256:26140bc75f2fcb3bf5da7d3b531d995c93d192837e37df0eb5ca46e2db953124", l.Descriptor.Digest); // TODO: determinism! + + VerifyDescriptorInfo(l); + + var allEntries = LoadAllTarEntries(l.BackingFile); + Assert.True(allEntries.TryGetValue("app", out var appEntry) && appEntry.EntryType == TarEntryType.Directory, "Missing app directory entry"); + Assert.True(allEntries.TryGetValue("app/TestFile.txt", out var fileEntry) && fileEntry.EntryType == TarEntryType.RegularFile, "Missing TestFile.txt file entry"); + } + + [Fact] + public void SingleFileInFolderWindows() + { + using TransientTestFolder folder = new(); + + string testFilePath = Path.Join(folder.Path, "TestFile.txt"); + string testString = $"Test content for {nameof(SingleFileInFolder)}"; + + File.WriteAllText(testFilePath, testString); + + Layer l = Layer.FromDirectory(directory: folder.Path, containerPath: "C:\\app", true); + + var allEntries = LoadAllTarEntries(l.BackingFile); + Assert.True(allEntries.TryGetValue("Files", out var filesEntry) && filesEntry.EntryType == TarEntryType.Directory, "Missing Files directory entry"); + Assert.True(allEntries.TryGetValue("Files/app", out var appEntry) && appEntry.EntryType == TarEntryType.Directory, "Missing Files/app directory entry"); + Assert.True(allEntries.TryGetValue("Files/app/TestFile.txt", out var fileEntry) && fileEntry.EntryType == TarEntryType.RegularFile, "Missing Files/app/TestFile.txt file entry"); + Assert.True(allEntries.TryGetValue("Hives", out var hivesEntry) && hivesEntry.EntryType == TarEntryType.Directory, "Missing Hives directory entry"); + + // Enable after https://github.com/dotnet/runtime/issues/81699 is resolved + // foreach (var entry in allEntries.Values) + // { + // Assert.IsInstanceOfType(entry, typeof(PaxTarEntry)); + // var pax = (PaxTarEntry)entry; + // Assert.IsTrue(pax.ExtendedAttributes.ContainsKey("MSWINDOWS.rawsd"), + // "Missing MSWINDOWS.rawsd definition for " + entry.Name); + // } + } + + private static void VerifyDescriptorInfo(Layer l) + { + Assert.Equal(l.Descriptor.Size, new FileInfo(l.BackingFile).Length); + + byte[] hashBytes; + byte[] uncompressedHashBytes; + + using (FileStream fs = File.OpenRead(l.BackingFile)) + { + hashBytes = SHA256.HashData(fs); + + fs.Position = 0; + + using (GZipStream decompressionStream = new GZipStream(fs, CompressionMode.Decompress)) + { + uncompressedHashBytes = SHA256.HashData(decompressionStream); + } + } + + Assert.Equal(Convert.ToHexString(hashBytes), l.Descriptor.Digest.Substring("sha256:".Length), ignoreCase: true); + Assert.Equal(Convert.ToHexString(uncompressedHashBytes), l.Descriptor.UncompressedDigest?.Substring("sha256:".Length), ignoreCase: true); + } + + TransientTestFolder? testSpecificArtifactRoot; + string? priorArtifactRoot; + + public void Dispose() + { + testSpecificArtifactRoot?.Dispose(); + if (priorArtifactRoot is not null) + { + ContentStore.ArtifactRoot = priorArtifactRoot; + } + } + + + private static Dictionary LoadAllTarEntries(string file) + { + using var gzip = new GZipStream(File.OpenRead(file), CompressionMode.Decompress); + using var tar = new TarReader(gzip); + + var entries = new Dictionary(); + + TarEntry? entry; + while ((entry = tar.GetNextEntry()) != null) + { + entries[entry.Name] = entry; + } + + return entries; + } +} diff --git a/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/Microsoft.NET.Build.Containers.IntegrationTests.csproj b/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/Microsoft.NET.Build.Containers.IntegrationTests.csproj new file mode 100644 index 000000000000..a57475c8a9ff --- /dev/null +++ b/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/Microsoft.NET.Build.Containers.IntegrationTests.csproj @@ -0,0 +1,48 @@ + + + + $(SdkTargetFramework);net472 + enable + false + true + true + enable + MicrosoftShared + Exe + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/PackageTests.cs b/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/PackageTests.cs new file mode 100644 index 000000000000..6a4f90260b72 --- /dev/null +++ b/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/PackageTests.cs @@ -0,0 +1,168 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.IO.Compression; +using System.Xml.Linq; +using FluentAssertions; +using Microsoft.NET.TestFramework; +using Microsoft.DotNet.Cli.Utils; +using Xunit; + +namespace Microsoft.NET.Build.Containers.IntegrationTests; + +public class PackageTests +{ + [Fact] + public void SanityTest_ContainerizeDependencies() + { + IReadOnlyList knownPackageReferences = new List() + { + "System.CommandLine", + "Microsoft.Extensions.Logging", + "Microsoft.Extensions.Logging.Console" + }; + IReadOnlyList knownProjectReferences = new List() + { + "..\\Microsoft.NET.Build.Containers\\Microsoft.NET.Build.Containers.csproj", + "..\\..\\Cli\\Microsoft.DotNet.Cli.Utils\\Microsoft.DotNet.Cli.Utils.csproj" + }; + + string projectFilePath = Path.Combine(TestContext.Current.TestExecutionDirectory, "Container", "ProjectFiles", "containerize.csproj"); + XDocument project = XDocument.Load(projectFilePath); + XNamespace ns = project.Root?.Name.Namespace ?? throw new InvalidOperationException("Project file is empty"); + + IEnumerable packageReferences = project.Descendants().Where(element => element.Name.Equals(ns + "PackageReference")).Select(element => element.Attribute("Include")?.Value); + packageReferences.Should().BeEquivalentTo(knownPackageReferences, $"Known package references for containerize project are different from actual. Check if this is expected. If the new package reference is expected, add it to {nameof(knownPackageReferences)} and verify they are included to NuGet package in package.csproj correctly"); + + IEnumerable projectReferences = project.Descendants().Where(element => element.Name.Equals(ns + "ProjectReference")).Select(element => element.Attribute("Include")?.Value); + projectReferences.Should().BeEquivalentTo(knownProjectReferences, $"Known project references for containerize project are different from actual. Check if this is expected. If the new project reference is expected, add it to {nameof(knownProjectReferences)} and verify they are included to NuGet package in package.csproj correctly"); + } + + [Fact] + public void SanityTest_NET_Build_ContainersDependencies() + { + IReadOnlyList knownPackageReferences = new List() + { + "Microsoft.Build.Utilities.Core", + "Microsoft.CodeAnalysis.PublicApiAnalyzers", + "Nuget.Packaging", + "Valleysoft.DockerCredsProvider", + "Microsoft.Extensions.Logging", + "Microsoft.Extensions.Logging.Abstractions" + }; + IReadOnlyList knownProjectReferences = new List() + { + "..\\..\\Cli\\Microsoft.DotNet.Cli.Utils\\Microsoft.DotNet.Cli.Utils.csproj" + }; + + string projectFilePath = Path.Combine(TestContext.Current.TestExecutionDirectory, "Container", "ProjectFiles", "Microsoft.NET.Build.Containers.csproj"); + XDocument project = XDocument.Load(projectFilePath); + XNamespace ns = project.Root?.Name.Namespace ?? throw new InvalidOperationException("Project file is empty"); + + IEnumerable packageReferences = project.Descendants().Where(element => element.Name.Equals(ns + "PackageReference")).Select(element => element.Attribute("Include")?.Value); + packageReferences.Should().BeEquivalentTo(knownPackageReferences, $"Known package references for Microsoft.NET.Build.Containers project are different from actual. Check if this is expected. If the new package reference is expected, add it to {nameof(knownPackageReferences)} and verify they are included to NuGet package in package.csproj correctly"); + + IEnumerable projectReferences = project.Descendants().Where(element => element.Name.Equals(ns + "ProjectReference")).Select(element => element.Attribute("Include")?.Value); + projectReferences.Should().BeEquivalentTo(knownProjectReferences, $"Known project references for Microsoft.NET.Build.Containers project are different from actual. Check if this is expected. If the new project reference is expected, add it to {nameof(knownProjectReferences)} and verify they are included to NuGet package in package.csproj correctly"); + } + + [Fact] + public void PackageContentTest() + { + string ignoredZipFileEntriesPrefix = "package/services/metadata"; + + IReadOnlyList packageContents = new List() + { + "_rels/.rels", + "[Content_Types].xml", + "build/Microsoft.NET.Build.Containers.props", + "build/Microsoft.NET.Build.Containers.targets", + "containerize/containerize.dll", + "containerize/containerize.runtimeconfig.json", + "containerize/Microsoft.DotNet.Cli.Utils.dll", + "containerize/Microsoft.Extensions.Configuration.Abstractions.dll", + "containerize/Microsoft.Extensions.Configuration.Binder.dll", + "containerize/Microsoft.Extensions.Configuration.dll", + "containerize/Microsoft.Extensions.DependencyInjection.Abstractions.dll", + "containerize/Microsoft.Extensions.DependencyInjection.dll", + "containerize/Microsoft.Extensions.DependencyModel.dll", + "containerize/Microsoft.Extensions.Logging.Abstractions.dll", + "containerize/Microsoft.Extensions.Logging.Configuration.dll", + "containerize/Microsoft.Extensions.Logging.Console.dll", + "containerize/Microsoft.Extensions.Logging.dll", + "containerize/Microsoft.Extensions.Options.ConfigurationExtensions.dll", + "containerize/Microsoft.Extensions.Options.dll", + "containerize/Microsoft.Extensions.Primitives.dll", + "containerize/Microsoft.NET.Build.Containers.dll", + "containerize/Newtonsoft.Json.dll", + "containerize/NuGet.Common.dll", + "containerize/NuGet.Configuration.dll", + "containerize/NuGet.DependencyResolver.Core.dll", + "containerize/NuGet.Frameworks.dll", + "containerize/NuGet.LibraryModel.dll", + "containerize/NuGet.Packaging.dll", + "containerize/NuGet.ProjectModel.dll", + "containerize/NuGet.Protocol.dll", + "containerize/NuGet.Versioning.dll", + "containerize/System.CommandLine.dll", + "containerize/Valleysoft.DockerCredsProvider.dll", + "Icon.png", + "Microsoft.NET.Build.Containers.nuspec", + "README.md", + "tasks/net472/Microsoft.Extensions.DependencyInjection.Abstractions.dll", + "tasks/net472/Microsoft.Extensions.DependencyInjection.dll", + "tasks/net472/Microsoft.Extensions.DependencyModel.dll", + "tasks/net472/Microsoft.Extensions.Logging.Abstractions.dll", + "tasks/net472/Microsoft.Extensions.Logging.dll", + "tasks/net472/Microsoft.Extensions.Options.dll", + "tasks/net472/Microsoft.Extensions.Primitives.dll", + "tasks/net472/Microsoft.NET.Build.Containers.dll", + "tasks/net472/Newtonsoft.Json.dll", + "tasks/net472/NuGet.Common.dll", + "tasks/net472/NuGet.Configuration.dll", + "tasks/net472/NuGet.DependencyResolver.Core.dll", + "tasks/net472/NuGet.Frameworks.dll", + "tasks/net472/NuGet.LibraryModel.dll", + "tasks/net472/NuGet.Packaging.Core.dll", + "tasks/net472/NuGet.Packaging.dll", + "tasks/net472/NuGet.ProjectModel.dll", + "tasks/net472/NuGet.Protocol.dll", + "tasks/net472/NuGet.Versioning.dll", + "tasks/net8.0/Microsoft.DotNet.Cli.Utils.dll", + "tasks/net8.0/Microsoft.Extensions.DependencyInjection.Abstractions.dll", + "tasks/net8.0/Microsoft.Extensions.DependencyInjection.dll", + "tasks/net8.0/Microsoft.Extensions.DependencyModel.dll", + "tasks/net8.0/Microsoft.Extensions.Logging.Abstractions.dll", + "tasks/net8.0/Microsoft.Extensions.Logging.dll", + "tasks/net8.0/Microsoft.Extensions.Options.dll", + "tasks/net8.0/Microsoft.Extensions.Primitives.dll", + "tasks/net8.0/Microsoft.NET.Build.Containers.deps.json", + "tasks/net8.0/Microsoft.NET.Build.Containers.dll", + "tasks/net8.0/Newtonsoft.Json.dll", + "tasks/net8.0/NuGet.Common.dll", + "tasks/net8.0/NuGet.Configuration.dll", + "tasks/net8.0/NuGet.DependencyResolver.Core.dll", + "tasks/net8.0/NuGet.Frameworks.dll", + "tasks/net8.0/NuGet.LibraryModel.dll", + "tasks/net8.0/NuGet.Packaging.dll", + "tasks/net8.0/NuGet.Packaging.Core.dll", + "tasks/net8.0/NuGet.ProjectModel.dll", + "tasks/net8.0/NuGet.Protocol.dll", + "tasks/net8.0/NuGet.Versioning.dll", + "tasks/net8.0/Valleysoft.DockerCredsProvider.dll" + }; + + string packageFilePath = Path.Combine(TestContext.Current.TestExecutionDirectory, "Container", "package", $"Microsoft.NET.Build.Containers.{Product.Version}.nupkg"); + using ZipArchive archive = new(File.OpenRead(packageFilePath), ZipArchiveMode.Read, false); + + IEnumerable actualEntries = archive.Entries + .Select(e => e.FullName) + .Where(e => !e.StartsWith(ignoredZipFileEntriesPrefix, StringComparison.InvariantCultureIgnoreCase)) + .OrderBy(e => e); + + actualEntries + .Should() + .BeEquivalentTo(packageContents, $"Microsoft.NET.Build.Containers.{Product.Version}.nupkg content differs from expected. Please add the entry to the list, if the addition is expected."); + } +} diff --git a/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/ParseContainerPropertiesTests.cs b/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/ParseContainerPropertiesTests.cs new file mode 100644 index 000000000000..9d8aaf171050 --- /dev/null +++ b/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/ParseContainerPropertiesTests.cs @@ -0,0 +1,103 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using FluentAssertions; +using Microsoft.NET.Build.Containers.IntegrationTests; +using Microsoft.NET.Build.Containers.UnitTests; +using Xunit; +using static Microsoft.NET.Build.Containers.KnownStrings; +using static Microsoft.NET.Build.Containers.KnownStrings.Properties; + +namespace Microsoft.NET.Build.Containers.Tasks.IntegrationTests; + +[Collection("Docker tests")] +public class ParseContainerPropertiesTests +{ + [DockerAvailableFact] + public void Baseline() + { + var (project, logs, d) = ProjectInitializer.InitProject(new () { + [ContainerBaseImage] = "mcr.microsoft.com/dotnet/runtime:7.0", + [ContainerRegistry] = "localhost:5010", + [ContainerRepository] = "dotnet/testimage", + [ContainerImageTags] = "7.0;latest" + }); + using var _ = d; + var instance = project.CreateProjectInstance(global::Microsoft.Build.Execution.ProjectInstanceSettings.None); + Assert.True(instance.Build(new[]{ComputeContainerConfig}, new [] { logs }, null, out var outputs)); + + Assert.Equal("mcr.microsoft.com", instance.GetPropertyValue(ContainerBaseRegistry)); + Assert.Equal("dotnet/runtime", instance.GetPropertyValue(ContainerBaseName)); + Assert.Equal("7.0", instance.GetPropertyValue(ContainerBaseTag)); + + Assert.Equal("dotnet/testimage", instance.GetPropertyValue(ContainerRepository)); + instance.GetItems(ContainerImageTags).Select(i => i.EvaluatedInclude).ToArray().Should().BeEquivalentTo(new[] { "7.0", "latest" }); + instance.GetItems("ProjectCapability").Select(i => i.EvaluatedInclude).ToArray().Should().BeEquivalentTo(new[] { "NetSdkOCIImageBuild" }); + } + + [DockerAvailableFact] + public void SpacesGetReplacedWithDashes() + { + var (project, logs, d) = ProjectInitializer.InitProject(new () { + [ContainerBaseImage] = "mcr.microsoft.com/dotnet runtime:7.0", + [ContainerRegistry] = "localhost:5010" + }); + using var _ = d; + var instance = project.CreateProjectInstance(global::Microsoft.Build.Execution.ProjectInstanceSettings.None); + Assert.True(instance.Build(new[]{ComputeContainerConfig}, new [] { logs }, null, out var outputs)); + + Assert.Equal("mcr.microsoft.com",instance.GetPropertyValue(ContainerBaseRegistry)); + Assert.Equal("dotnet-runtime", instance.GetPropertyValue(ContainerBaseName)); + Assert.Equal("7.0", instance.GetPropertyValue(ContainerBaseTag)); + } + + [DockerAvailableFact] + public void RegexCatchesInvalidContainerNames() + { + var (project, logs, d) = ProjectInitializer.InitProject(new () { + [ContainerBaseImage] = "mcr.microsoft.com/dotnet/runtime:7.0", + [ContainerRegistry] = "localhost:5010", + [ContainerRepository] = "dotnet testimage", + [ContainerImageTag] = "5.0" + }); + using var _ = d; + var instance = project.CreateProjectInstance(global::Microsoft.Build.Execution.ProjectInstanceSettings.None); + Assert.True(instance.Build(new[]{ComputeContainerConfig}, new [] { logs }, null, out var outputs)); + Assert.Contains(logs.Messages, m => m.Message?.Contains("'ContainerRepository' was not a valid container image name, it was normalized to 'dotnet-testimage'") == true); + } + + [DockerAvailableFact] + public void RegexCatchesInvalidContainerTags() + { + var (project, logs, d) = ProjectInitializer.InitProject(new () { + [ContainerBaseImage] = "mcr.microsoft.com/dotnet/runtime:7.0", + [ContainerRegistry] = "localhost:5010", + [ContainerRepository] = "dotnet/testimage", + [ContainerImageTag] = "5 0" + }); + using var _ = d; + var instance = project.CreateProjectInstance(global::Microsoft.Build.Execution.ProjectInstanceSettings.None); + Assert.False(instance.Build(new[]{ComputeContainerConfig}, new [] { logs }, null, out var outputs)); + + Assert.True(logs.Errors.Count > 0); + Assert.Equal(logs.Errors[0].Code, ErrorCodes.CONTAINER2007); + } + + [DockerAvailableFact] + public void CanOnlySupplyOneOfTagAndTags() + { + var (project, logs, d) = ProjectInitializer.InitProject(new () { + [ContainerBaseImage] = "mcr.microsoft.com/dotnet/runtime:7.0", + [ContainerRegistry] = "localhost:5010", + [ContainerRepository] = "dotnet/testimage", + [ContainerImageTag] = "5.0", + [ContainerImageTags] = "latest;oldest" + }); + using var _ = d; + var instance = project.CreateProjectInstance(global::Microsoft.Build.Execution.ProjectInstanceSettings.None); + Assert.False(instance.Build(new[]{ComputeContainerConfig}, new [] { logs }, null, out var outputs)); + + Assert.True(logs.Errors.Count > 0); + Assert.Equal(logs.Errors[0].Code, ErrorCodes.CONTAINER2008); + } +} diff --git a/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/ProjectInitializer.cs b/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/ProjectInitializer.cs new file mode 100644 index 000000000000..2f8d51d87a89 --- /dev/null +++ b/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/ProjectInitializer.cs @@ -0,0 +1,75 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.CompilerServices; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Framework; +using Microsoft.NET.TestFramework; +using Xunit; + +namespace Microsoft.NET.Build.Containers.IntegrationTests; + +public sealed class ProjectInitializer +{ + private readonly static string _combinedTargetsLocation; + + static ProjectInitializer() + { + var artifactPackagingDirectory = Path.Combine(TestContext.Current.TestExecutionDirectory, "Container", "packaging"); + var targetsFile = Path.Combine(artifactPackagingDirectory, "Microsoft.NET.Build.Containers.targets"); + var propsFile = Path.ChangeExtension(targetsFile, ".props"); + _combinedTargetsLocation = CombineFiles(propsFile, targetsFile); + } + + private static string CombineFiles(string propsFile, string targetsFile) + { + var propsContent = File.ReadAllLines(propsFile); + var targetsContent = File.ReadAllLines(targetsFile); + var combinedContent = new List(); + combinedContent.AddRange(propsContent[..^1]); + combinedContent.AddRange(targetsContent[1..]); + var tempTargetLocation = Path.Combine(TestSettings.TestArtifactsDirectory, "Containers", "Microsoft.NET.Build.Containers.targets"); + string? directoryName = Path.GetDirectoryName(tempTargetLocation); + Assert.NotNull(directoryName); + Directory.CreateDirectory(directoryName); + File.WriteAllLines(tempTargetLocation, combinedContent); + return tempTargetLocation; + } + + public static (Project, CapturingLogger, IDisposable) InitProject(Dictionary bonusProps, [CallerMemberName]string projectName = "") + { + var props = new Dictionary(); + // required parameters + props["TargetFileName"] = "foo.dll"; + props["AssemblyName"] = "foo"; + props["TargetFrameworkVersion"] = "v7.0"; + props["TargetFrameworkIdentifier"] = ".NETCoreApp"; + props["TargetFramework"] = "net7.0"; + props["_NativeExecutableExtension"] = ".exe"; //TODO: windows/unix split here + props["Version"] = "1.0.0"; // TODO: need to test non-compliant version strings here + props["NetCoreSdkVersion"] = "7.0.100"; // TODO: float this to current SDK? + // test setup parameters so that we can load the props/targets/tasks + props["ContainerCustomTasksAssembly"] = Path.GetFullPath(Path.Combine(".", "Microsoft.NET.Build.Containers.dll")); + props["_IsTest"] = "true"; + // default here, can be overridden by tests if needed + props["NETCoreSdkPortableRuntimeIdentifier"] = "linux-x64"; + + var safeBinlogFileName = projectName.Replace(" ", "_").Replace(":", "_").Replace("/", "_").Replace("\\", "_").Replace("*", "_"); + var loggers = new List + { + new global::Microsoft.Build.Logging.BinaryLogger() {CollectProjectImports = global::Microsoft.Build.Logging.BinaryLogger.ProjectImportsCollectionMode.Embed, Verbosity = LoggerVerbosity.Diagnostic, Parameters = $"LogFile={safeBinlogFileName}.binlog" }, + new global::Microsoft.Build.Logging.ConsoleLogger(LoggerVerbosity.Detailed) + }; + CapturingLogger logs = new CapturingLogger(); + loggers.Add(logs); + + var collection = new ProjectCollection(null, loggers, ToolsetDefinitionLocations.Default); + foreach (var kvp in bonusProps) + { + props[kvp.Key] = kvp.Value; + } + // derived properties, since these might be set by bonusProps + props["_TargetFrameworkVersionWithoutV"] = props["TargetFrameworkVersion"].TrimStart('v'); + return (collection.LoadProject(_combinedTargetsLocation, props, null), logs, collection); + } +} diff --git a/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/RegistryTests.cs b/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/RegistryTests.cs new file mode 100644 index 000000000000..4869475abe1f --- /dev/null +++ b/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/RegistryTests.cs @@ -0,0 +1,59 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.NET.Build.Containers.UnitTests; +using Microsoft.NET.TestFramework; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.NET.Build.Containers.IntegrationTests; + +public class RegistryTests : IDisposable +{ + private ITestOutputHelper _testOutput; + private readonly TestLoggerFactory _loggerFactory; + + public RegistryTests(ITestOutputHelper testOutput) + { + _testOutput = testOutput; + _loggerFactory = new TestLoggerFactory(testOutput); + } + + public void Dispose() + { + _loggerFactory.Dispose(); + } + + [InlineData("quay.io/centos/centos")] + [InlineData("registry.access.redhat.com/ubi8/dotnet-70")] + [Theory] + public async Task CanReadManifestFromRegistry(string fullyQualifiedContainerName) + { + bool parsed = ContainerHelpers.TryParseFullyQualifiedContainerName(fullyQualifiedContainerName, + out string? containerRegistry, + out string? containerName, + out string? containerTag, + out string? containerDigest, + out bool isRegistrySpecified); + Assert.True(parsed); + Assert.True(isRegistrySpecified); + Assert.NotNull(containerRegistry); + Assert.NotNull(containerName); + containerTag ??= "latest"; + + ILogger logger = _loggerFactory.CreateLogger(nameof(CanReadManifestFromRegistry)); + Registry registry = new Registry(new Uri($"https://{containerRegistry}"), logger); + + var ridgraphfile = ToolsetUtils.GetRuntimeGraphFilePath(); + + ImageBuilder? downloadedImage = await registry.GetImageManifestAsync( + containerName, + containerTag, + "linux-x64", + ridgraphfile, + cancellationToken: default).ConfigureAwait(false); + + Assert.NotNull(downloadedImage); + } +} diff --git a/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/TargetsTests.cs b/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/TargetsTests.cs new file mode 100644 index 000000000000..783980f63b1e --- /dev/null +++ b/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/TargetsTests.cs @@ -0,0 +1,229 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using static Microsoft.NET.Build.Containers.KnownStrings.Properties; +using FluentAssertions; +using Microsoft.Build.Execution; +using Xunit; +using Microsoft.NET.Build.Containers.IntegrationTests; +using Microsoft.NET.Build.Containers.UnitTests; +using System.Linq; + +namespace Microsoft.NET.Build.Containers.Targets.IntegrationTests; + +public class TargetsTests +{ + [InlineData(true, "/app/foo.exe")] + [InlineData(false, "dotnet", "/app/foo.dll")] + [Theory] + public void CanSetEntrypointArgsToUseAppHost(bool useAppHost, params string[] entrypointArgs) + { + var (project, _, d) = ProjectInitializer.InitProject(new() + { + [UseAppHost] = useAppHost.ToString() + }, projectName: $"{nameof(CanSetEntrypointArgsToUseAppHost)}_{useAppHost}_{String.Join("_", entrypointArgs)}"); + using var _ = d; + Assert.True(project.Build(ComputeContainerConfig)); + var computedEntrypointArgs = project.GetItems(ContainerEntrypoint).Select(i => i.EvaluatedInclude).ToArray(); + foreach (var (First, Second) in entrypointArgs.Zip(computedEntrypointArgs)) + { + Assert.Equal(First, Second); + } + } + + [Fact] + public void CanDeferToContainerImageNameWhenPresent() { + var customImageName = "my-container-app"; + var (project, logger, d) = ProjectInitializer.InitProject(new() + { + [ContainerImageName] = customImageName + }); + using var _ = d; + var instance = project.CreateProjectInstance(global::Microsoft.Build.Execution.ProjectInstanceSettings.None); + instance.Build(new[] { ComputeContainerConfig }, new []{ logger }); + logger.Warnings.Should().HaveCount(1, "a warning for the use of the old ContainerImageName property should have been created"); + logger.Warnings[0].Code.Should().Be(KnownStrings.ErrorCodes.CONTAINER003); + Assert.Equal(customImageName, instance.GetPropertyValue(ContainerRepository)); + } + + [InlineData("WebApplication44", "webapplication44", true)] + [InlineData("friendly-suspicious-alligator", "friendly-suspicious-alligator", true)] + [InlineData("*friendly-suspicious-alligator", "", false)] + [InlineData("web/app2+7", "web/app2-7", true)] + [InlineData("Microsoft.Apps.Demo.ContosoWeb", "microsoft-apps-demo-contosoweb", true)] + [Theory] + public void CanNormalizeInputContainerNames(string projectName, string expectedContainerImageName, bool shouldPass) + { + var (project, logger, d) = ProjectInitializer.InitProject(new() + { + [AssemblyName] = projectName + }, projectName: $"{nameof(CanNormalizeInputContainerNames)}_{projectName}_{expectedContainerImageName}_{shouldPass}"); + using var _ = d; + var instance = project.CreateProjectInstance(global::Microsoft.Build.Execution.ProjectInstanceSettings.None); + instance.Build(new[] { ComputeContainerConfig }, new [] { logger }, null, out var outputs).Should().Be(shouldPass, "Build should have succeeded"); + Assert.Equal(expectedContainerImageName, instance.GetPropertyValue(ContainerRepository)); + } + + [InlineData("7.0.100", true)] + [InlineData("8.0.100", true)] + [InlineData("7.0.100-preview.7", true)] + [InlineData("7.0.100-rc.1", true)] + [InlineData("6.0.100", false)] + [InlineData("7.0.100-preview.1", false)] + [Theory] + public void CanWarnOnInvalidSDKVersions(string sdkVersion, bool isAllowed) + { + var (project, logger, d) = ProjectInitializer.InitProject(new() + { + ["NETCoreSdkVersion"] = sdkVersion, + ["PublishProfile"] = "DefaultContainer" + }, projectName: $"{nameof(CanWarnOnInvalidSDKVersions)}_{sdkVersion}_{isAllowed}"); + using var _ = d; + var instance = project.CreateProjectInstance(global::Microsoft.Build.Execution.ProjectInstanceSettings.None); + instance.Build(new[]{"_ContainerVerifySDKVersion"}, new[] { logger }, null, out var outputs).Should().Be(isAllowed); + var derivedIsAllowed = Boolean.Parse(project.GetProperty("_IsSDKContainerAllowedVersion").EvaluatedValue); + if (isAllowed) + { + logger.Errors.Should().HaveCount(0, "an error should not have been created"); + derivedIsAllowed.Should().Be(true, "SDK version {0} should have been allowed", sdkVersion); + } + else + { + logger.Errors.Should().HaveCount(1, "an error should have been created").And.Satisfy(error => error.Code == KnownStrings.ErrorCodes.CONTAINER002); + derivedIsAllowed.Should().Be(false, "SDK version {0} should not have been allowed", sdkVersion); + } + } + + [InlineData(true)] + [InlineData(false)] + [Theory] + public void GetsConventionalLabelsByDefault(bool shouldEvaluateLabels) + { + var (project, logger, d) = ProjectInitializer.InitProject(new() + { + [ContainerGenerateLabels] = shouldEvaluateLabels.ToString() + }, projectName: $"{nameof(GetsConventionalLabelsByDefault)}_{shouldEvaluateLabels}"); + using var _ = d; + var instance = project.CreateProjectInstance(global::Microsoft.Build.Execution.ProjectInstanceSettings.None); + instance.Build(new[] { ComputeContainerConfig }, new [] { logger }, null, out var outputs).Should().BeTrue("Build should have succeeded"); + if (shouldEvaluateLabels) + { + instance.GetItems(ContainerLabel).Should().NotBeEmpty("Should have evaluated some labels by default"); + } + else + { + instance.GetItems(ContainerLabel).Should().BeEmpty("Should not have evaluated any labels by default"); + } + } + + private static bool LabelMatch(string label, string value, ProjectItemInstance item) => item.EvaluatedInclude == label && item.GetMetadata("Value") is { } v && v.EvaluatedValue == value; + + [InlineData(true)] + [InlineData(false)] + [Theory] + public void ShouldNotIncludeSourceControlLabelsUnlessUserOptsIn(bool includeSourceControl) + { + var commitHash = "abcdef"; + var repoUrl = "https://git.cosmere.com/shard/whimsy.git"; + + var (project, logger, d) = ProjectInitializer.InitProject(new() + { + ["PublishRepositoryUrl"] = includeSourceControl.ToString(), + ["PrivateRepositoryUrl"] = repoUrl, + ["SourceRevisionId"] = commitHash + }, projectName: $"{nameof(ShouldNotIncludeSourceControlLabelsUnlessUserOptsIn)}_{includeSourceControl}"); + using var _ = d; + var instance = project.CreateProjectInstance(global::Microsoft.Build.Execution.ProjectInstanceSettings.None); + instance.Build(new[] { ComputeContainerConfig }, new [] { logger }, null, out var outputs).Should().BeTrue("Build should have succeeded but failed due to {0}", String.Join("\n", logger.AllMessages)); + var labels = instance.GetItems(ContainerLabel); + if (includeSourceControl) + { + labels.Should().NotBeEmpty("Should have evaluated some labels by default") + .And.ContainSingle(label => LabelMatch("org.opencontainers.image.source", repoUrl, label)) + .And.ContainSingle(label => LabelMatch("org.opencontainers.image.revision", commitHash, label)); ; + } + else + { + labels.Should().NotBeEmpty("Should have evaluated some labels by default") + .And.NotContain(label => LabelMatch("org.opencontainers.image.source", repoUrl, label)) + .And.NotContain(label => LabelMatch("org.opencontainers.image.revision", commitHash, label)); ; + }; + } + + [InlineData("7.0.100", "v7.0", "7.0")] + [InlineData("7.0.100-preview.7", "v7.0", "7.0")] + [InlineData("7.0.100-rc.1", "v7.0", "7.0")] + [InlineData("8.0.100", "v8.0", "8.0")] + [InlineData("8.0.100", "v7.0", "7.0")] + [InlineData("8.0.100-preview.7", "v8.0", "8.0.0-preview.7")] + [InlineData("8.0.100-rc.1", "v8.0", "8.0.0-rc.1")] + [InlineData("8.0.100-rc.1", "v7.0", "7.0")] + [InlineData("8.0.200", "v8.0", "8.0")] + [InlineData("8.0.200", "v7.0", "7.0")] + [InlineData("8.0.200-preview3", "v7.0", "7.0")] + [InlineData("8.0.200-preview3", "v8.0", "8.0")] + [InlineData("6.0.100", "v6.0", "6.0")] + [InlineData("6.0.100-preview.1", "v6.0", "6.0")] + [InlineData("8.0.100-dev", "v8.0", "8.0-preview")] + [InlineData("8.0.100-ci", "v8.0", "8.0-preview")] + [InlineData("8.0.100-alpha.12345", "v8.0", "8.0-preview")] + [InlineData("9.0.100-alpha.12345", "v9.0", "9.0-preview")] + [Theory] + public void CanComputeTagsForSupportedSDKVersions(string sdkVersion, string tfm, string expectedTag) + { + var (project, logger, d) = ProjectInitializer.InitProject(new() + { + ["TargetFrameworkIdentifier"] = ".NETCoreApp", + ["NETCoreSdkVersion"] = sdkVersion, + ["TargetFrameworkVersion"] = tfm, + ["PublishProfile"] = "DefaultContainer" + }, projectName: $"{nameof(CanComputeTagsForSupportedSDKVersions)}_{sdkVersion}_{tfm}_{expectedTag}"); + using var _ = d; + var instance = project.CreateProjectInstance(global::Microsoft.Build.Execution.ProjectInstanceSettings.None); + instance.Build(new[]{"_ComputeContainerBaseImageTag"}, new [] { logger }, null, out var outputs).Should().BeTrue(String.Join(Environment.NewLine, logger.Errors)); + var computedTag = instance.GetProperty("_ContainerBaseImageTag").EvaluatedValue; + computedTag.Should().Be(expectedTag); + } + + [InlineData("v8.0", "linux-x64", "64198")] + [InlineData("v8.0", "win-x64", "ContainerUser")] + [InlineData("v7.0", "linux-x64", null)] + [InlineData("v7.0", "win-x64", null)] + [InlineData("v9.0", "linux-x64", "64198")] + [InlineData("v9.0", "win-x64", "ContainerUser")] + [Theory] + public void CanComputeContainerUser(string tfm, string rid, string expectedUser) + { + var (project, logger, d) = ProjectInitializer.InitProject(new() + { + ["TargetFrameworkIdentifier"] = ".NETCoreApp", + ["TargetFrameworkVersion"] = tfm, + ["TargetFramework"] = "net" + tfm.TrimStart('v'), + ["ContainerRuntimeIdentifier"] = rid + }, projectName: $"{nameof(CanComputeContainerUser)}_{tfm}_{rid}_{expectedUser}"); + using var _ = d; + var instance = project.CreateProjectInstance(global::Microsoft.Build.Execution.ProjectInstanceSettings.None); + instance.Build(new[]{ComputeContainerConfig}, new [] { logger }, null, out var outputs).Should().BeTrue(String.Join(Environment.NewLine, logger.Errors)); + var computedTag = instance.GetProperty("ContainerUser")?.EvaluatedValue; + computedTag.Should().Be(expectedUser); + } + + [InlineData("linux-x64", "linux-x64")] + [InlineData("linux-arm64", "linux-arm64")] + [InlineData("windows-x64", "linux-x64")] + [InlineData("windows-arm64", "linux-arm64")] + [Theory] + public void WindowsUsersGetLinuxContainers(string sdkPortableRid, string expectedRid) + { + var (project, logger, d) = ProjectInitializer.InitProject(new() + { + ["TargetFrameworkVersion"] = "v6.0", + ["NETCoreSdkPortableRuntimeIdentifier"] = sdkPortableRid + }, projectName: $"{nameof(WindowsUsersGetLinuxContainers)}_{sdkPortableRid}_{expectedRid}"); + using var _ = d; + var instance = project.CreateProjectInstance(global::Microsoft.Build.Execution.ProjectInstanceSettings.None); + instance.Build(new[]{ComputeContainerConfig}, null, null, out var outputs).Should().BeTrue(String.Join(Environment.NewLine, logger.Errors)); + var computedRid = instance.GetProperty(KnownStrings.Properties.ContainerRuntimeIdentifier)?.EvaluatedValue; + computedRid.Should().Be(expectedRid); + } +} diff --git a/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/TestSettings.cs b/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/TestSettings.cs new file mode 100644 index 000000000000..d2935b7ee319 --- /dev/null +++ b/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/TestSettings.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Globalization; +using Microsoft.NET.TestFramework; + +namespace Microsoft.NET.Build.Containers.IntegrationTests; + +internal static class TestSettings +{ + private static readonly object _tmpLock = new(); + private static string? _testArtifactsDir; + + /// + /// Gets temporary location for test artifacts. + /// + internal static string TestArtifactsDirectory + { + get + { + if (_testArtifactsDir == null) + { + lock(_tmpLock) + { + if(_testArtifactsDir == null) + { + string tmpDir = Path.Combine(TestContext.Current.TestExecutionDirectory, "ContainersTests", DateTime.Now.ToString("yyyyMMddHHmmssfff", CultureInfo.InvariantCulture)); + if (!Directory.Exists(tmpDir)) + { + Directory.CreateDirectory(tmpDir); + } + return _testArtifactsDir = tmpDir; + } + } + } + return _testArtifactsDir; + } + } +} diff --git a/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/ToolsetUtils.cs b/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/ToolsetUtils.cs new file mode 100644 index 000000000000..f2e10949c177 --- /dev/null +++ b/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/ToolsetUtils.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; +using Microsoft.NET.TestFramework; +using Xunit; + +namespace Microsoft.NET.Build.Containers.IntegrationTests; + +internal static class ToolsetUtils +{ + /// + /// Gets path to RuntimeIdentifierGraph.json file. + /// + /// + internal static string GetRuntimeGraphFilePath() + { + string dotnetRoot = TestContext.Current.ToolsetUnderTest.DotNetRoot; + + DirectoryInfo sdksDir = new(Path.Combine(dotnetRoot, "sdk")); + + var lastWrittenSdk = sdksDir.EnumerateDirectories().OrderByDescending(di => di.LastWriteTime).First(); + + return lastWrittenSdk.GetFiles("RuntimeIdentifierGraph.json").Single().FullName; + } +} diff --git a/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/TransientTestFolder.cs b/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/TransientTestFolder.cs new file mode 100644 index 000000000000..b2deedcf065f --- /dev/null +++ b/src/Tests/Microsoft.NET.Build.Containers.IntegrationTests/TransientTestFolder.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using static System.IO.Path; + +namespace Microsoft.NET.Build.Containers.IntegrationTests; + +/// +/// Helper class to clean up after tests that touch the filesystem. +/// +internal sealed class TransientTestFolder : IDisposable +{ + public readonly string Path = Combine(TestSettings.TestArtifactsDirectory, GetRandomFileName()); + public readonly DirectoryInfo DirectoryInfo; + + public TransientTestFolder() + { + DirectoryInfo = Directory.CreateDirectory(Path); + } + + public void Dispose() + { + Directory.Delete(Path, recursive: true); + } +} diff --git a/src/Tests/Microsoft.NET.Build.Containers.UnitTests/AuthHeaderCacheTests.cs b/src/Tests/Microsoft.NET.Build.Containers.UnitTests/AuthHeaderCacheTests.cs new file mode 100644 index 000000000000..020172b80bc6 --- /dev/null +++ b/src/Tests/Microsoft.NET.Build.Containers.UnitTests/AuthHeaderCacheTests.cs @@ -0,0 +1,24 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Xunit; + +namespace Microsoft.NET.Build.Containers.UnitTests; + +public class AuthHeaderCacheTests +{ + [Theory] + [InlineData("https://mcr.microsoft.com/v2/dotnet/runtime/manifests/7.0", "mcr.microsoft.com/v2/dotnet/runtime/manifests/")] + [InlineData("https://mcr.microsoft.com/v2/dotnet/runtime/manifests/sha256:aa5e231e0f9a11220e10530899b489fc33182ed1168bc684949ab0cd83debd4a", "mcr.microsoft.com/v2/dotnet/runtime/manifests/")] + [InlineData("https://mcr.microsoft.com/v2/dotnet/runtime/blobs/sha256:ae2858f05eb4a525b320c2b2c702cda9924e58aae19a6b040eca4eee565c71e5", "mcr.microsoft.com/v2/dotnet/runtime/blobs/")] + [InlineData("https://public.ecr.aws/v2/abcdef12/sdk-containers-test/blobs/uploads/", "public.ecr.aws/v2/abcdef12/sdk-containers-test/blobs/uploads/")] + [InlineData("https://public.ecr.aws/token/", "public.ecr.aws/token/")] + [InlineData("https://public.ecr.aws/v2/abcdef12/sdk-containers-test/blobs/uploads/e9c6cb4a-da5a-4a45-b1db-1f17ce0d21f7", "public.ecr.aws/v2/abcdef12/sdk-containers-test/blobs/uploads/")] + [InlineData("https://public.ecr.aws/v2/abcdef12/sdk-containers-test/blobs/uploads/e9c6cb4a-da5a-4a45-b1db-1f17ce0d21f7?&digest=sha256%3Ad981f2c20c93e1c57a46cd87bc5b9a554be5323072a0d0ab4b354aabd237bbcf", "public.ecr.aws/v2/abcdef12/sdk-containers-test/blobs/uploads/")] + [InlineData("https://public.ecr.aws/v2/abcdef12/sdk-containers-test/manifests/sha256:d1f6df587a3da02b668ef33566a348374eb1500c9c050680c47295b7c0a35616", "public.ecr.aws/v2/abcdef12/sdk-containers-test/manifests/")] + public void DerivesCacheKeyCorrectly(string uri, string expectedKey) + { + Assert.Equal(expectedKey, AuthHeaderCache.GetCacheKey(new Uri(uri))); + } +} diff --git a/src/Tests/Microsoft.NET.Build.Containers.UnitTests/ContainerHelpersTests.cs b/src/Tests/Microsoft.NET.Build.Containers.UnitTests/ContainerHelpersTests.cs new file mode 100644 index 000000000000..43df690a90b9 --- /dev/null +++ b/src/Tests/Microsoft.NET.Build.Containers.UnitTests/ContainerHelpersTests.cs @@ -0,0 +1,122 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace Microsoft.NET.Build.Containers.UnitTests; + +public class ContainerHelpersTests +{ + private const string DefaultRegistry = "docker.io"; + + [Theory] + // Valid Tests + [InlineData("mcr.microsoft.com", true)] + [InlineData("mcr.microsoft.com:5001", true)] // Registries can have ports + [InlineData("docker.io", true)] // default docker registry is considered valid + + // // Invalid tests + [InlineData("mcr.mi-=crosoft.com", false)] // invalid url + [InlineData("mcr.microsoft.com/", false)] // invalid url + public void IsValidRegistry(string registry, bool expectedReturn) + { + Console.WriteLine($"Domain pattern is '{ReferenceParser.AnchoredDomainRegexp.ToString()}'"); + Assert.Equal(expectedReturn, ContainerHelpers.IsValidRegistry(registry)); + } + + [Theory] + [InlineData("mcr.microsoft.com/dotnet/runtime:6.0", true, "mcr.microsoft.com", "dotnet/runtime", "6.0", true)] + [InlineData("mcr.microsoft.com/dotnet/runtime", true, "mcr.microsoft.com", "dotnet/runtime", null, true)] + [InlineData("mcr.microsoft.com/", false, null, null, null, false)] // no image = nothing resolves + // Ports tag along + [InlineData("mcr.microsoft.com:54/dotnet/runtime", true, "mcr.microsoft.com:54", "dotnet/runtime", null, true)] + // Even if nonsensical + [InlineData("mcr.microsoft.com:0/dotnet/runtime", true, "mcr.microsoft.com:0", "dotnet/runtime", null, true)] + // We don't allow hosts with missing ports when a port is anticipated + [InlineData("mcr.microsoft.com:/dotnet/runtime", false, null, null, null, false)] + // Use default registry when no registry specified. + [InlineData("ubuntu:jammy", true, DefaultRegistry, "library/ubuntu", "jammy", false)] + [InlineData("ubuntu/runtime:jammy", true, DefaultRegistry, "ubuntu/runtime", "jammy", false)] + // Alias 'docker.io' to Docker registry. + [InlineData("docker.io/ubuntu:jammy", true, DefaultRegistry, "library/ubuntu", "jammy", true)] + [InlineData("docker.io/ubuntu/runtime:jammy", true, DefaultRegistry, "ubuntu/runtime", "jammy", true)] + // 'localhost' registry. + [InlineData("localhost/ubuntu:jammy", true, "localhost", "ubuntu", "jammy", true)] + public void TryParseFullyQualifiedContainerName(string fullyQualifiedName, bool expectedReturn, string expectedRegistry, string expectedImage, string expectedTag, bool expectedIsRegistrySpecified) + { + Assert.Equal(expectedReturn, ContainerHelpers.TryParseFullyQualifiedContainerName(fullyQualifiedName, out string? containerReg, out string? containerName, out string? containerTag, out string? containerDigest, out bool isRegistrySpecified)); + Assert.Equal(expectedRegistry, containerReg); + Assert.Equal(expectedImage, containerName); + Assert.Equal(expectedTag, containerTag); + Assert.Equal(expectedIsRegistrySpecified, isRegistrySpecified); + } + + [Theory] + [InlineData("dotnet/runtime", true)] + [InlineData("foo/bar", true)] + [InlineData("registry", true)] + [InlineData("-foo/bar", false)] + [InlineData(".foo/bar", false)] + [InlineData("_foo/bar", false)] + [InlineData("foo/bar-", false)] + [InlineData("foo/bar.", false)] + [InlineData("foo/bar_", false)] + public void IsValidImageName(string imageName, bool expectedReturn) + { + Assert.Equal(expectedReturn, ContainerHelpers.IsValidImageName(imageName)); + } + + [Theory] + [InlineData("6.0", true)] // baseline + [InlineData("5.2-asd123", true)] // with commit hash + [InlineData(".6.0", false)] // starts with . + [InlineData("-6.0", false)] // starts with - + [InlineData("---", false)] // malformed + public void IsValidImageTag(string imageTag, bool expectedReturn) + { + Assert.Equal(expectedReturn, ContainerHelpers.IsValidImageTag(imageTag)); + } + + [Fact] + public void IsValidImageTag_InvalidLength() + { + Assert.False(ContainerHelpers.IsValidImageTag(new string('a', 129))); + } + + [Theory] + [InlineData("80/tcp", true, 80, PortType.tcp, null)] + [InlineData("80", true, 80, PortType.tcp, null)] + [InlineData("125/dup", false, 125, PortType.tcp, ContainerHelpers.ParsePortError.InvalidPortType)] + [InlineData("invalidNumber", false, null, null, ContainerHelpers.ParsePortError.InvalidPortNumber)] + [InlineData("welp/unknowntype", false, null, null, (ContainerHelpers.ParsePortError)3)] + [InlineData("a/b/c", false, null, null, ContainerHelpers.ParsePortError.UnknownPortFormat)] + [InlineData("/tcp", false, null, null, ContainerHelpers.ParsePortError.MissingPortNumber)] + public void CanParsePort(string input, bool shouldParse, int? expectedPortNumber, PortType? expectedType, ContainerHelpers.ParsePortError? expectedError) { + var parseSuccess = ContainerHelpers.TryParsePort(input, out var port, out var errors); + Assert.Equal(shouldParse, parseSuccess); + + if (shouldParse) { + Assert.NotNull(port); + Assert.Equal(port.Value.Number, expectedPortNumber); + Assert.Equal(port.Value.Type, expectedType); + } else { + Assert.Null(port); + Assert.NotNull(errors); + Assert.Equal(expectedError, errors); + } + } + + [Theory] + [InlineData("FOO", true)] + [InlineData("foo_bar", true)] + [InlineData("foo-bar", false)] + [InlineData("foo.bar", false)] + [InlineData("foo bar", false)] + [InlineData("1_NAME", false)] + [InlineData("ASPNETCORE_URLS", true)] + [InlineData("ASPNETCORE_URLS2", true)] + public void CanRecognizeEnvironmentVariableNames(string envVarName, bool isValid) { + var success = ContainerHelpers.IsValidEnvironmentVariable(envVarName); + Assert.Equal(isValid, success); + } +} diff --git a/src/Tests/Microsoft.NET.Build.Containers.UnitTests/CreateNewImageTests.cs b/src/Tests/Microsoft.NET.Build.Containers.UnitTests/CreateNewImageTests.cs new file mode 100644 index 000000000000..a9ada0545b2a --- /dev/null +++ b/src/Tests/Microsoft.NET.Build.Containers.UnitTests/CreateNewImageTests.cs @@ -0,0 +1,99 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; +using Moq; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.NET.Build.Containers.Tasks; + +namespace Microsoft.NET.Build.Containers.UnitTests; + +public class CreateNewImageTests +{ + [Theory] + // Entrypoint, backwards compatibility. + [InlineData("", "entrypointArg", "appCommand", "", "", null, new [] { "appCommand" }, new [] { "entrypointArg"})] + // When no entrypoint is specified, emit the AppCommand as the Entrypoint. + [InlineData("", "", "appCommand", "appCommandArgs", "defaultArgs", "baseEntrypoint", new[] { "appCommand", "appCommandArgs" }, new[] { "defaultArgs" })] + // Set all properties. When an entrypoint is specified, emit the AppCommand as Cmd. + [InlineData("entrypoint", "entrypointArgs", "appCommand", "appCommandArgs", "defaultArgs", + "baseEntrypoint", new[] { "entrypoint", "entrypointArgs" }, new[] { "appCommand", "appCommandArgs", "defaultArgs" })] + public void EntrypointAndCmd_NoInstruction(string entrypoint, string entrypointArgs, string appCommand, string appCommandArgs, string defaultArgs, string? baseImageEntrypoint, string[]? expectedEntrypoint, string[]? expectedCmd) + => ValidateArgsAndCmd("", entrypoint, entrypointArgs, appCommand, appCommandArgs, defaultArgs, baseImageEntrypoint, expectedEntrypoint, expectedCmd); + + [Theory] + // Set all properties. + [InlineData("entrypoint", "entrypointArgs", "appCommand", "appCommandArgs", "defaultArgs", + "baseEntrypoint", new[] { "entrypoint", "entrypointArgs" }, new[] { "appCommand", "appCommandArgs", "defaultArgs" })] + // No Entrypoint, AppCommand specified, base entrypoint is preserved. + [InlineData("", "", "appCommand", "", "", "", null, new [] { "appCommand" })] + [InlineData("", "", "appCommand", "appCommandArgs", "", "", null, new [] { "appCommand", "appCommandArgs" })] + [InlineData("", "", "appCommand", "appCommandArgs", "defaultArgs", "", null, new [] { "appCommand", "appCommandArgs", "defaultArgs" })] + [InlineData("", "", "appCommand", "", "", "baseEntrypoint", new[] { "baseEntrypoint" }, new [] { "appCommand" })] + [InlineData("", "", "appCommand", "appCommandArgs", "", "baseEntrypoint", new[] { "baseEntrypoint" }, new [] { "appCommand", "appCommandArgs" })] + [InlineData("", "", "appCommand", "appCommandArgs", "defaultArgs", "baseEntrypoint", new[] { "baseEntrypoint" }, new [] { "appCommand", "appCommandArgs", "defaultArgs" })] + // No Entrypoint, AppCommand specified, 'dotnet' base entrypoint is ignored. + [InlineData("", "", "appCommand", "", "", "dotnet", null, new [] { "appCommand" })] + [InlineData("", "", "appCommand", "appCommandArgs", "", "dotnet", null, new [] { "appCommand", "appCommandArgs" })] + [InlineData("", "", "appCommand", "appCommandArgs", "defaultArgs", "dotnet", null, new [] { "appCommand", "appCommandArgs", "defaultArgs" })] + // No Entrypoint, AppCommand specified, '/usr/bin/dotnet' base entrypoint is ignored. + [InlineData("", "", "appCommand", "", "", "/usr/bin/dotnet", null, new [] { "appCommand" })] + [InlineData("", "", "appCommand", "appCommandArgs", "", "/usr/bin/dotnet", null, new [] { "appCommand", "appCommandArgs" })] + [InlineData("", "", "appCommand", "appCommandArgs", "defaultArgs", "/usr/bin/dotnet", null, new [] { "appCommand", "appCommandArgs", "defaultArgs" })] + public void EntrypointAndCmd_DefaultArgsInstruction(string entrypoint, string entrypointArgs, string appCommand, string appCommandArgs, string defaultArgs, string? baseImageEntrypoint, string[]? expectedEntrypoint, string[]? expectedCmd) + => ValidateArgsAndCmd("DefaultArgs", entrypoint, entrypointArgs, appCommand, appCommandArgs, defaultArgs, baseImageEntrypoint, expectedEntrypoint, expectedCmd); + + [Theory] + // Set all properties except entrypoint and entrypointArgs. + [InlineData("", "", "appCommand", "appCommandArgs", "defaultArgs", "baseEntrypoint", new[] { "appCommand", "appCommandArgs" }, new[] { "defaultArgs" })] + // Can't set entrypoint or entrypointArgs with instruction 'Entrypoint'. + [InlineData("entrypoint", "entrypointArgs", "appCommand", "appCommandArgs", "defaultArgs", "baseEntrypoint", null, null)] + [InlineData("", "entrypointArgs", "appCommand", "appCommandArgs", "defaultArgs", "baseEntrypoint", null, null)] + [InlineData("entrypoint", "", "appCommand", "appCommandArgs", "defaultArgs", "baseEntrypoint", null, null)] + public void EntrypointAndCmd_EntrypointInstruction(string entrypoint, string entrypointArgs, string appCommand, string appCommandArgs, string defaultArgs, string? baseImageEntrypoint, string[]? expectedEntrypoint, string[]? expectedCmd) + => ValidateArgsAndCmd("Entrypoint", entrypoint, entrypointArgs, appCommand, appCommandArgs, defaultArgs, baseImageEntrypoint, expectedEntrypoint, expectedCmd); + + [Theory] + // Set all properties except appCommand and appCommandArgs. + [InlineData("entrypoint", "entrypointArgs", "", "", "defaultArgs", "baseEntrypoint", new[] { "entrypoint", "entrypointArgs" }, new[] { "defaultArgs" })] + // Can't set appCommand or appCommandArgs with instruction 'None'. + [InlineData("entrypoint", "entrypointArgs", "appCommand", "appCommandArgs", "defaultArgs", "baseEntrypoint", null, null)] + [InlineData("entrypoint", "entrypointArgs", "", "appCommandArgs", "defaultArgs", "baseEntrypoint", null, null)] + [InlineData("entrypoint", "entrypointArgs", "appCommand", "", "defaultArgs", "baseEntrypoint", null, null)] + public void EntrypointAndCmd_NoneInstruction(string entrypoint, string entrypointArgs, string appCommand, string appCommandArgs, string defaultArgs, string? baseImageEntrypoint, string[]? expectedEntrypoint, string[]? expectedCmd) + => ValidateArgsAndCmd("None", entrypoint, entrypointArgs, appCommand, appCommandArgs, defaultArgs, baseImageEntrypoint, expectedEntrypoint, expectedCmd); + + [Theory] + // Set all properties accepted. + [InlineData("entrypoint", "entrypointArgs", "appCommand", "appCommandArgs", "defaultArgs", "baseEntrypoint", new[] { "entrypoint", "entrypointArgs" }, new[] { "appCommand", "appCommandArgs", "defaultArgs" })] + // Set all properties except entrypoint fails: can't set entrypointArgs without setting entrypoint. + [InlineData("", "entrypointArgs", "appCommand", "appCommandArgs", "defaultArgs", "baseEntrypoint", null, null)] + // Set all properties except appCommand fails: can't set appCommandArgs without setting appCommand. + [InlineData("entrypoint", "entrypointArgs", "", "appCommandArgs", "defaultArgs", "baseEntrypoint", null, null)] + public void EntrypointAndCmd_RequiredProperties(string entrypoint, string entrypointArgs, string appCommand, string appCommandArgs, string defaultArgs, string? baseImageEntrypoint, string[]? expectedEntrypoint, string[]? expectedCmd) + => ValidateArgsAndCmd("DefaultArgs", entrypoint, entrypointArgs, appCommand, appCommandArgs, defaultArgs, baseImageEntrypoint, expectedEntrypoint, expectedCmd); + + private static void ValidateArgsAndCmd(string appCommandInstruction, string entrypoint, string entrypointArgs, string appCommand, string appCommandArgs, string defaultArgs, string? baseImageEntrypoint, string[]? expectedEntrypoint, string[]? expectedCmd) + { + var newImage = new CreateNewImage() + { + Entrypoint = CreateTaskItems(entrypoint), + EntrypointArgs = CreateTaskItems(entrypointArgs), + DefaultArgs = CreateTaskItems(defaultArgs), + AppCommand = CreateTaskItems(appCommand), + AppCommandArgs = CreateTaskItems(appCommandArgs), + AppCommandInstruction = appCommandInstruction, + BuildEngine = new Mock().Object + }; + + (string[] imageEntrypoint, string[] imageCmd) = newImage.DetermineEntrypointAndCmd(baseImageEntrypoint?.Split(';', StringSplitOptions.RemoveEmptyEntries)); + + Assert.Equal(newImage.Log.HasLoggedErrors, imageEntrypoint.Length == 0 && imageCmd.Length == 0); + Assert.Equal(expectedEntrypoint ?? Array.Empty(), imageEntrypoint); + Assert.Equal(expectedCmd ?? Array.Empty(), imageCmd); + + static ITaskItem[] CreateTaskItems(string value) + => value.Split(';', StringSplitOptions.RemoveEmptyEntries).Select(s => new TaskItem(s)).ToArray(); + } +} diff --git a/src/Tests/Microsoft.NET.Build.Containers.UnitTests/DescriptorTests.cs b/src/Tests/Microsoft.NET.Build.Containers.UnitTests/DescriptorTests.cs new file mode 100644 index 000000000000..18d2fe9c2814 --- /dev/null +++ b/src/Tests/Microsoft.NET.Build.Containers.UnitTests/DescriptorTests.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json; +using Xunit; + +namespace Microsoft.NET.Build.Containers.UnitTests; + +public class DescriptorTests +{ + [Fact] + public void BasicConstructor() + { + Descriptor d = new( + mediaType: "application/vnd.oci.image.manifest.v1+json", + digest: "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270", + size: 7682); + + Console.WriteLine(JsonSerializer.Serialize(d, new JsonSerializerOptions { WriteIndented = true })); + + Assert.Equal("application/vnd.oci.image.manifest.v1+json", d.MediaType); + Assert.Equal("sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270", d.Digest); + Assert.Equal(7_682, d.Size); + + Assert.Null(d.Annotations); + Assert.Null(d.Data); + Assert.Null(d.Urls); + } +} diff --git a/src/Tests/Microsoft.NET.Build.Containers.UnitTests/DiagnosticMessageTests.cs b/src/Tests/Microsoft.NET.Build.Containers.UnitTests/DiagnosticMessageTests.cs new file mode 100644 index 000000000000..46da6362a7dd --- /dev/null +++ b/src/Tests/Microsoft.NET.Build.Containers.UnitTests/DiagnosticMessageTests.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.NET.Build.Containers.Resources; +using Xunit; + +namespace Microsoft.NET.Build.Containers.UnitTests +{ + public class DiagnosticMessageTests + { + [Fact] + public void Error_ShouldCreateValidMessage() + { + string result = DiagnosticMessage.Error("code", "text"); + + Assert.Equal("Containerize : error code : text", result); + } + + [Fact] + public void Warning_ShouldCreateValidMessage() + { + string result = DiagnosticMessage.Error("code", "text"); + + Assert.Equal("Containerize : error code : text", result); + } + + [Fact] + public void Error_ShouldCreateValidMessageFromResource() + { + string result = DiagnosticMessage.ErrorFromResourceWithCode(nameof(Strings._Test), "param"); + + Assert.Equal("Containerize : error CONTAINER0000: Value for unit test param", result); + } + + [Fact] + public void Warning_ShouldCreateValidMessageFromResource() + { + string result = DiagnosticMessage.WarningFromResourceWithCode(nameof(Strings._Test), "param"); + + Assert.Equal("Containerize : warning CONTAINER0000: Value for unit test param", result); + } + } +} diff --git a/src/Tests/Microsoft.NET.Build.Containers.UnitTests/DockerAvailableUtils.cs b/src/Tests/Microsoft.NET.Build.Containers.UnitTests/DockerAvailableUtils.cs new file mode 100644 index 000000000000..0e72fc1868f5 --- /dev/null +++ b/src/Tests/Microsoft.NET.Build.Containers.UnitTests/DockerAvailableUtils.cs @@ -0,0 +1,54 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.NET.TestFramework; +using Xunit; + +namespace Microsoft.NET.Build.Containers.UnitTests; + +public class DockerAvailableTheoryAttribute : TheoryAttribute +{ + public DockerAvailableTheoryAttribute(bool skipPodman = false) + { + if (!DockerCliStatus.IsAvailable) + { + base.Skip = "Skipping test because Docker is not available on this host."; + } + + if (skipPodman && DockerCliStatus.Command == DockerCli.PodmanCommand) + { + base.Skip = $"Skipping test with {DockerCliStatus.Command} cli."; + } + } +} + +public class DockerAvailableFactAttribute : FactAttribute +{ + public DockerAvailableFactAttribute(bool skipPodman = false) + { + if (!DockerCliStatus.IsAvailable) + { + base.Skip = "Skipping test because Docker is not available on this host."; + } + + if (skipPodman && DockerCliStatus.Command == DockerCli.PodmanCommand) + { + base.Skip = $"Skipping test with {DockerCliStatus.Command} cli."; + } + } +} + +// tiny optimization - since there are many instances of this attribute we should only get +// the daemon status once +file static class DockerCliStatus +{ + public static readonly bool IsAvailable; + public static readonly string? Command; + + static DockerCliStatus() + { + DockerCli cli = new(new TestLoggerFactory()); + IsAvailable = cli.IsAvailable(); + Command = cli.GetCommand(); + } +} diff --git a/src/Tests/Microsoft.NET.Build.Containers.UnitTests/DockerDaemonTests.cs b/src/Tests/Microsoft.NET.Build.Containers.UnitTests/DockerDaemonTests.cs new file mode 100644 index 000000000000..ca167accd97c --- /dev/null +++ b/src/Tests/Microsoft.NET.Build.Containers.UnitTests/DockerDaemonTests.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.NET.TestFramework; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.NET.Build.Containers.UnitTests; + +[CollectionDefinition("Daemon Tests")] +public class DaemonTestsCollection +{ +} + +[Collection("Daemon Tests")] +public class DockerDaemonTests : IDisposable +{ + private ITestOutputHelper _testOutput; + private readonly TestLoggerFactory _loggerFactory; + + public DockerDaemonTests(ITestOutputHelper testOutput) + { + _testOutput = testOutput; + _loggerFactory = new TestLoggerFactory(testOutput); + } + + public void Dispose() + { + _loggerFactory.Dispose(); + } + + [DockerAvailableFact(skipPodman: true)] // podman is a local cli not meant for connecting to remote Docker daemons. + public async Task Can_detect_when_no_daemon_is_running() { + // mimic no daemon running by setting the DOCKER_HOST to a nonexistent socket + try { + System.Environment.SetEnvironmentVariable("DOCKER_HOST", "tcp://123.123.123.123:12345"); + var available = await new DockerCli(_loggerFactory).IsAvailableAsync(default).ConfigureAwait(false); + Assert.False(available, "No daemon should be listening at that port"); + } + finally + { + Environment.SetEnvironmentVariable("DOCKER_HOST", null); + } + } + + [DockerAvailableFact] + public async Task Can_detect_when_daemon_is_running() { + var available = await new DockerCli(_loggerFactory).IsAvailableAsync(default).ConfigureAwait(false); + Assert.True(available, "Should have found a working daemon"); + } +} diff --git a/src/Tests/Microsoft.NET.Build.Containers.UnitTests/ImageBuilderTests.cs b/src/Tests/Microsoft.NET.Build.Containers.UnitTests/ImageBuilderTests.cs new file mode 100644 index 000000000000..d1ab8e31aabc --- /dev/null +++ b/src/Tests/Microsoft.NET.Build.Containers.UnitTests/ImageBuilderTests.cs @@ -0,0 +1,378 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json.Nodes; +using Xunit; + +namespace Microsoft.NET.Build.Containers.UnitTests; + +public class ImageBuilderTests +{ + [Fact] + public void CanAddLabelsToImage() + { + string simpleImageConfig = + """ + { + "architecture": "amd64", + "config": { + "Hostname": "", + "Domainname": "", + "User": "", + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "ASPNETCORE_URLS=http://+:80", + "DOTNET_RUNNING_IN_CONTAINER=true", + "DOTNET_VERSION=7.0.2", + "ASPNET_VERSION=7.0.2" + ], + "Cmd": ["bash"], + "Image": "sha256:d772d27ebeec80393349a4770dc37f977be2c776a01c88b624d43f93fa369d69", + "Volumes": null, + "WorkingDir": "", + "Entrypoint": null, + "OnBuild": null, + "Labels": null + }, + "created": "2023-02-04T08:14:52.000901321Z", + "os": "linux", + "rootfs": { + "type": "layers", + "diff_ids": [ + "sha256:bd2fe8b74db65d82ea10db97368d35b92998d4ea0e7e7dc819481fe4a68f64cf", + "sha256:94100d1041b650c6f7d7848c550cd98c25d0bdc193d30692e5ea5474d7b3b085", + "sha256:53c2a75a33c8f971b4b5036d34764373e134f91ee01d8053b4c3573c42e1cf5d", + "sha256:49a61320e585180286535a2545be5722b09e40ad44c7c190b20ec96c9e42e4a3", + "sha256:8a379cce2ac272aa71aa029a7bbba85c852ba81711d9f90afaefd3bf5036dc48" + ] + } + } + """; + + JsonNode? node = JsonNode.Parse(simpleImageConfig); + Assert.NotNull(node); + + ImageConfig baseConfig = new ImageConfig(node); + + baseConfig.AddLabel("testLabel1", "v1"); + baseConfig.AddLabel("testLabel2", "v2"); + + string readyImage = baseConfig.BuildConfig(); + + JsonNode? result = JsonNode.Parse(readyImage); + + var resultLabels = result?["config"]?["Labels"] as JsonObject; + Assert.NotNull(resultLabels); + + Assert.Equal(2, resultLabels.Count); + Assert.Equal("v1", resultLabels["testLabel1"]?.ToString()); + Assert.Equal("v2", resultLabels["testLabel2"]?.ToString()); + } + + [Fact] + public void CanPreserveExistingLabels() + { + string simpleImageConfig = + """ + { + "architecture": "amd64", + "config": { + "Hostname": "", + "Domainname": "", + "User": "", + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "ASPNETCORE_URLS=http://+:80", + "DOTNET_RUNNING_IN_CONTAINER=true", + "DOTNET_VERSION=7.0.2", + "ASPNET_VERSION=7.0.2" + ], + "Cmd": ["bash"], + "Image": "sha256:d772d27ebeec80393349a4770dc37f977be2c776a01c88b624d43f93fa369d69", + "Volumes": null, + "WorkingDir": "", + "Entrypoint": null, + "OnBuild": null, + "Labels": + { + "existing" : "e1", + "existing2" : "e2" + } + }, + "created": "2023-02-04T08:14:52.000901321Z", + "os": "linux", + "rootfs": { + "type": "layers", + "diff_ids": [ + "sha256:bd2fe8b74db65d82ea10db97368d35b92998d4ea0e7e7dc819481fe4a68f64cf", + "sha256:94100d1041b650c6f7d7848c550cd98c25d0bdc193d30692e5ea5474d7b3b085", + "sha256:53c2a75a33c8f971b4b5036d34764373e134f91ee01d8053b4c3573c42e1cf5d", + "sha256:49a61320e585180286535a2545be5722b09e40ad44c7c190b20ec96c9e42e4a3", + "sha256:8a379cce2ac272aa71aa029a7bbba85c852ba81711d9f90afaefd3bf5036dc48" + ] + } + } + """; + + JsonNode? node = JsonNode.Parse(simpleImageConfig); + Assert.NotNull(node); + + ImageConfig baseConfig = new ImageConfig(node); + + baseConfig.AddLabel("testLabel1", "v1"); + baseConfig.AddLabel("existing2", "v2"); + + string readyImage = baseConfig.BuildConfig(); + + JsonNode? result = JsonNode.Parse(readyImage); + + var resultLabels = result?["config"]?["Labels"] as JsonObject; + Assert.NotNull(resultLabels); + + Assert.Equal(3, resultLabels.Count); + Assert.Equal("v1", resultLabels["testLabel1"]?.ToString()); + Assert.Equal("v2", resultLabels["existing2"]?.ToString()); + Assert.Equal("e1", resultLabels["existing"]?.ToString()); + } + + [Fact] + public void CanAddPortsToImage() + { + string simpleImageConfig = + """ + { + "architecture": "amd64", + "config": { + "Hostname": "", + "Domainname": "", + "User": "", + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "ASPNETCORE_URLS=http://+:80", + "DOTNET_RUNNING_IN_CONTAINER=true", + "DOTNET_VERSION=7.0.2", + "ASPNET_VERSION=7.0.2" + ], + "Cmd": ["bash"], + "Image": "sha256:d772d27ebeec80393349a4770dc37f977be2c776a01c88b624d43f93fa369d69", + "Volumes": null, + "WorkingDir": "", + "Entrypoint": null, + "OnBuild": null, + "Labels": null + }, + "created": "2023-02-04T08:14:52.000901321Z", + "os": "linux", + "rootfs": { + "type": "layers", + "diff_ids": [ + "sha256:bd2fe8b74db65d82ea10db97368d35b92998d4ea0e7e7dc819481fe4a68f64cf", + "sha256:94100d1041b650c6f7d7848c550cd98c25d0bdc193d30692e5ea5474d7b3b085", + "sha256:53c2a75a33c8f971b4b5036d34764373e134f91ee01d8053b4c3573c42e1cf5d", + "sha256:49a61320e585180286535a2545be5722b09e40ad44c7c190b20ec96c9e42e4a3", + "sha256:8a379cce2ac272aa71aa029a7bbba85c852ba81711d9f90afaefd3bf5036dc48" + ] + } + } + """; + + JsonNode? node = JsonNode.Parse(simpleImageConfig); + Assert.NotNull(node); + + ImageConfig baseConfig = new ImageConfig(node); + + baseConfig.ExposePort(6000, PortType.tcp); + baseConfig.ExposePort(6010, PortType.udp); + + string readyImage = baseConfig.BuildConfig(); + + JsonNode? result = JsonNode.Parse(readyImage); + + var resultPorts = result?["config"]?["ExposedPorts"] as JsonObject; + Assert.NotNull(resultPorts); + + Assert.Equal(2, resultPorts.Count); + Assert.NotNull(resultPorts["6000/tcp"] as JsonObject); + Assert.NotNull( resultPorts["6010/udp"] as JsonObject); + } + + [Fact] + public void CanPreserveExistingPorts() + { + string simpleImageConfig = + """ + { + "architecture": "amd64", + "config": { + "Hostname": "", + "Domainname": "", + "User": "", + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "ASPNETCORE_URLS=http://+:80", + "DOTNET_RUNNING_IN_CONTAINER=true", + "DOTNET_VERSION=7.0.2", + "ASPNET_VERSION=7.0.2" + ], + "Cmd": ["bash"], + "Image": "sha256:d772d27ebeec80393349a4770dc37f977be2c776a01c88b624d43f93fa369d69", + "Volumes": null, + "WorkingDir": "", + "Entrypoint": null, + "OnBuild": null, + "Labels": null, + "ExposedPorts": + { + "6100/tcp": {}, + "6200": {} + } + }, + "created": "2023-02-04T08:14:52.000901321Z", + "os": "linux", + "rootfs": { + "type": "layers", + "diff_ids": [ + "sha256:bd2fe8b74db65d82ea10db97368d35b92998d4ea0e7e7dc819481fe4a68f64cf", + "sha256:94100d1041b650c6f7d7848c550cd98c25d0bdc193d30692e5ea5474d7b3b085", + "sha256:53c2a75a33c8f971b4b5036d34764373e134f91ee01d8053b4c3573c42e1cf5d", + "sha256:49a61320e585180286535a2545be5722b09e40ad44c7c190b20ec96c9e42e4a3", + "sha256:8a379cce2ac272aa71aa029a7bbba85c852ba81711d9f90afaefd3bf5036dc48" + ] + } + } + """; + + JsonNode? node = JsonNode.Parse(simpleImageConfig); + Assert.NotNull(node); + + ImageConfig baseConfig = new ImageConfig(node); + + baseConfig.ExposePort(6000, PortType.tcp); + baseConfig.ExposePort(6010, PortType.udp); + baseConfig.ExposePort(6100, PortType.udp); + baseConfig.ExposePort(6200, PortType.tcp); + + string readyImage = baseConfig.BuildConfig(); + + JsonNode? result = JsonNode.Parse(readyImage); + + var resultPorts = result?["config"]?["ExposedPorts"] as JsonObject; + Assert.NotNull(resultPorts); + + Assert.Equal(5, resultPorts.Count); + Assert.NotNull(resultPorts["6000/tcp"] as JsonObject); + Assert.NotNull(resultPorts["6010/udp"] as JsonObject); + Assert.NotNull(resultPorts["6100/udp"] as JsonObject); + Assert.NotNull(resultPorts["6100/tcp"] as JsonObject); + Assert.NotNull(resultPorts["6200/tcp"] as JsonObject); + } + + [Fact] + public void HistoryEntriesMatchNonEmptyLayers() + { + // Note how the base image config is alredy "corrupt" by having + // only 5 layers with only 2 history entries. We expect the + // final config to also have 5 (non empty) history entries. + + string simpleImageConfig = + """ + { + "architecture": "amd64", + "config": { + "Hostname": "", + "Domainname": "", + "User": "", + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "ASPNETCORE_URLS=http://+:80", + "DOTNET_RUNNING_IN_CONTAINER=true", + "DOTNET_VERSION=7.0.2", + "ASPNET_VERSION=7.0.2" + ], + "Cmd": ["bash"], + "Image": "sha256:d772d27ebeec80393349a4770dc37f977be2c776a01c88b624d43f93fa369d69", + "Volumes": null, + "WorkingDir": "", + "Entrypoint": null, + "OnBuild": null, + "Labels": null, + "ExposedPorts": + { + "6100/tcp": {}, + "6200": {} + } + }, + "created": "2023-02-04T08:14:52.000901321Z", + "os": "linux", + "rootfs": { + "type": "layers", + "diff_ids": [ + "sha256:bd2fe8b74db65d82ea10db97368d35b92998d4ea0e7e7dc819481fe4a68f64cf", + "sha256:94100d1041b650c6f7d7848c550cd98c25d0bdc193d30692e5ea5474d7b3b085", + "sha256:53c2a75a33c8f971b4b5036d34764373e134f91ee01d8053b4c3573c42e1cf5d", + "sha256:49a61320e585180286535a2545be5722b09e40ad44c7c190b20ec96c9e42e4a3", + "sha256:8a379cce2ac272aa71aa029a7bbba85c852ba81711d9f90afaefd3bf5036dc48" + ] + }, + "history": [{ + "created": "2023-03-01T04:09:58.669479866Z", + "created_by": "/bin/sh -c #(nop) ADD file:493a5b0c8d2d63a1343258b3f9aa5fcd59a93f44fe26ad9e56b094c3a08fd3be in / " + }, { + "created": "2023-03-01T04:09:59.032972609Z", + "created_by": "/bin/sh -c #(nop) CMD [\u0022bash\u0022]", + "empty_layer": true + }] + } + """; + + JsonNode? node = JsonNode.Parse(simpleImageConfig); + Assert.NotNull(node); + + ImageConfig baseConfig = new(node); + + string readyImage = baseConfig.BuildConfig(); + + JsonNode? result = JsonNode.Parse(readyImage); + + var historyNode = result?["history"]; + Assert.NotNull(historyNode); + + var layerDiffsNode = result?["rootfs"]?["diff_ids"]; + Assert.NotNull(layerDiffsNode); + + int nonEmptyHistoryNodes = historyNode.AsArray() + .Count(h => h?.AsObject()["empty_layer"]?.GetValue() is null or false); + int layerCount = layerDiffsNode.AsArray().Count; + Assert.Equal(nonEmptyHistoryNodes, layerCount); + } +} diff --git a/src/Tests/Microsoft.NET.Build.Containers.UnitTests/ImageConfigTests.cs b/src/Tests/Microsoft.NET.Build.Containers.UnitTests/ImageConfigTests.cs new file mode 100644 index 000000000000..10b29ffc8efb --- /dev/null +++ b/src/Tests/Microsoft.NET.Build.Containers.UnitTests/ImageConfigTests.cs @@ -0,0 +1,58 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json.Nodes; +using Xunit; + +namespace Microsoft.NET.Build.Containers.UnitTests; + +public class ImageConfigTests +{ + private const string SampleImageConfig = """ + { + "architecture": "amd64", + "config": { + "User": "app", + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "ASPNETCORE_URLS=http://+:80", + "DOTNET_RUNNING_IN_CONTAINER=true", + "DOTNET_VERSION=7.0.2", + "ASPNET_VERSION=7.0.2" + ], + "Cmd": ["bash"], + "Volumes": { + "/var/log/": {} + }, + "WorkingDir": "/app", + "Entrypoint": null, + "Labels": null, + "StopSignal": "SIGTERM" + }, + "created": "2023-02-04T08:14:52.000901321Z", + "os": "linux", + "rootfs": { + "type": "layers", + "diff_ids": [ + "sha256:bd2fe8b74db65d82ea10db97368d35b92998d4ea0e7e7dc819481fe4a68f64cf", + "sha256:94100d1041b650c6f7d7848c550cd98c25d0bdc193d30692e5ea5474d7b3b085", + "sha256:53c2a75a33c8f971b4b5036d34764373e134f91ee01d8053b4c3573c42e1cf5d", + "sha256:49a61320e585180286535a2545be5722b09e40ad44c7c190b20ec96c9e42e4a3", + "sha256:8a379cce2ac272aa71aa029a7bbba85c852ba81711d9f90afaefd3bf5036dc48" + ] + } + } + """; + + [InlineData("User")] + [InlineData("Volumes")] + [InlineData("StopSignal")] + [Theory] + public void PassesThroughPropertyEvenThoughPropertyIsntExplicitlyHandled(string property) + { + ImageConfig c = new(SampleImageConfig); + JsonNode after = JsonNode.Parse(c.BuildConfig())!; + JsonNode? prop = after["config"]?[property]; + Assert.NotNull(prop); + } +} diff --git a/src/Tests/Microsoft.NET.Build.Containers.UnitTests/Microsoft.NET.Build.Containers.UnitTests.csproj b/src/Tests/Microsoft.NET.Build.Containers.UnitTests/Microsoft.NET.Build.Containers.UnitTests.csproj new file mode 100644 index 000000000000..09d401602488 --- /dev/null +++ b/src/Tests/Microsoft.NET.Build.Containers.UnitTests/Microsoft.NET.Build.Containers.UnitTests.csproj @@ -0,0 +1,19 @@ + + + + $(SdkTargetFramework) + enable + enable + false + true + MicrosoftShared + Exe + + + + + + + + + diff --git a/src/Tests/Microsoft.NET.Build.Containers.UnitTests/RegistryTests.cs b/src/Tests/Microsoft.NET.Build.Containers.UnitTests/RegistryTests.cs new file mode 100644 index 000000000000..9f1f5ecb5035 --- /dev/null +++ b/src/Tests/Microsoft.NET.Build.Containers.UnitTests/RegistryTests.cs @@ -0,0 +1,397 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net; +using Microsoft.Extensions.Logging; +using Microsoft.NET.Build.Containers.Resources; +using Microsoft.NET.TestFramework; +using Moq; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.NET.Build.Containers.UnitTests; + +public class RegistryTests : IDisposable +{ + private ITestOutputHelper _testOutput; + private readonly TestLoggerFactory _loggerFactory; + + public RegistryTests(ITestOutputHelper testOutput) + { + _testOutput = testOutput; + _loggerFactory = new TestLoggerFactory(testOutput); + } + + public void Dispose() + { + _loggerFactory.Dispose(); + } + + [InlineData("us-south1-docker.pkg.dev", true)] + [InlineData("us.gcr.io", false)] + [Theory] + public void CheckIfGoogleArtifactRegistry(string registryName, bool isECR) + { + ILogger logger = _loggerFactory.CreateLogger(nameof(CheckIfGoogleArtifactRegistry)); + Registry registry = new Registry(ContainerHelpers.TryExpandRegistryToUri(registryName), logger); + Assert.Equal(isECR, registry.IsGoogleArtifactRegistry); + } + + [Fact] + public void DockerIoAlias() + { + ILogger logger = _loggerFactory.CreateLogger(nameof(DockerIoAlias)); + Registry registry = new Registry(new Uri("https://docker.io"), logger); + Assert.True(registry.IsDockerHub); + Assert.Equal("docker.io", registry.RegistryName); + Assert.Equal("registry-1.docker.io", registry.BaseUri.Host); + } + + [Fact] + public async Task RegistriesThatProvideNoUploadSizeAttemptFullUpload() + { + ILogger logger = _loggerFactory.CreateLogger(nameof(RegistriesThatProvideNoUploadSizeAttemptFullUpload)); + var repoName = "testRepo"; + var layerDigest = "sha256:fafafafafafafafafafafafafafafafa"; + var mockLayer = new Mock(MockBehavior.Strict); + mockLayer + .Setup(l => l.OpenBackingFile()).Returns(new MemoryStream(new byte[1000])); + mockLayer + .Setup(l => l.Descriptor).Returns(new Descriptor("blah", layerDigest, 1234)); + + var uploadPath = new Uri("/uploads/foo/12345", UriKind.Relative); + var api = new Mock(MockBehavior.Loose); + api.Setup(api => api.Blob.ExistsAsync(repoName, layerDigest, It.IsAny())).Returns(Task.FromResult(false)); + api.Setup(api => api.Blob.Upload.StartAsync(repoName, It.IsAny())).Returns(Task.FromResult(new StartUploadInformation(uploadPath))); + api.Setup(api => api.Blob.Upload.UploadAtomicallyAsync(uploadPath, It.IsAny(), It.IsAny())).Returns(Task.FromResult(new FinalizeUploadInformation(uploadPath))); + + Registry registry = new(ContainerHelpers.TryExpandRegistryToUri("public.ecr.aws"), logger, api.Object, new RegistrySettings()); + await registry.PushLayerAsync(mockLayer.Object, repoName, CancellationToken.None); + + api.Verify(api => api.Blob.Upload.UploadChunkAsync(uploadPath, It.IsAny(), It.IsAny()), Times.Never()); + api.Verify(api => api.Blob.Upload.UploadAtomicallyAsync(uploadPath, It.IsAny(), It.IsAny()), Times.Once()); + } + + [Fact] + public async Task RegistriesThatProvideUploadSizePrefersFullUploadWhenChunkSizeIsLowerThanContentLength() + { + ILogger logger = _loggerFactory.CreateLogger(nameof(RegistriesThatProvideUploadSizePrefersFullUploadWhenChunkSizeIsLowerThanContentLength)); + var repoName = "testRepo"; + var layerDigest = "sha256:fafafafafafafafafafafafafafafafa"; + var mockLayer = new Mock(MockBehavior.Strict); + var chunkSizeLessThanContentLength = 10000; + var registryUri = ContainerHelpers.TryExpandRegistryToUri("public.ecr.aws"); + mockLayer + .Setup(l => l.OpenBackingFile()).Returns(new MemoryStream(new byte[100000])); + mockLayer + .Setup(l => l.Descriptor).Returns(new Descriptor("blah", layerDigest, 1234)); + + var uploadPath = new Uri("/uploads/foo/12345", UriKind.Relative); + var absoluteUploadUri = new Uri(registryUri, uploadPath); + var api = new Mock(MockBehavior.Loose); + var uploadedCount = 0; + api.Setup(api => api.Blob.ExistsAsync(repoName, layerDigest, It.IsAny())).Returns(Task.FromResult(false)); + api.Setup(api => api.Blob.Upload.StartAsync(repoName, It.IsAny())).Returns(Task.FromResult(new StartUploadInformation(uploadPath))); + api.Setup(api => api.Blob.Upload.UploadAtomicallyAsync(uploadPath, It.IsAny(), It.IsAny())).Returns(Task.FromResult(new FinalizeUploadInformation(uploadPath))); + api.Setup(api => api.Blob.Upload.UploadChunkAsync(It.IsIn(absoluteUploadUri, uploadPath), It.IsAny(), It.IsAny())).Returns(() => { + uploadedCount += chunkSizeLessThanContentLength; + return Task.FromResult(ChunkUploadSuccessful(absoluteUploadUri, uploadPath, uploadedCount)); + }); + + Registry registry = new(registryUri, logger, api.Object, new RegistrySettings()); + await registry.PushLayerAsync(mockLayer.Object, repoName, CancellationToken.None); + + api.Verify(api => api.Blob.Upload.UploadAtomicallyAsync(It.IsIn(absoluteUploadUri, uploadPath), It.IsAny(), It.IsAny()), Times.Exactly(1)); + api.Verify(api => api.Blob.Upload.UploadChunkAsync(It.IsIn(absoluteUploadUri, uploadPath), It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public async Task RegistriesThatFailAtomicUploadFallbackToChunked() + { + ILogger logger = _loggerFactory.CreateLogger(nameof(RegistriesThatFailAtomicUploadFallbackToChunked)); + var repoName = "testRepo"; + var layerDigest = "sha256:fafafafafafafafafafafafafafafafa"; + var mockLayer = new Mock(MockBehavior.Strict); + var contentLength = 100000; + var chunkSizeLessThanContentLength = 100000; + var registryUri = ContainerHelpers.TryExpandRegistryToUri("public.ecr.aws"); + mockLayer + .Setup(l => l.OpenBackingFile()).Returns(new MemoryStream(new byte[contentLength])); + mockLayer + .Setup(l => l.Descriptor).Returns(new Descriptor("blah", layerDigest, 1234)); + + var uploadPath = new Uri("/uploads/foo/12345", UriKind.Relative); + var absoluteUploadUri = new Uri(registryUri, uploadPath); + var api = new Mock(MockBehavior.Loose); + var uploadedCount = 0; + api.Setup(api => api.Blob.ExistsAsync(repoName, layerDigest, It.IsAny())).Returns(Task.FromResult(false)); + api.Setup(api => api.Blob.Upload.StartAsync(repoName, It.IsAny())).Returns(Task.FromResult(new StartUploadInformation(uploadPath))); + api.Setup(api => api.Blob.Upload.UploadAtomicallyAsync(It.IsIn(absoluteUploadUri, uploadPath), It.IsAny(), It.IsAny())).Throws(new Exception("Server-side shutdown the thing")); + api.Setup(api => api.Blob.Upload.UploadChunkAsync(It.IsIn(absoluteUploadUri, uploadPath), It.IsAny(), It.IsAny())).Returns(() => { + uploadedCount += chunkSizeLessThanContentLength; + return Task.FromResult(ChunkUploadSuccessful(absoluteUploadUri, uploadPath, uploadedCount)); + }); + + Registry registry = new(registryUri, logger, api.Object, new RegistrySettings()); + await registry.PushLayerAsync(mockLayer.Object, repoName, CancellationToken.None); + + api.Verify(api => api.Blob.Upload.UploadAtomicallyAsync(It.IsIn(absoluteUploadUri, uploadPath), It.IsAny(), It.IsAny()), Times.Once()); + api.Verify(api => api.Blob.Upload.UploadChunkAsync(It.IsIn(absoluteUploadUri, uploadPath), It.IsAny(), It.IsAny()), Times.Exactly(contentLength / chunkSizeLessThanContentLength)); + } + + [Fact] + public async Task ChunkedUploadCalculatesChunksCorrectly() + { + ILogger logger = _loggerFactory.CreateLogger(nameof(RegistriesThatFailAtomicUploadFallbackToChunked)); + var repoName = "testRepo"; + var layerDigest = "sha256:fafafafafafafafafafafafafafafafa"; + var mockLayer = new Mock(MockBehavior.Strict); + var contentLength = 1000000; + var chunkSize = 100000; + var registryUri = ContainerHelpers.TryExpandRegistryToUri("public.ecr.aws"); + mockLayer + .Setup(l => l.OpenBackingFile()).Returns(new MemoryStream(new byte[contentLength])); + mockLayer + .Setup(l => l.Descriptor).Returns(new Descriptor("blah", layerDigest, 1234)); + + var uploadPath = new Uri("/uploads/foo/12345", UriKind.Relative); + var absoluteUploadUri = new Uri(registryUri, uploadPath); + var api = new Mock(MockBehavior.Loose); + var uploadedCount = 0; + api.Setup(api => api.Blob.ExistsAsync(repoName, layerDigest, It.IsAny())).Returns(Task.FromResult(false)); + api.Setup(api => api.Blob.Upload.StartAsync(repoName, It.IsAny())).Returns(Task.FromResult(new StartUploadInformation(uploadPath))); + api.Setup(api => api.Blob.Upload.UploadAtomicallyAsync(It.IsIn(absoluteUploadUri, uploadPath), It.IsAny(), It.IsAny())).Throws(new Exception("Server-side shutdown the thing")); + api.Setup(api => api.Blob.Upload.UploadChunkAsync(It.IsIn(absoluteUploadUri, uploadPath), It.IsAny(), It.IsAny())).Returns(() => { + uploadedCount += chunkSize; + return Task.FromResult(ChunkUploadSuccessful(absoluteUploadUri, uploadPath, uploadedCount)); + }); + + RegistrySettings settings = new() + { + ParallelUploadEnabled = false, + ForceChunkedUpload = false, + ChunkedUploadSizeBytes = chunkSize, + }; + + Registry registry = new(registryUri, logger, api.Object, settings); + await registry.PushLayerAsync(mockLayer.Object, repoName, CancellationToken.None); + + api.Verify(api => api.Blob.Upload.UploadAtomicallyAsync(It.IsIn(absoluteUploadUri, uploadPath), It.IsAny(), It.IsAny()), Times.Once()); + api.Verify(api => api.Blob.Upload.UploadChunkAsync(It.IsIn(absoluteUploadUri, uploadPath), It.IsAny(), It.IsAny()), Times.Exactly(10)); + } + + [Fact] + public async Task PushAsync_Logging() + { + using TestLoggerFactory loggerFactory = new(_testOutput); + List<(LogLevel, string)> loggedMessages = new(); + loggerFactory.AddProvider(new InMemoryLoggerProvider(loggedMessages)); + ILogger logger = loggerFactory.CreateLogger(nameof(PushAsync_Logging)); + + var repoName = "testRepo"; + var layerDigest = "sha256:fafafafafafafafafafafafafafafafa"; + var mockLayer = new Mock(MockBehavior.Strict); + mockLayer + .Setup(l => l.OpenBackingFile()).Returns(new MemoryStream(new byte[1000])); + mockLayer + .Setup(l => l.Descriptor).Returns(new Descriptor("blah", layerDigest, 1234)); + + var uploadPath = new Uri("/uploads/foo/12345", UriKind.Relative); + var api = new Mock(MockBehavior.Loose); + api.Setup(api => api.Blob.ExistsAsync(repoName, layerDigest, It.IsAny())).Returns(Task.FromResult(false)); + api.Setup(api => api.Blob.Upload.StartAsync(repoName, It.IsAny())).Returns(Task.FromResult(new StartUploadInformation(uploadPath))); + api.Setup(api => api.Blob.Upload.UploadAtomicallyAsync(uploadPath, It.IsAny(), It.IsAny())).Returns(Task.FromResult(new FinalizeUploadInformation(uploadPath))); + + Registry registry = new(ContainerHelpers.TryExpandRegistryToUri("public.ecr.aws"), logger, api.Object, new RegistrySettings()); + await registry.PushLayerAsync(mockLayer.Object, repoName, CancellationToken.None); + + Assert.NotEmpty(loggedMessages); + Assert.True(loggedMessages.All(m => m.Item1 == LogLevel.Trace)); + var messages = loggedMessages.Select(m => m.Item2).ToList(); + Assert.Contains(messages, m => m == "Started upload session for sha256:fafafafafafafafafafafafafafafafa"); + Assert.Contains(messages, m => m == "Finalized upload session for sha256:fafafafafafafafafafafafafafafafa"); + } + + [Fact] + public async Task PushAsync_ForceChunkedUpload() + { + ILogger logger = _loggerFactory.CreateLogger(nameof(PushAsync_ForceChunkedUpload)); + + string repoName = "testRepo"; + string layerDigest = "sha256:fafafafafafafafafafafafafafafafa"; + Mock mockLayer = new(MockBehavior.Strict); + int contentLength = 1000000; + int chunkSize = 100000; + Uri registryUri = ContainerHelpers.TryExpandRegistryToUri("public.ecr.aws"); + mockLayer + .Setup(l => l.OpenBackingFile()).Returns(new MemoryStream(new byte[contentLength])); + mockLayer + .Setup(l => l.Descriptor).Returns(new Descriptor("blah", layerDigest, 1234)); + + Uri uploadPath = new("/uploads/foo/12345", UriKind.Relative); + Uri absoluteUploadUri = new(registryUri, uploadPath); + Mock api = new(MockBehavior.Loose); + int uploadedCount = 0; + api.Setup(api => api.Blob.ExistsAsync(repoName, layerDigest, It.IsAny())).Returns(Task.FromResult(false)); + api.Setup(api => api.Blob.Upload.StartAsync(repoName, It.IsAny())).Returns(Task.FromResult(new StartUploadInformation(uploadPath))); + api.Setup(api => api.Blob.Upload.UploadChunkAsync(It.IsIn(absoluteUploadUri, uploadPath), It.IsAny(), It.IsAny())).Returns(() => { + uploadedCount += chunkSize; + return Task.FromResult(ChunkUploadSuccessful(absoluteUploadUri, uploadPath, uploadedCount)); + }); + + RegistrySettings settings = new() + { + ParallelUploadEnabled = false, + ForceChunkedUpload = true, + ChunkedUploadSizeBytes = chunkSize, + }; + + Registry registry = new(registryUri, logger, api.Object, settings); + await registry.PushLayerAsync(mockLayer.Object, repoName, CancellationToken.None); + + api.Verify(api => api.Blob.Upload.UploadAtomicallyAsync(It.IsIn(absoluteUploadUri, uploadPath), It.IsAny(), It.IsAny()), Times.Never()); + api.Verify(api => api.Blob.Upload.UploadChunkAsync(It.IsIn(absoluteUploadUri, uploadPath), It.IsAny(), It.IsAny()), Times.Exactly(10)); + } + + [Fact] + public async Task CanParseRegistryDeclaredChunkSize_FromRange() + { + ILogger logger = _loggerFactory.CreateLogger(nameof(CanParseRegistryDeclaredChunkSize_FromRange)); + string repoName = "testRepo"; + + Mock client = new(MockBehavior.Loose); + HttpResponseMessage httpResponse = new() + { + StatusCode = HttpStatusCode.Accepted, + }; + httpResponse.Headers.Add("Range", "0-100000"); + httpResponse.Headers.Location = new Uri("https://my-registy.com/v2/testRepo/blobs/uploads/"); + client.Setup(client => client.SendAsync(It.Is(m => m.RequestUri == new Uri("https://my-registy.com/v2/testRepo/blobs/uploads/")), It.IsAny())).Returns(Task.FromResult(httpResponse)); + + HttpClient finalClient = client.Object; + DefaultBlobUploadOperations operations = new(new Uri("https://my-registy.com"), finalClient, logger); + StartUploadInformation result = await operations.StartAsync(repoName, CancellationToken.None); + + Assert.Equal("https://my-registy.com/v2/testRepo/blobs/uploads/", result.UploadUri.AbsoluteUri); + } + + [Fact] + public async Task CanParseRegistryDeclaredChunkSize_FromOCIChunkMinLength() + { + ILogger logger = _loggerFactory.CreateLogger(nameof(CanParseRegistryDeclaredChunkSize_FromOCIChunkMinLength)); + string repoName = "testRepo"; + + Mock client = new(MockBehavior.Loose); + HttpResponseMessage httpResponse = new() + { + StatusCode = HttpStatusCode.Accepted, + }; + httpResponse.Headers.Add("OCI-Chunk-Min-Length", "100000"); + httpResponse.Headers.Location = new Uri("https://my-registy.com/v2/testRepo/blobs/uploads/"); + client.Setup(client => client.SendAsync(It.Is(m => m.RequestUri == new Uri("https://my-registy.com/v2/testRepo/blobs/uploads/")), It.IsAny())).Returns(Task.FromResult(httpResponse)); + + HttpClient finalClient = client.Object; + DefaultBlobUploadOperations operations = new(new Uri("https://my-registy.com"), finalClient, logger); + StartUploadInformation result = await operations.StartAsync(repoName, CancellationToken.None); + + Assert.Equal("https://my-registy.com/v2/testRepo/blobs/uploads/", result.UploadUri.AbsoluteUri); + } + + [Fact] + public async Task CanParseRegistryDeclaredChunkSize_None() + { + ILogger logger = _loggerFactory.CreateLogger(nameof(CanParseRegistryDeclaredChunkSize_None)); + string repoName = "testRepo"; + + Mock client = new(MockBehavior.Loose); + HttpResponseMessage httpResponse = new() + { + StatusCode = HttpStatusCode.Accepted, + }; + httpResponse.Headers.Location = new Uri("https://my-registy.com/v2/testRepo/blobs/uploads/"); + client.Setup(client => client.SendAsync(It.Is(m => m.RequestUri == new Uri("https://my-registy.com/v2/testRepo/blobs/uploads/")), It.IsAny())).Returns(Task.FromResult(httpResponse)); + + HttpClient finalClient = client.Object; + DefaultBlobUploadOperations operations = new(new Uri("https://my-registy.com"), finalClient, logger); + StartUploadInformation result = await operations.StartAsync(repoName, CancellationToken.None); + + Assert.Equal("https://my-registy.com/v2/testRepo/blobs/uploads/", result.UploadUri.AbsoluteUri); + } + + [Fact] + public async Task UploadBlobChunkedAsync_NormalFlow() + { + ILogger logger = _loggerFactory.CreateLogger(nameof(UploadBlobChunkedAsync_NormalFlow)); + Uri registryUri = ContainerHelpers.TryExpandRegistryToUri("public.ecr.aws"); + + int contentLength = 50000000; + int chunkSize = 10000000; + + Stream testStream = new MemoryStream(new byte[contentLength]); + + Uri uploadPath = new("/uploads/foo/12345", UriKind.Relative); + Uri absoluteUploadUri = new(registryUri, uploadPath); + Mock api = new(MockBehavior.Loose); + int uploadedCount = 0; + api.Setup(api => api.Blob.Upload.UploadChunkAsync(It.IsIn(absoluteUploadUri, uploadPath), It.IsAny(), It.IsAny())).Returns(() => { + uploadedCount += chunkSize; + return Task.FromResult(ChunkUploadSuccessful(absoluteUploadUri, uploadPath, uploadedCount)); + }); + + RegistrySettings settings = new() + { + ParallelUploadEnabled = false, + ForceChunkedUpload = false, + ChunkedUploadSizeBytes = chunkSize, + }; + + Registry registry = new(registryUri, logger, api.Object, settings); + await registry.UploadBlobChunkedAsync(testStream, new StartUploadInformation(absoluteUploadUri), CancellationToken.None); + + api.Verify(api => api.Blob.Upload.UploadChunkAsync(It.IsIn(absoluteUploadUri, uploadPath), It.IsAny(), It.IsAny()), Times.Exactly(5)); + } + + [Fact] + public async Task UploadBlobChunkedAsync_Failure() + { + ILogger logger = _loggerFactory.CreateLogger(nameof(UploadBlobChunkedAsync_NormalFlow)); + Uri registryUri = ContainerHelpers.TryExpandRegistryToUri("public.ecr.aws"); + + int contentLength = 50000000; + int chunkSize = 10000000; + + Stream testStream = new MemoryStream(new byte[contentLength]); + + Uri uploadPath = new("/uploads/foo/12345", UriKind.Relative); + Uri absoluteUploadUri = new(registryUri, uploadPath); + Mock api = new(MockBehavior.Loose); + + Exception preparedException = new ApplicationException(Resource.FormatString(nameof(Strings.BlobUploadFailed), $"PATCH ", HttpStatusCode.InternalServerError)); + + api.Setup(api => api.Blob.Upload.UploadChunkAsync(It.IsIn(absoluteUploadUri, uploadPath), It.IsAny(), It.IsAny())).Returns(() => { + throw preparedException; + }); + + RegistrySettings settings = new() + { + ParallelUploadEnabled = false, + ForceChunkedUpload = false, + ChunkedUploadSizeBytes = chunkSize, + }; + + Registry registry = new(registryUri, logger, api.Object, settings); + ApplicationException receivedException = await Assert.ThrowsAsync(() => registry.UploadBlobChunkedAsync(testStream, new StartUploadInformation(absoluteUploadUri), CancellationToken.None)); + + Assert.Equal(preparedException, receivedException); + + api.Verify(api => api.Blob.Upload.UploadChunkAsync(It.IsIn(absoluteUploadUri, uploadPath), It.IsAny(), It.IsAny()), Times.Exactly(1)); + } + + + + private static NextChunkUploadInformation ChunkUploadSuccessful(Uri requestUri, Uri uploadUrl, int? contentLength, HttpStatusCode code = HttpStatusCode.Accepted) + { + return new(uploadUrl); + } + +} diff --git a/src/Tests/Microsoft.NET.Build.Containers.UnitTests/Resources/ResourceTests.cs b/src/Tests/Microsoft.NET.Build.Containers.UnitTests/Resources/ResourceTests.cs new file mode 100644 index 000000000000..4e5d57777de6 --- /dev/null +++ b/src/Tests/Microsoft.NET.Build.Containers.UnitTests/Resources/ResourceTests.cs @@ -0,0 +1,53 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections; +using System.Globalization; +using System.Resources; +using Microsoft.NET.Build.Containers.Resources; +using Xunit; + +namespace Microsoft.NET.Build.Containers.UnitTests.Resources +{ + public class ResourceTests + { + [Fact] + public void GetString_ReturnsValueFromResources() + { + Assert.Equal("CONTAINER0000: Value for unit test {0}", Resource.GetString(nameof(Strings._Test))); + } + + [Fact] + public void FormatString_ReturnsValueFromResources() + { + Assert.Equal("CONTAINER0000: Value for unit test 1", Resource.FormatString(nameof(Strings._Test), 1)); + } + + [Fact] + public void EnsureErrorCodeUniqueness() + { + ResourceSet? resourceSet = Resource.Manager.GetResourceSet(CultureInfo.InvariantCulture, true, true); + Assert.NotNull(resourceSet); + + IEnumerable> groups = resourceSet + .OfType() + .Where(e => e.Value is string value && value.StartsWith("CONTAINER", StringComparison.OrdinalIgnoreCase)) + .GroupBy(e => e.Value!.ToString()!.Substring(9, 4)) + .Where(g => g.Count() > 1); + + foreach(IGrouping group in groups) + { + if (!group.First().Key!.ToString()!.Contains('_')) + { + Assert.Fail($"Code with number '{group.Key}' is used for multiple resources. You can use single code for multiple messages, but the name of these resources must share same prefix delimited by underscore."); + } + else + { + string prefix = group.First().Key!.ToString()!.Split('_')[0]; + + Assert.All(group, e => e.Key!.ToString()!.StartsWith(prefix, StringComparison.Ordinal)); + } + } + } + } +}