From 83f3f626af5aadb935106380fbd944210785b7d9 Mon Sep 17 00:00:00 2001 From: Logan Bussell Date: Tue, 27 Oct 2026 17:50:00 -0700 Subject: [PATCH 1/2] Align repository C# formatting with docker-tools Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .editorconfig | 557 ++++++-- .../workflows/update-dependencies-tests.yml | 16 +- Directory.Build.props | 8 + eng/shared/.editorconfig | 5 - eng/shared/DotNetVersion.cs | 5 +- eng/update-dependencies/AzdoAuthProvider.cs | 6 +- eng/update-dependencies/AzdoHttpClient.cs | 2 +- eng/update-dependencies/BuildAssetService.cs | 11 +- eng/update-dependencies/BuildLabelService.cs | 2 +- eng/update-dependencies/ChecksumHelper.cs | 22 +- .../Commands/CreatePullRequestOptions.cs | 28 +- .../Commands/DependencyCommand.cs | 10 +- .../Commands/FromBuildCommand.cs | 8 +- .../Commands/FromBuildOptions.cs | 6 +- .../Commands/FromChannelCommand.cs | 6 +- .../Commands/FromChannelOptions.cs | 6 +- .../Commands/FromGitHubReleaseCommand.cs | 4 +- .../Commands/FromStagingPipelineCommand.cs | 51 +- .../Commands/FromStagingPipelineOptions.cs | 15 +- .../Commands/MonitorCommand.cs | 16 +- .../Commands/PowerShellCommand.cs | 36 +- .../Commands/SyncInternalReleaseCommand.cs | 40 +- .../DependencyUpdateRunner.cs | 30 +- eng/update-dependencies/DirectoryStack.cs | 12 +- eng/update-dependencies/Git/GitRepoHelper.cs | 8 +- .../Git/GitRepoHelperFactory.cs | 10 +- .../Git/GitValidationExtensions.cs | 2 +- .../Git/IRemoteGitRepoHelper.cs | 4 +- .../Git/LocalGitRepoHelper.cs | 22 +- .../Git/RemoteGitRepoFactory.cs | 2 +- .../Git/RemoteGitRepoHelper.cs | 6 +- eng/update-dependencies/GitRemote.cs | 4 +- eng/update-dependencies/ManifestHelper.cs | 15 +- eng/update-dependencies/ManifestVariables.cs | 17 +- .../Model/Release/ReleaseConfig.cs | 4 +- .../PipelineArtifactProvider.cs | 11 +- eng/update-dependencies/PipelinesService.cs | 7 +- eng/update-dependencies/Program.cs | 17 +- eng/update-dependencies/ScriptRunner.cs | 8 +- .../Sync/IInternalVersionsService.cs | 1 + .../Sync/InternalStageContainers.cs | 2 +- .../Sync/InternalVersionsService.cs | 11 +- .../Updaters/AspireUpdater.cs | 8 +- .../Updaters/BaseUrlUpdater.cs | 4 +- .../Updaters/ChiselUpdater.cs | 14 +- .../Updaters/DependencyUpdate.cs | 1 + .../Updaters/DotNetUpdater.cs | 13 +- .../Updaters/MonitorUpdater.cs | 12 +- .../Updaters/NuGetConfigUpdater.cs | 8 +- .../Updaters/PowerShellUpdater.cs | 22 +- .../Updaters/RocksToolboxUpdater.cs | 4 +- eng/update-dependencies/VariableHelper.cs | 4 +- eng/update-dependencies/VersionHelper.cs | 4 +- .../update-dependencies.csproj | 2 +- tests/Microsoft.DotNet.Docker.Tests/Arch.cs | 14 +- .../AspireDashboardImageTests.cs | 7 +- .../AspireDashboardManifestTests.cs | 6 +- .../AspnetImageTests.cs | 172 +-- .../AssemblyInfo.cs | 1 - .../CommonRuntimeImageTests.cs | 118 +- tests/Microsoft.DotNet.Docker.Tests/Config.cs | 184 ++- .../DockerHelper.cs | 583 ++++---- .../DockerRunArgsBuilder.cs | 100 +- .../DockerfileHelper.cs | 1 - .../DockerfileInfo.cs | 4 +- .../DotNetImageRepo.cs | 15 +- .../DotNetImageVariant.cs | 30 +- .../DotNetTheoryAttribute.cs | 11 +- .../DotNetTheoryDiscoverer.cs | 27 +- .../EnvironmentVariableInfo.cs | 129 +- .../ExecuteHelper.cs | 72 +- .../Extensions/ShouldlyExtensions.cs | 1 - .../FileHelper.cs | 165 ++- .../GeneratedArtifactTests.cs | 61 +- .../HttpClientExtensions.cs | 15 +- .../ImageData.cs | 285 ++-- .../ImageVersion.cs | 72 +- .../LinuxImageTheoryAttribute.cs | 14 +- .../Microsoft.DotNet.Docker.Tests/Manifest.cs | 50 +- .../ManifestHelper.cs | 76 +- .../MonitorImageTests.cs | 1221 ++++++++--------- tests/Microsoft.DotNet.Docker.Tests/OS.cs | 1 - tests/Microsoft.DotNet.Docker.Tests/OSInfo.cs | 1 - .../PowerShellTests.cs | 18 +- .../ProductImageData.cs | 318 +++-- .../ProductImageTests.cs | 715 +++++----- .../RuntimeDepsImageTests.cs | 263 ++-- .../RuntimeImageTests.cs | 146 +- .../SampleImageData.cs | 64 +- .../SampleImageTests.cs | 285 ++-- .../SampleImageType.cs | 12 +- .../SdkImageTests.cs | 662 +++++---- .../SkippableTheoryAttribute.cs | 26 +- .../StaticTagTests.cs | 1109 ++++++++------- .../SyftHelper.cs | 29 +- .../TestAppArtifacts/Directory.Build.props | 4 + .../Microsoft.DotNet.Docker.Tests/TestData.cs | 1210 ++++++++-------- .../TestDataTests.cs | 5 +- .../AspireDashboardBasicScenario.cs | 5 +- .../TestScenarios/BlazorWasmScenario.cs | 5 +- .../TestScenarios/ConsoleAppScenario.cs | 12 +- .../TestScenarios/GlobalizationScenario.cs | 22 +- .../TestScenarios/ITestScenario.cs | 3 +- .../TestScenarios/NlsScenario.cs | 24 +- .../ProjectTemplateTestScenario.cs | 56 +- .../ReverseProxyBasicScenario.cs | 14 +- .../TestScenarios/TestDockerfile.cs | 27 +- .../TestScenarios/WebScenario.cs | 29 +- .../TestSolution.cs | 18 +- .../WindowsImageTheoryAttribute.cs | 1 - .../YarpImageTests.cs | 17 +- .../AzureCredentialProviderTests.cs | 12 +- .../BuildLabelServiceTests.cs | 14 +- .../DotNetUpdaterTests.cs | 14 +- .../FromStagingPipelineCommandTests.cs | 52 +- .../InternalStagingBuildsTests.cs | 18 +- .../LocalGitRepoHelperTests.cs | 30 +- .../ManifestVariablesTests.cs | 29 +- .../MonitorUpdaterTests.cs | 12 +- .../NuGetConfigUpdaterTests.cs | 4 +- .../PowerShellUpdaterTests.cs | 60 +- .../SyncInternalReleaseTests.cs | 112 +- tests/UpdateDependencies.Tests/TempRepo.cs | 9 +- .../VersionHelperTests.cs | 5 +- 124 files changed, 5015 insertions(+), 5040 deletions(-) create mode 100644 Directory.Build.props delete mode 100644 eng/shared/.editorconfig create mode 100644 tests/Microsoft.DotNet.Docker.Tests/TestAppArtifacts/Directory.Build.props diff --git a/.editorconfig b/.editorconfig index f29b4f297e..d303bca2be 100644 --- a/.editorconfig +++ b/.editorconfig @@ -15,15 +15,250 @@ trim_trailing_whitespace = true [project.json] indent_size = 2 -# C# files -[*.cs] +# Xml project files +[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,nativeproj,locproj,slnx}] +indent_size = 2 + +[*.{csproj,vbproj,proj,nativeproj,locproj}] +charset = utf-8 + +# Xml build files +[*.builds] +indent_size = 2 + +# Xml files +[*.{xml,stylecop,resx,ruleset}] +indent_size = 2 + +# Xml config files +[*.{props,targets,config,nuspec}] +indent_size = 2 + +# YAML config files +[*.{yml,yaml}] +indent_size = 2 + +# Shell scripts +[*.sh] +end_of_line = lf +[*.{cmd, bat}] +end_of_line = crlf + +# Markdown files +[*.md] +indent_size = 2 + +# Keep in sync with https://github.com/dotnet/docker-tools/blob/main/.editorconfig +# Do not copy suppressions for code-quality issues specific to docker-tools. +# Samples and standalone scripts are outside this scope; test-app fixtures are excluded below. +[{eng/update-dependencies,eng/shared,tests}/**.cs] +tab_width = 4 + +# CS1591: Missing XML comment for publicly visible type or member. +# Public documentation comments are not required for repository tooling and tests. +dotnet_diagnostic.CS1591.severity = none + +# Organize usings and license headers +dotnet_separate_import_directive_groups = false +dotnet_sort_system_directives_first = true +# IDE0005: Remove unnecessary using directives. +dotnet_diagnostic.IDE0005.severity = warning +file_header_template = Licensed to the .NET Foundation under one or more agreements.\nThe .NET Foundation licenses this file to you under the MIT license. +# IDE0073: Require file header. +dotnet_diagnostic.IDE0073.severity = warning + +# this. preferences +dotnet_style_qualification_for_event = false:warning +dotnet_style_qualification_for_field = false:warning +dotnet_style_qualification_for_method = false:warning +dotnet_style_qualification_for_property = false:warning + +# Language keywords vs BCL types +dotnet_style_predefined_type_for_locals_parameters_members = true:warning +dotnet_style_predefined_type_for_member_access = true:warning + +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:warning +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:warning +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:warning +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:warning + +# Modifier preferences +dotnet_style_require_accessibility_modifiers = for_non_interface_members:warning + +# Expression-level preferences +dotnet_prefer_system_hash_code = true:warning +dotnet_style_coalesce_expression = true:warning +dotnet_style_collection_initializer = true:warning +dotnet_style_explicit_tuple_names = true:warning +dotnet_style_namespace_match_folder = true:warning +dotnet_style_null_propagation = true:warning +dotnet_style_object_initializer = true:warning +dotnet_style_operator_placement_when_wrapping = beginning_of_line +dotnet_style_prefer_auto_properties = true:warning +dotnet_style_prefer_collection_expression = when_types_loosely_match:warning +# IDE0305: Prefer collection expressions over fluent collection creation. +# Keep fluent calls rather than converting them to collection spread expressions. +dotnet_diagnostic.IDE0305.severity = silent +dotnet_style_prefer_compound_assignment = true:warning +dotnet_style_prefer_conditional_expression_over_assignment = true:warning +dotnet_style_prefer_conditional_expression_over_return = false:silent +dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed:warning +dotnet_style_prefer_inferred_anonymous_type_member_names = true:warning +dotnet_style_prefer_inferred_tuple_names = true:warning +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:warning +dotnet_style_prefer_non_hidden_explicit_cast_in_source = true:warning +dotnet_style_prefer_simplified_boolean_expressions = true:warning +dotnet_style_prefer_simplified_interpolation = true:warning +# IDE0001: Simplify qualified type names in declarations and expressions. +dotnet_diagnostic.IDE0001.severity = warning +# IDE0002: Simplify qualified static member access. +dotnet_diagnostic.IDE0002.severity = warning +# IDE0004: Remove unnecessary casts. +dotnet_diagnostic.IDE0004.severity = warning +# IDE0010: Add missing cases to switch statements. +# Allow a default label to handle enum members without listing each one explicitly. +dotnet_diagnostic.IDE0010.severity = silent +# IDE0043: Detect invalid composite format strings. +dotnet_diagnostic.IDE0043.severity = warning +# IDE0051: Remove unused private members. +dotnet_diagnostic.IDE0051.severity = warning +# IDE0052: Remove private members whose values are written but never read. +dotnet_diagnostic.IDE0052.severity = warning +# IDE0082: Replace typeof(T).Name with nameof(T). +dotnet_diagnostic.IDE0082.severity = warning +# IDE0100: Remove unnecessary equality comparisons against Boolean constants. +dotnet_diagnostic.IDE0100.severity = warning +# IDE0120: Simplify LINQ expressions by removing redundant operations. +dotnet_diagnostic.IDE0120.severity = warning +# IDE0121: Replace LINQ Where type checks followed by Cast with OfType. +dotnet_diagnostic.IDE0121.severity = warning +# RE0001: Validate regular expression patterns. +dotnet_diagnostic.RE0001.severity = warning +# JSON001: Validate JSON strings. +dotnet_diagnostic.JSON001.severity = warning + +# Field and parameter preferences +dotnet_style_readonly_field = true:warning +dotnet_code_quality_unused_parameters = all:warning +# IDE0280: Use nameof instead of a string literal when an attribute names a symbol. +dotnet_diagnostic.IDE0280.severity = warning + +# Suppression preferences +dotnet_remove_unnecessary_suppression_exclusions = none +# IDE0076: Remove global SuppressMessage attributes with invalid scope or target values. +dotnet_diagnostic.IDE0076.severity = warning +# IDE0077: Use documentation comment IDs in global SuppressMessage attributes. +dotnet_diagnostic.IDE0077.severity = warning +# IDE0079: Remove suppressions that do not suppress a diagnostic. +dotnet_diagnostic.IDE0079.severity = warning +# IDE0370: Remove null-forgiving operators that do not suppress a nullable warning. +dotnet_diagnostic.IDE0370.severity = warning + +# New line preferences +dotnet_style_allow_multiple_blank_lines_experimental = false:warning +dotnet_style_allow_statement_immediately_after_block_experimental = false:warning + +# var preferences +csharp_style_var_elsewhere = false:warning +csharp_style_var_for_built_in_types = false:warning +csharp_style_var_when_type_is_apparent = false:warning + +# Expression-bodied members +csharp_style_expression_bodied_accessors = when_on_single_line:warning +csharp_style_expression_bodied_constructors = when_on_single_line:warning +csharp_style_expression_bodied_indexers = when_on_single_line:warning +csharp_style_expression_bodied_lambdas = when_on_single_line:warning +csharp_style_expression_bodied_local_functions = when_on_single_line:warning +csharp_style_expression_bodied_methods = when_on_single_line:warning +csharp_style_expression_bodied_operators = when_on_single_line:warning +csharp_style_expression_bodied_properties = when_on_single_line:warning + +# Pattern matching preferences +csharp_style_pattern_matching_over_as_with_null_check = true:warning +csharp_style_pattern_matching_over_is_with_cast_check = true:warning +csharp_style_prefer_extended_property_pattern = true:warning +csharp_style_prefer_not_pattern = true:warning +csharp_style_prefer_pattern_matching = true:warning +csharp_style_prefer_switch_expression = true:warning + +# Null-checking preferences +csharp_style_conditional_delegate_call = true:warning + +# Modifier preferences +csharp_prefer_static_anonymous_function = true:warning +csharp_prefer_static_local_function = true:warning +csharp_preferred_modifier_order = public,private,protected,internal,file,const,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async:warning +csharp_style_prefer_readonly_struct = true:warning +csharp_style_prefer_readonly_struct_member = true:warning +# IDE0064: Make readonly struct fields writable when the struct assigns to this. +dotnet_diagnostic.IDE0064.severity = warning +# IDE0380: Remove unnecessary unsafe modifiers. +dotnet_diagnostic.IDE0380.severity = warning +# IDE0390: Make async methods without await synchronous when their signatures can change. +dotnet_diagnostic.IDE0390.severity = warning +# IDE0391: Remove async from methods without await when their signatures cannot change. +dotnet_diagnostic.IDE0391.severity = warning + +# Code-block preferences +csharp_prefer_braces = true:warning +csharp_prefer_simple_using_statement = true:warning +csharp_prefer_system_threading_lock = true:warning +csharp_style_namespace_declarations = file_scoped:warning +csharp_style_prefer_labeled_jump_statements = true:warning +csharp_style_prefer_method_group_conversion = true:warning +csharp_style_prefer_primary_constructors = true:warning +csharp_style_prefer_simple_property_accessors = true:warning +csharp_style_prefer_top_level_statements = true:warning + +# Expression-level preferences +csharp_prefer_simple_default_expression = true:warning +csharp_style_deconstructed_variable_declaration = true:warning +csharp_style_implicit_object_creation_when_type_is_apparent = false:warning +csharp_style_inlined_variable_declaration = true:warning +csharp_style_prefer_implicitly_typed_lambda_expression = true:warning +csharp_style_prefer_index_operator = true:warning +csharp_style_prefer_local_over_anonymous_function = true:warning +csharp_style_prefer_null_check_over_type_check = true:warning +csharp_style_prefer_range_operator = true:warning +csharp_style_prefer_tuple_swap = true:warning +csharp_style_prefer_unbound_generic_type_in_nameof = true:warning +csharp_style_prefer_utf8_string_literals = true:warning +csharp_style_throw_expression = true:warning +csharp_style_unused_value_assignment_preference = discard_variable:warning +csharp_style_unused_value_expression_statement_preference = discard_variable:silent +# IDE0072: Add missing arms to switch expressions. +# Allow a discard arm to handle enum members without listing each one explicitly. +dotnet_diagnostic.IDE0072.severity = silent +# IDE0080: Remove unnecessary null-forgiving operators in is expressions. +dotnet_diagnostic.IDE0080.severity = warning +# IDE0110: Remove unnecessary discard designations in patterns. +dotnet_diagnostic.IDE0110.severity = warning +# IDE0240: Remove nullable directives that repeat the current nullable context. +dotnet_diagnostic.IDE0240.severity = warning +# IDE0241: Remove nullable directives that have no effect on nullable analysis. +dotnet_diagnostic.IDE0241.severity = warning + +# using directive preferences +csharp_using_directive_placement = outside_namespace:warning + +# New line preferences +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = false:warning +csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true:silent +csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = false:warning +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = false:warning +csharp_style_allow_embedded_statements_on_same_line_experimental = false:warning + +# IDE0055: Enforce whitespace, indentation, and formatting rules on build. +dotnet_diagnostic.IDE0055.severity = warning + # New line preferences -csharp_new_line_before_open_brace = all -csharp_new_line_before_else = true csharp_new_line_before_catch = true +csharp_new_line_before_else = true csharp_new_line_before_finally = true -csharp_new_line_before_members_in_object_initializers = true csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_open_brace = all csharp_new_line_between_query_expression_clauses = true # Indentation preferences @@ -31,103 +266,8 @@ csharp_indent_block_contents = true csharp_indent_braces = false csharp_indent_case_contents = true csharp_indent_case_contents_when_block = true -csharp_indent_switch_labels = true csharp_indent_labels = one_less_than_current - -# Modifier preferences -csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion - -# avoid this. unless absolutely necessary -dotnet_style_qualification_for_field = false:suggestion -dotnet_style_qualification_for_property = false:suggestion -dotnet_style_qualification_for_method = false:suggestion -dotnet_style_qualification_for_event = false:suggestion - -# Types: use keywords instead of BCL types, and permit var only when the type is clear -csharp_style_var_for_built_in_types = false:suggestion -csharp_style_var_when_type_is_apparent = false:none -csharp_style_var_elsewhere = false:suggestion -dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion -dotnet_style_predefined_type_for_member_access = true:suggestion - -# name all constant fields using PascalCase -dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion -dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields -dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style -dotnet_naming_symbols.constant_fields.applicable_kinds = field -dotnet_naming_symbols.constant_fields.required_modifiers = const -dotnet_naming_style.pascal_case_style.capitalization = pascal_case - -# static fields should have s_ prefix -dotnet_naming_rule.static_fields_should_have_prefix.severity = suggestion -dotnet_naming_rule.static_fields_should_have_prefix.symbols = static_fields -dotnet_naming_rule.static_fields_should_have_prefix.style = static_prefix_style -dotnet_naming_symbols.static_fields.applicable_kinds = field -dotnet_naming_symbols.static_fields.required_modifiers = static -dotnet_naming_symbols.static_fields.applicable_accessibilities = private, internal, private_protected -dotnet_naming_style.static_prefix_style.required_prefix = s_ -dotnet_naming_style.static_prefix_style.capitalization = camel_case - -# internal and private fields should be _camelCase -dotnet_naming_rule.camel_case_for_private_internal_fields.severity = suggestion -dotnet_naming_rule.camel_case_for_private_internal_fields.symbols = private_internal_fields -dotnet_naming_rule.camel_case_for_private_internal_fields.style = camel_case_underscore_style -dotnet_naming_symbols.private_internal_fields.applicable_kinds = field -dotnet_naming_symbols.private_internal_fields.applicable_accessibilities = private, internal -dotnet_naming_style.camel_case_underscore_style.required_prefix = _ -dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case - -# Code style defaults -csharp_using_directive_placement = outside_namespace:suggestion -dotnet_sort_system_directives_first = true -csharp_prefer_braces = true:silent -csharp_preserve_single_line_blocks = true:none -csharp_preserve_single_line_statements = false:none -csharp_prefer_static_local_function = true:suggestion -csharp_prefer_simple_using_statement = false:none -csharp_style_prefer_switch_expression = true:suggestion - -# Code quality -dotnet_style_readonly_field = true:suggestion -dotnet_code_quality_unused_parameters = non_public:suggestion - -# Expression-level preferences -dotnet_style_object_initializer = true:suggestion -dotnet_style_collection_initializer = true:suggestion -dotnet_style_explicit_tuple_names = true:suggestion -dotnet_style_coalesce_expression = true:suggestion -dotnet_style_null_propagation = true:suggestion -dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion -dotnet_style_prefer_inferred_tuple_names = true:suggestion -dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion -dotnet_style_prefer_auto_properties = true:suggestion -dotnet_style_prefer_conditional_expression_over_assignment = true:silent -dotnet_style_prefer_conditional_expression_over_return = true:silent -csharp_prefer_simple_default_expression = true:suggestion - -# Expression-bodied members -csharp_style_expression_bodied_methods = true:silent -csharp_style_expression_bodied_constructors = true:silent -csharp_style_expression_bodied_operators = true:silent -csharp_style_expression_bodied_properties = true:silent -csharp_style_expression_bodied_indexers = true:silent -csharp_style_expression_bodied_accessors = true:silent -csharp_style_expression_bodied_lambdas = true:silent -csharp_style_expression_bodied_local_functions = true:silent - -# Pattern matching -csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion -csharp_style_pattern_matching_over_as_with_null_check = true:suggestion -csharp_style_inlined_variable_declaration = true:suggestion - -# Null checking preferences -csharp_style_throw_expression = true:suggestion -csharp_style_conditional_delegate_call = true:suggestion - -# Other features -csharp_style_prefer_index_operator = false:none -csharp_style_prefer_range_operator = false:none -csharp_style_pattern_local_over_anonymous_function = false:none +csharp_indent_switch_labels = true # Space preferences csharp_space_after_cast = false @@ -137,7 +277,7 @@ csharp_space_after_dot = false csharp_space_after_keywords_in_control_flow_statements = true csharp_space_after_semicolon_in_for_statement = true csharp_space_around_binary_operators = before_and_after -csharp_space_around_declaration_statements = do_not_ignore +csharp_space_around_declaration_statements = false csharp_space_before_colon_in_inheritance_clause = true csharp_space_before_comma = false csharp_space_before_dot = false @@ -153,42 +293,195 @@ csharp_space_between_method_declaration_parameter_list_parentheses = false csharp_space_between_parentheses = false csharp_space_between_square_brackets = false -# Analyzers -dotnet_code_quality.ca1802.api_surface = private, internal -dotnet_code_quality.ca2208.api_surface = public +# Wrapping preferences +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true -# License header -file_header_template = Licensed to the .NET Foundation under one or more agreements.\nThe .NET Foundation licenses this file to you under the MIT license.\n +# Naming rules +dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.severity = warning +dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.symbols = types_and_namespaces +dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.style = pascalcase -# Xml project files -[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,nativeproj,locproj,slnx}] -indent_size = 2 +dotnet_naming_rule.interfaces_should_be_ipascalcase.severity = warning +dotnet_naming_rule.interfaces_should_be_ipascalcase.symbols = interfaces +dotnet_naming_rule.interfaces_should_be_ipascalcase.style = ipascalcase -[*.{csproj,vbproj,proj,nativeproj,locproj}] -charset = utf-8 +dotnet_naming_rule.type_parameters_should_be_tpascalcase.severity = warning +dotnet_naming_rule.type_parameters_should_be_tpascalcase.symbols = type_parameters +dotnet_naming_rule.type_parameters_should_be_tpascalcase.style = tpascalcase -# Xml build files -[*.builds] -indent_size = 2 +dotnet_naming_rule.methods_should_be_pascalcase.severity = warning +dotnet_naming_rule.methods_should_be_pascalcase.symbols = methods +dotnet_naming_rule.methods_should_be_pascalcase.style = pascalcase -# Xml files -[*.{xml,stylecop,resx,ruleset}] -indent_size = 2 +dotnet_naming_rule.properties_should_be_pascalcase.severity = warning +dotnet_naming_rule.properties_should_be_pascalcase.symbols = properties +dotnet_naming_rule.properties_should_be_pascalcase.style = pascalcase -# Xml config files -[*.{props,targets,config,nuspec}] -indent_size = 2 +dotnet_naming_rule.events_should_be_pascalcase.severity = warning +dotnet_naming_rule.events_should_be_pascalcase.symbols = events +dotnet_naming_rule.events_should_be_pascalcase.style = pascalcase -# YAML config files -[*.{yml,yaml}] -indent_size = 2 +dotnet_naming_rule.local_variables_should_be_camelcase.severity = warning +dotnet_naming_rule.local_variables_should_be_camelcase.symbols = local_variables +dotnet_naming_rule.local_variables_should_be_camelcase.style = camelcase -# Shell scripts -[*.sh] -end_of_line = lf -[*.{cmd, bat}] -end_of_line = crlf +dotnet_naming_rule.local_constants_should_be_camelcase.severity = warning +dotnet_naming_rule.local_constants_should_be_camelcase.symbols = local_constants +dotnet_naming_rule.local_constants_should_be_camelcase.style = camelcase -# Markdown files -[*.md] -indent_size = 2 +dotnet_naming_rule.parameters_should_be_camelcase.severity = warning +dotnet_naming_rule.parameters_should_be_camelcase.symbols = parameters +dotnet_naming_rule.parameters_should_be_camelcase.style = camelcase + +dotnet_naming_rule.public_fields_should_be_pascalcase.severity = warning +dotnet_naming_rule.public_fields_should_be_pascalcase.symbols = public_fields +dotnet_naming_rule.public_fields_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.private_fields_should_be__camelcase.severity = warning +dotnet_naming_rule.private_fields_should_be__camelcase.symbols = private_fields +dotnet_naming_rule.private_fields_should_be__camelcase.style = _camelcase + +dotnet_naming_rule.private_static_fields_should_be_s_camelcase.severity = warning +dotnet_naming_rule.private_static_fields_should_be_s_camelcase.symbols = private_static_fields +dotnet_naming_rule.private_static_fields_should_be_s_camelcase.style = s_camelcase + +dotnet_naming_rule.public_constant_fields_should_be_pascalcase.severity = warning +dotnet_naming_rule.public_constant_fields_should_be_pascalcase.symbols = public_constant_fields +dotnet_naming_rule.public_constant_fields_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.private_constant_fields_should_be_pascalcase.severity = warning +dotnet_naming_rule.private_constant_fields_should_be_pascalcase.symbols = private_constant_fields +dotnet_naming_rule.private_constant_fields_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.severity = warning +dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.symbols = public_static_readonly_fields +dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.severity = warning +dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.symbols = private_static_readonly_fields +dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.enums_should_be_pascalcase.severity = warning +dotnet_naming_rule.enums_should_be_pascalcase.symbols = enums +dotnet_naming_rule.enums_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.local_functions_should_be_pascalcase.severity = warning +dotnet_naming_rule.local_functions_should_be_pascalcase.symbols = local_functions +dotnet_naming_rule.local_functions_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.non_field_members_should_be_pascalcase.severity = warning +dotnet_naming_rule.non_field_members_should_be_pascalcase.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascalcase.style = pascalcase + +# Symbol specifications +dotnet_naming_symbols.interfaces.applicable_kinds = interface +dotnet_naming_symbols.interfaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interfaces.required_modifiers = + +dotnet_naming_symbols.enums.applicable_kinds = enum +dotnet_naming_symbols.enums.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.enums.required_modifiers = + +dotnet_naming_symbols.events.applicable_kinds = event +dotnet_naming_symbols.events.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.events.required_modifiers = + +dotnet_naming_symbols.methods.applicable_kinds = method +dotnet_naming_symbols.methods.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.methods.required_modifiers = + +dotnet_naming_symbols.properties.applicable_kinds = property +dotnet_naming_symbols.properties.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.properties.required_modifiers = + +dotnet_naming_symbols.public_fields.applicable_kinds = field +dotnet_naming_symbols.public_fields.applicable_accessibilities = public, internal +dotnet_naming_symbols.public_fields.required_modifiers = + +dotnet_naming_symbols.private_fields.applicable_kinds = field +dotnet_naming_symbols.private_fields.applicable_accessibilities = private, protected, protected_internal, private_protected +dotnet_naming_symbols.private_fields.required_modifiers = + +dotnet_naming_symbols.private_static_fields.applicable_kinds = field +dotnet_naming_symbols.private_static_fields.applicable_accessibilities = private, protected, protected_internal, private_protected +dotnet_naming_symbols.private_static_fields.required_modifiers = static + +dotnet_naming_symbols.types_and_namespaces.applicable_kinds = namespace, class, struct, interface, enum +dotnet_naming_symbols.types_and_namespaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types_and_namespaces.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +dotnet_naming_symbols.type_parameters.applicable_kinds = type_parameter +dotnet_naming_symbols.type_parameters.applicable_accessibilities = * +dotnet_naming_symbols.type_parameters.required_modifiers = + +dotnet_naming_symbols.private_constant_fields.applicable_kinds = field +dotnet_naming_symbols.private_constant_fields.applicable_accessibilities = private, protected, protected_internal, private_protected +dotnet_naming_symbols.private_constant_fields.required_modifiers = const + +dotnet_naming_symbols.local_variables.applicable_kinds = local +dotnet_naming_symbols.local_variables.applicable_accessibilities = local +dotnet_naming_symbols.local_variables.required_modifiers = + +dotnet_naming_symbols.local_constants.applicable_kinds = local +dotnet_naming_symbols.local_constants.applicable_accessibilities = local +dotnet_naming_symbols.local_constants.required_modifiers = const + +dotnet_naming_symbols.parameters.applicable_kinds = parameter +dotnet_naming_symbols.parameters.applicable_accessibilities = * +dotnet_naming_symbols.parameters.required_modifiers = + +dotnet_naming_symbols.public_constant_fields.applicable_kinds = field +dotnet_naming_symbols.public_constant_fields.applicable_accessibilities = public, internal +dotnet_naming_symbols.public_constant_fields.required_modifiers = const + +dotnet_naming_symbols.public_static_readonly_fields.applicable_kinds = field +dotnet_naming_symbols.public_static_readonly_fields.applicable_accessibilities = public, internal +dotnet_naming_symbols.public_static_readonly_fields.required_modifiers = readonly, static + +dotnet_naming_symbols.private_static_readonly_fields.applicable_kinds = field +dotnet_naming_symbols.private_static_readonly_fields.applicable_accessibilities = private, protected, protected_internal, private_protected +dotnet_naming_symbols.private_static_readonly_fields.required_modifiers = readonly, static + +dotnet_naming_symbols.local_functions.applicable_kinds = local_function +dotnet_naming_symbols.local_functions.applicable_accessibilities = * +dotnet_naming_symbols.local_functions.required_modifiers = + +# Naming styles +dotnet_naming_style.pascalcase.required_prefix = +dotnet_naming_style.pascalcase.required_suffix = +dotnet_naming_style.pascalcase.word_separator = +dotnet_naming_style.pascalcase.capitalization = pascal_case + +dotnet_naming_style.ipascalcase.required_prefix = I +dotnet_naming_style.ipascalcase.required_suffix = +dotnet_naming_style.ipascalcase.word_separator = +dotnet_naming_style.ipascalcase.capitalization = pascal_case + +dotnet_naming_style.tpascalcase.required_prefix = T +dotnet_naming_style.tpascalcase.required_suffix = +dotnet_naming_style.tpascalcase.word_separator = +dotnet_naming_style.tpascalcase.capitalization = pascal_case + +dotnet_naming_style._camelcase.required_prefix = _ +dotnet_naming_style._camelcase.required_suffix = +dotnet_naming_style._camelcase.word_separator = +dotnet_naming_style._camelcase.capitalization = camel_case + +dotnet_naming_style.camelcase.required_prefix = +dotnet_naming_style.camelcase.required_suffix = +dotnet_naming_style.camelcase.word_separator = +dotnet_naming_style.camelcase.capitalization = camel_case + +dotnet_naming_style.s_camelcase.required_prefix = s_ +dotnet_naming_style.s_camelcase.required_suffix = +dotnet_naming_style.s_camelcase.word_separator = +dotnet_naming_style.s_camelcase.capitalization = camel_case + +[tests/Microsoft.DotNet.Docker.Tests/TestAppArtifacts/**.cs] +# These are inputs to generated test applications, not part of the test runner. +generated_code = true diff --git a/.github/workflows/update-dependencies-tests.yml b/.github/workflows/update-dependencies-tests.yml index 5089a79d8a..d593747a65 100644 --- a/.github/workflows/update-dependencies-tests.yml +++ b/.github/workflows/update-dependencies-tests.yml @@ -3,8 +3,13 @@ name: Update Dependencies Tests on: pull_request: paths: + - '.editorconfig' + - 'Directory.Build.props' + - 'Microsoft.DotNet.Docker.slnx' - 'eng/update-dependencies/**' - - 'tests/UpdateDependencies.Tests/**' + - 'eng/shared/**' + - 'tests/**' + - '!tests/Microsoft.DotNet.Docker.Tests/TestAppArtifacts/**' - '.github/workflows/update-dependencies-tests.yml' permissions: {} @@ -28,13 +33,16 @@ jobs: - name: Restore dependencies run: > dotnet restore - tests/UpdateDependencies.Tests/UpdateDependencies.Tests.csproj + Microsoft.DotNet.Docker.slnx --verbosity normal - - name: Build tests + - name: Verify formatting + run: dotnet format Microsoft.DotNet.Docker.slnx --no-restore --verify-no-changes + + - name: Build solution run: > dotnet build - tests/UpdateDependencies.Tests/UpdateDependencies.Tests.csproj + Microsoft.DotNet.Docker.slnx --no-restore - name: Run tests diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000000..110ec78439 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,8 @@ + + + true + true + + true + + diff --git a/eng/shared/.editorconfig b/eng/shared/.editorconfig deleted file mode 100644 index 46a15adc27..0000000000 --- a/eng/shared/.editorconfig +++ /dev/null @@ -1,5 +0,0 @@ -[*.cs] -# CS1591: Missing XML comment for publicly visible type or member -# https://learn.microsoft.com/dotnet/csharp/language-reference/compiler-messages/cs1591 -# Since this is a shared project, public members should be documented. -dotnet_diagnostic.CS1591.severity = suggestion diff --git a/eng/shared/DotNetVersion.cs b/eng/shared/DotNetVersion.cs index 5115c6596b..501a6b4021 100644 --- a/eng/shared/DotNetVersion.cs +++ b/eng/shared/DotNetVersion.cs @@ -1,6 +1,5 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. using System.Text.RegularExpressions; using NuGet.Versioning; @@ -31,7 +30,7 @@ public sealed partial class DotNetVersion(SemanticVersion version) : SemanticVer // SemanticVersion only handles full, strict semantic versions. // If we can parse it as a full semantic version, use that. It resolves // full build versions correctly including prerelease suffixes (e.g. '-preview.7') - if (SemanticVersion.TryParse(versionString, out var fullSemanticVersion)) + if (TryParse(versionString, out SemanticVersion? fullSemanticVersion)) { return new DotNetVersion(fullSemanticVersion); } @@ -42,7 +41,7 @@ public sealed partial class DotNetVersion(SemanticVersion version) : SemanticVer // we don't need to account for that in our parsing. // Don't use System.Version.Parse here, because it defaults unmatched // minor/patch versions to -1, whereas we want them to default to 0. - var matchGroups = MajorMinorVersionRegex.Match(versionString).Groups; + GroupCollection matchGroups = MajorMinorVersionRegex.Match(versionString).Groups; int major = int.Parse(matchGroups["major"].Value); int minor = int.TryParse(matchGroups["minor"].Value.TrimStart('.'), out int minorValue) ? minorValue : 0; return new DotNetVersion(new SemanticVersion(major, minor, 0)); diff --git a/eng/update-dependencies/AzdoAuthProvider.cs b/eng/update-dependencies/AzdoAuthProvider.cs index 2f4c56c8dd..9bc902e248 100644 --- a/eng/update-dependencies/AzdoAuthProvider.cs +++ b/eng/update-dependencies/AzdoAuthProvider.cs @@ -55,9 +55,9 @@ public string AccessToken /// public VssConnection GetVssConnection(string azdoOrg) { - var baseUrl = new Uri(azdoOrg); - var credential = new VssBasicCredential(userName: string.Empty, password: AccessToken); - var connection = new VssConnection(baseUrl, credential); + Uri baseUrl = new Uri(azdoOrg); + VssBasicCredential credential = new VssBasicCredential(userName: string.Empty, password: AccessToken); + VssConnection connection = new VssConnection(baseUrl, credential); return connection; } } diff --git a/eng/update-dependencies/AzdoHttpClient.cs b/eng/update-dependencies/AzdoHttpClient.cs index 458a956a01..cdd3bdecc8 100644 --- a/eng/update-dependencies/AzdoHttpClient.cs +++ b/eng/update-dependencies/AzdoHttpClient.cs @@ -11,7 +11,7 @@ internal class AzdoHttpClient(IAzdoAuthProvider azdoAuthProvider, HttpClient htt { public async Task GetAsync(string requestUri, CancellationToken ct = default) { - using var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); request.Headers.Authorization = new AuthenticationHeaderValue( "Basic", Convert.ToBase64String( diff --git a/eng/update-dependencies/BuildAssetService.cs b/eng/update-dependencies/BuildAssetService.cs index f250913443..b544d5ee99 100644 --- a/eng/update-dependencies/BuildAssetService.cs +++ b/eng/update-dependencies/BuildAssetService.cs @@ -23,7 +23,7 @@ public async Task GetAssetTextContentsAsync(Asset asset) try { _logger.LogInformation("Fetching contents from {url}", url); - var response = await _httpClient.GetAsync(url); + HttpResponseMessage response = await _httpClient.GetAsync(url); response.EnsureSuccessStatusCode(); string content = await response.Content.ReadAsStringAsync(); _logger.LogInformation( @@ -44,8 +44,8 @@ public async Task GetAssetTextContentsAsync(Asset asset) private string ResolveAssetUrl(Asset asset) { - var blobStorageLocations = asset.Locations.Where(l => l.Location.Contains("blob.core.windows.net")); - var azdoLocations = asset.Locations.Where(l => l.Location.Contains("dev.azure.com")); + IEnumerable blobStorageLocations = asset.Locations.Where(l => l.Location.Contains("blob.core.windows.net")); + IEnumerable azdoLocations = asset.Locations.Where(l => l.Location.Contains("dev.azure.com")); string allLocations = string.Join(Environment.NewLine, asset.Locations.Select(l => $"Type: {l.Type}; Url: {l.Location}")); @@ -67,8 +67,5 @@ private string ResolveAssetUrl(Asset asset) return url; } - private static string FormatErrorMessage(Asset asset, string message) - { - return $"Build {asset.BuildId} Asset {asset.Name} (version {asset.Version}) {message}"; - } + private static string FormatErrorMessage(Asset asset, string message) => $"Build {asset.BuildId} Asset {asset.Name} (version {asset.Version}) {message}"; } diff --git a/eng/update-dependencies/BuildLabelService.cs b/eng/update-dependencies/BuildLabelService.cs index 47f11bed62..b1e29a041a 100644 --- a/eng/update-dependencies/BuildLabelService.cs +++ b/eng/update-dependencies/BuildLabelService.cs @@ -29,7 +29,7 @@ internal sealed class BuildLabelService(TextWriter output) : IBuildLabelService /// public void AddBuildTags(params IEnumerable tags) { - foreach (var tag in tags) + foreach (string tag in tags) { output.WriteLine($"##vso[build.addbuildtag]{tag}"); } diff --git a/eng/update-dependencies/ChecksumHelper.cs b/eng/update-dependencies/ChecksumHelper.cs index 51c9c1a5ea..92acdc4880 100644 --- a/eng/update-dependencies/ChecksumHelper.cs +++ b/eng/update-dependencies/ChecksumHelper.cs @@ -1,3 +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.Diagnostics; using System.Security.Cryptography; using System.Text; @@ -17,18 +20,17 @@ internal static class ChecksumHelper { if (response.IsSuccessStatusCode) { - using (Stream httpStream = await response.Content.ReadAsStreamAsync(cancellationToken)) - using (SHA512 hash = SHA512.Create()) - { - byte[] hashedInputBytes = hash.ComputeHash(httpStream); + using Stream httpStream = await response.Content.ReadAsStreamAsync(cancellationToken); + using SHA512 hash = SHA512.Create(); + byte[] hashedInputBytes = hash.ComputeHash(httpStream); - StringBuilder stringBuilder = new(128); - foreach (byte b in hashedInputBytes) - { - stringBuilder.Append(b.ToString("X2")); - } - sha = stringBuilder.ToString(); + StringBuilder stringBuilder = new(128); + foreach (byte b in hashedInputBytes) + { + stringBuilder.Append(b.ToString("X2")); } + + sha = stringBuilder.ToString(); } else { diff --git a/eng/update-dependencies/Commands/CreatePullRequestOptions.cs b/eng/update-dependencies/Commands/CreatePullRequestOptions.cs index 1f42c5cc25..bf4162d491 100644 --- a/eng/update-dependencies/Commands/CreatePullRequestOptions.cs +++ b/eng/update-dependencies/Commands/CreatePullRequestOptions.cs @@ -12,22 +12,22 @@ public record CreatePullRequestOptions public string TargetBranch { get; set; } = "nightly"; public bool SubmitPullRequest { get; set; } - private static readonly Option s_repoRoot = new("--repo-root") + private static readonly Option RepoRootOption = new("--repo-root") { Description = "The root of the dotnet-docker repo to run against (defaults to current working directory)", DefaultValueFactory = _ => Directory.GetCurrentDirectory(), }; - private static readonly Option s_submitPullRequest = new("--submit-pr") + private static readonly Option SubmitPullRequestOption = new("--submit-pr") { Description = "Apply updates in a temporary clone and submit a pull request with them." + " By default, updates are applied to the local repo and no git operations are run.", }; - private static readonly Option s_sourceBranch = new("--source-branch") + private static readonly Option SourceBranchOption = new("--source-branch") { Description = "Branch to pull updates from", DefaultValueFactory = _ => "", }; - private static readonly Option s_targetBranch = new("--target-branch") + private static readonly Option TargetBranchOption = new("--target-branch") { Description = "Pull request will be submitted targeting this branch", DefaultValueFactory = _ => "nightly", @@ -35,10 +35,10 @@ public record CreatePullRequestOptions public static void AddTo(Command command) { - command.Options.Add(s_repoRoot); - command.Options.Add(s_submitPullRequest); - command.Options.Add(s_sourceBranch); - command.Options.Add(s_targetBranch); + command.Options.Add(RepoRootOption); + command.Options.Add(SubmitPullRequestOption); + command.Options.Add(SourceBranchOption); + command.Options.Add(TargetBranchOption); } public static CreatePullRequestOptions Bind(ParseResult result) => @@ -47,17 +47,17 @@ public static CreatePullRequestOptions Bind(ParseResult result) => protected static TOptions Bind(ParseResult result, TOptions options) where TOptions : CreatePullRequestOptions { - options.RepoRoot = result.GetRequiredValue(s_repoRoot); - options.SubmitPullRequest = result.GetValue(s_submitPullRequest); - options.SourceBranch = result.GetRequiredValue(s_sourceBranch); - options.TargetBranch = result.GetRequiredValue(s_targetBranch); + options.RepoRoot = result.GetRequiredValue(RepoRootOption); + options.SubmitPullRequest = result.GetValue(SubmitPullRequestOption); + options.SourceBranch = result.GetRequiredValue(SourceBranchOption); + options.TargetBranch = result.GetRequiredValue(TargetBranchOption); return options; } public string CreatePullRequestBranchName(string name, string buildId = "") { - var buildIdSuffix = string.IsNullOrWhiteSpace(buildId) ? string.Empty : $"-{buildId}"; - var sanitizedTargetBranch = TargetBranch.Replace('/', '-'); + string buildIdSuffix = string.IsNullOrWhiteSpace(buildId) ? string.Empty : $"-{buildId}"; + string sanitizedTargetBranch = TargetBranch.Replace('/', '-'); return $"{sanitizedTargetBranch}/{name}{buildIdSuffix}"; } } diff --git a/eng/update-dependencies/Commands/DependencyCommand.cs b/eng/update-dependencies/Commands/DependencyCommand.cs index 75370d6d5d..c9aacfcdf4 100644 --- a/eng/update-dependencies/Commands/DependencyCommand.cs +++ b/eng/update-dependencies/Commands/DependencyCommand.cs @@ -10,19 +10,27 @@ internal static class DependencyCommand { public static Command CreateCliCommand(IServiceProvider services) where TUpdater : class, IUpdater { - var command = new Command(TUpdater.Name, $"Update {TUpdater.Name}"); + Command command = new Command(TUpdater.Name, $"Update {TUpdater.Name}"); if (typeof(IBarBuildUpdater).IsAssignableFrom(typeof(TUpdater))) + { command.Subcommands.Add(FromBuildCommand.CreateCliCommand(services)); + } if (typeof(IBarChannelUpdater).IsAssignableFrom(typeof(TUpdater))) + { command.Subcommands.Add(FromChannelCommand.CreateCliCommand(services)); + } if (typeof(IStagingPipelineUpdater).IsAssignableFrom(typeof(TUpdater))) + { command.Subcommands.Add(FromStagingPipelineCommand.CreateCliCommand(services)); + } if (typeof(IGitHubReleaseUpdater).IsAssignableFrom(typeof(TUpdater))) + { FromGitHubReleaseCommand.Configure(command, services); + } return command; } diff --git a/eng/update-dependencies/Commands/FromBuildCommand.cs b/eng/update-dependencies/Commands/FromBuildCommand.cs index 17cc4e3929..ed588f2830 100644 --- a/eng/update-dependencies/Commands/FromBuildCommand.cs +++ b/eng/update-dependencies/Commands/FromBuildCommand.cs @@ -13,15 +13,15 @@ internal static class FromBuildCommand { public static Command CreateCliCommand(IServiceProvider services) where TUpdater : class, IUpdater { - var command = new Command("build-id", "Update from a specific BAR build"); + Command command = new Command("build-id", "Update from a specific BAR build"); FromBuildOptions.AddTo(command); command.SetAction(async (result, cancellationToken) => { FromBuildOptions options = FromBuildOptions.Bind(result); - var barClient = services.GetRequiredService(); - var updater = (IBarBuildUpdater)services.GetRequiredService(); - var runner = services.GetRequiredService(); + IBasicBarClient barClient = services.GetRequiredService(); + IBarBuildUpdater updater = (IBarBuildUpdater)services.GetRequiredService(); + DependencyUpdateRunner runner = services.GetRequiredService(); Build build = await barClient.GetBuildAsync(options.Id).WaitAsync(cancellationToken); DependencyUpdate update = await updater.ResolveFromBarBuildAsync(build, cancellationToken); diff --git a/eng/update-dependencies/Commands/FromBuildOptions.cs b/eng/update-dependencies/Commands/FromBuildOptions.cs index 660a36ab16..4753746e57 100644 --- a/eng/update-dependencies/Commands/FromBuildOptions.cs +++ b/eng/update-dependencies/Commands/FromBuildOptions.cs @@ -9,14 +9,14 @@ internal record FromBuildOptions : CreatePullRequestOptions { public required int Id { get; init; } - private static readonly PositiveIdArgument s_id = new("The BAR build ID to use as the update source"); + private static readonly PositiveIdArgument IdArgument = new("The BAR build ID to use as the update source"); public static new void AddTo(Command command) { CreatePullRequestOptions.AddTo(command); - command.Arguments.Add(s_id); + command.Arguments.Add(IdArgument); } public static new FromBuildOptions Bind(ParseResult result) => - Bind(result, new FromBuildOptions { Id = result.GetValue(s_id) }); + Bind(result, new FromBuildOptions { Id = result.GetValue(IdArgument) }); } diff --git a/eng/update-dependencies/Commands/FromChannelCommand.cs b/eng/update-dependencies/Commands/FromChannelCommand.cs index 0a650e4bdd..8029c962bd 100644 --- a/eng/update-dependencies/Commands/FromChannelCommand.cs +++ b/eng/update-dependencies/Commands/FromChannelCommand.cs @@ -11,14 +11,14 @@ internal static class FromChannelCommand { public static Command CreateCliCommand(IServiceProvider services) where TUpdater : class, IUpdater { - var command = new Command("channel", "Update from the latest build in a BAR channel"); + Command command = new Command("channel", "Update from the latest build in a BAR channel"); FromChannelOptions.AddTo(command); command.SetAction(async (result, cancellationToken) => { FromChannelOptions options = FromChannelOptions.Bind(result); - var updater = (IBarChannelUpdater)services.GetRequiredService(); - var runner = services.GetRequiredService(); + IBarChannelUpdater updater = (IBarChannelUpdater)services.GetRequiredService(); + DependencyUpdateRunner runner = services.GetRequiredService(); DependencyUpdate update = await updater.ResolveFromBarChannelAsync(options.Channel, cancellationToken); await runner.RunAsync(options, TUpdater.VersionSourceName, update, cancellationToken); diff --git a/eng/update-dependencies/Commands/FromChannelOptions.cs b/eng/update-dependencies/Commands/FromChannelOptions.cs index 34b4f540cb..abf80799be 100644 --- a/eng/update-dependencies/Commands/FromChannelOptions.cs +++ b/eng/update-dependencies/Commands/FromChannelOptions.cs @@ -9,14 +9,14 @@ internal record FromChannelOptions : CreatePullRequestOptions { public required int Channel { get; init; } - private static readonly PositiveIdArgument s_channel = new("The BAR channel ID to use as the update source"); + private static readonly PositiveIdArgument ChannelArgument = new("The BAR channel ID to use as the update source"); public static new void AddTo(Command command) { CreatePullRequestOptions.AddTo(command); - command.Arguments.Add(s_channel); + command.Arguments.Add(ChannelArgument); } public static new FromChannelOptions Bind(ParseResult result) => - Bind(result, new FromChannelOptions { Channel = result.GetValue(s_channel) }); + Bind(result, new FromChannelOptions { Channel = result.GetValue(ChannelArgument) }); } diff --git a/eng/update-dependencies/Commands/FromGitHubReleaseCommand.cs b/eng/update-dependencies/Commands/FromGitHubReleaseCommand.cs index ce6abbbbc8..d4e2af83dd 100644 --- a/eng/update-dependencies/Commands/FromGitHubReleaseCommand.cs +++ b/eng/update-dependencies/Commands/FromGitHubReleaseCommand.cs @@ -17,8 +17,8 @@ public static void Configure(Command command, IServiceProvider service command.SetAction(async (result, cancellationToken) => { CreatePullRequestOptions options = CreatePullRequestOptions.Bind(result); - var updater = (IGitHubReleaseUpdater)services.GetRequiredService(); - var runner = services.GetRequiredService(); + IGitHubReleaseUpdater updater = (IGitHubReleaseUpdater)services.GetRequiredService(); + DependencyUpdateRunner runner = services.GetRequiredService(); DependencyUpdate update = await updater.ResolveFromGitHubReleaseAsync(cancellationToken); await runner.RunAsync(options, TUpdater.VersionSourceName, update, cancellationToken); diff --git a/eng/update-dependencies/Commands/FromStagingPipelineCommand.cs b/eng/update-dependencies/Commands/FromStagingPipelineCommand.cs index 093a3fa345..f6874ffcb7 100644 --- a/eng/update-dependencies/Commands/FromStagingPipelineCommand.cs +++ b/eng/update-dependencies/Commands/FromStagingPipelineCommand.cs @@ -2,10 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.CommandLine; +using Microsoft.DotNet.Docker.Shared; using Microsoft.DotNet.Docker.UpdateDependencies.Git; +using Microsoft.DotNet.Docker.UpdateDependencies.Model.Release; using Microsoft.DotNet.Docker.UpdateDependencies.Sync; using Microsoft.DotNet.Docker.UpdateDependencies.Updaters; -using Microsoft.DotNet.Docker.Shared; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -62,7 +63,7 @@ public FromStagingPipelineCommand( public static Command CreateCliCommand(IServiceProvider services) { - var command = new Command("staging-pipeline", "Update .NET from staging pipeline runs"); + Command command = new Command("staging-pipeline", "Update .NET from staging pipeline runs"); FromStagingPipelineOptions.AddTo(command); command.SetAction((result, _) => { @@ -74,7 +75,7 @@ public static Command CreateCliCommand(IServiceProvider services) public async Task ExecuteAsync(FromStagingPipelineOptions options) { - var stageContainers = options.GetStageContainerList(); + IReadOnlyList stageContainers = options.GetStageContainerList(); if (stageContainers.Count == 0) { @@ -91,17 +92,17 @@ public async Task ExecuteAsync(FromStagingPipelineOptions options) // passed in, we may or may not want to actually perform git operations. GitRepoContext // decides what git operations to perform and tells us where to make changes. This keeps // all the git-related logic in one place. - var gitRepoContext = await _createGitRepoContextAsync(options); + GitRepoContext gitRepoContext = await _createGitRepoContextAsync(options); List commitMessages = []; List prBodySections = []; // Process each stage container, creating a separate commit for each - foreach (var stageContainer in stageContainers) + foreach (string stageContainer in stageContainers) { _logger.LogInformation("Processing stage container: {StageContainer}", stageContainer); - var (commitMessage, prBodySection) = await ProcessStageContainerAsync( + (string? commitMessage, string? prBodySection) = await ProcessStageContainerAsync( options, stageContainer, gitRepoContext); @@ -114,11 +115,11 @@ public async Task ExecuteAsync(FromStagingPipelineOptions options) } // Create pull request with all commits - var prTitle = stageContainers.Count == 1 + string prTitle = stageContainers.Count == 1 ? $"[{options.TargetBranch}] {commitMessages[0]}" : $"[{options.TargetBranch}] Update .NET dependencies from {stageContainers.Count} stage containers"; - var prBody = string.Join(Environment.NewLine + Environment.NewLine, prBodySections); + string prBody = string.Join(Environment.NewLine + Environment.NewLine, prBodySections); await gitRepoContext.PushAndCreatePullRequest(prTitle, prBody); return 0; @@ -135,10 +136,10 @@ public async Task ExecuteAsync(FromStagingPipelineOptions options) string stageContainer, GitRepoContext gitRepoContext) { - var stagingPipelineRunId = StagingPipelineOptionsExtensions.GetStagingPipelineRunId(stageContainer); + int stagingPipelineRunId = StagingPipelineOptionsExtensions.GetStagingPipelineRunId(stageContainer); // Log staging pipeline tags for diagnostic purposes - var stagingPipelineTags = await _pipelinesService.GetBuildTagsAsync( + IReadOnlyList stagingPipelineTags = await _pipelinesService.GetBuildTagsAsync( _configuration.AzureDevOps.Organization, _configuration.AzureDevOps.Project, stagingPipelineRunId); @@ -160,7 +161,7 @@ public async Task ExecuteAsync(FromStagingPipelineOptions options) + $"/{stageContainer}/assets/shipping/assets"; } - var releaseConfig = await _pipelineArtifactProvider.GetReleaseConfigAsync( + ReleaseConfig releaseConfig = await _pipelineArtifactProvider.GetReleaseConfigAsync( _configuration.AzureDevOps.Organization, _configuration.AzureDevOps.Project, stagingPipelineRunId); @@ -183,7 +184,7 @@ public async Task ExecuteAsync(FromStagingPipelineOptions options) string? aspnetVersion = options.Internal ? releaseConfig.AspBuild : releaseConfig.Asp; // Example build URL: https://dev.azure.com///_build/results?buildId= - var buildUrl = $"{_configuration.AzureDevOps.Organization.TrimEnd('/')}/{_configuration.AzureDevOps.Project}/_build/results?buildId={stagingPipelineRunId}"; + string buildUrl = $"{_configuration.AzureDevOps.Organization.TrimEnd('/')}/{_configuration.AzureDevOps.Project}/_build/results?buildId={stagingPipelineRunId}"; _logger.LogInformation( "Applying internal build {StageContainer} ({BuildUrl})", stageContainer, buildUrl); @@ -194,7 +195,7 @@ await DependencyUpdateRunner.ApplyAsync( variables, repoRoot, releaseConfig, internalBaseUrl, token), CancellationToken.None); - var commitMessage = releaseConfig switch + string commitMessage = releaseConfig switch { { SdkOnly: true } => $"Update .NET {majorMinorVersionString} SDK to {sdkVersion}", _ => $"Update .NET {majorMinorVersionString} to {sdkVersion} SDK / {runtimeVersion} Runtime", @@ -208,8 +209,9 @@ await DependencyUpdateRunner.ApplyAsync( newVersionsList.Add($"- ASPNET: {aspnetVersion}"); newVersionsList.Add($"- ASPNET-COMPOSITE: {aspnetVersion}"); } + newVersionsList.Add($"- SDK: {sdkVersion}"); - var prBodySection = $""" + string prBodySection = $""" ## .NET {majorMinorVersionString} This updates .NET {majorMinorVersionString} to the following versions: @@ -280,21 +282,22 @@ public static async Task CreateAsync( if (options.SubmitPullRequest) { - var remoteUrl = configuration.AzureDevOps.GetRepoUrl(); - var targetBranch = options.TargetBranch; - var buildId = environmentService.GetBuildId() ?? ""; - var stageContainerList = options.GetStageContainerList(); + string remoteUrl = configuration.AzureDevOps.GetRepoUrl(); + string targetBranch = options.TargetBranch; + string buildId = environmentService.GetBuildId() ?? ""; + IReadOnlyList stageContainerList = options.GetStageContainerList(); if (stageContainerList.Count == 0) { throw new ArgumentException("At least one stage container must be provided."); } - var prBranch = options.CreatePullRequestBranchName( + + string prBranch = options.CreatePullRequestBranchName( $"update-deps-int-{stageContainerList[0]}", buildId); - var committer = configuration.GetCommitterIdentity(); + (string Name, string Email) committer = configuration.GetCommitterIdentity(); // Clone the repo and configure git identity for commits - var git = await gitRepoFactory.CreateAndCloneAsync(remoteUrl, gitIdentity: committer); + IGitRepoHelper git = await gitRepoFactory.CreateAndCloneAsync(remoteUrl, gitIdentity: committer); // Ensure the branch we want to modify exists, then check it out await git.Remote.EnsureBranchExistsAsync(targetBranch); // Create a new branch to push changes to and create a PR from @@ -325,17 +328,19 @@ await git.Remote.CreatePullRequestAsync(new( logger.LogInformation("No git operations will be performed."); localRepoPath = options.RepoRoot; - commitChanges = async (commitMessage) => + commitChanges = (commitMessage) => { logger.LogInformation("Skipping commit."); logger.LogInformation("Commit message: {CommitMessage}", commitMessage); + return Task.CompletedTask; }; - pushAndCreatePullRequest = async (prTitle, prBody) => + pushAndCreatePullRequest = (prTitle, prBody) => { logger.LogInformation("Skipping push and pull request creation."); logger.LogInformation("Pull request title: {PullRequestTitle}", prTitle); logger.LogInformation("Pull request body:\n{PullRequestBody}", prBody); + return Task.CompletedTask; }; } diff --git a/eng/update-dependencies/Commands/FromStagingPipelineOptions.cs b/eng/update-dependencies/Commands/FromStagingPipelineOptions.cs index 28f0b3099c..5e73cd5467 100644 --- a/eng/update-dependencies/Commands/FromStagingPipelineOptions.cs +++ b/eng/update-dependencies/Commands/FromStagingPipelineOptions.cs @@ -28,7 +28,7 @@ internal partial record FromStagingPipelineOptions : CreatePullRequestOptions /// public string? StagingStorageAccount { get; init; } - private static readonly Argument s_stageContainers = new("stage-containers") + private static readonly Argument StageContainersArgument = new("stage-containers") { Description = "A comma-delimited list of stage container names to use as a source for the update (e.g., 'stage-1234567,stage-2345678')", }; @@ -39,7 +39,7 @@ internal partial record FromStagingPipelineOptions : CreatePullRequestOptions + " For example: https://dotnetstagetest.blob.core.windows.net/", DefaultValueFactory = _ => string.Empty, }; - private static readonly Option s_internal = new(InternalOption) + private static readonly Option UseInternalOption = new(InternalOption) { Description = "Whether or not to use the internal versions of the staged build. When not using an internal" + " build, Dockerfiles will be updated as if the staged build has already been released. When using an" @@ -50,17 +50,17 @@ internal partial record FromStagingPipelineOptions : CreatePullRequestOptions public static new void AddTo(Command command) { CreatePullRequestOptions.AddTo(command); - command.Arguments.Add(s_stageContainers); + command.Arguments.Add(StageContainersArgument); command.Options.Add(StagingStorageAccountOption); - command.Options.Add(s_internal); + command.Options.Add(UseInternalOption); } public static new FromStagingPipelineOptions Bind(ParseResult result) => Bind(result, new FromStagingPipelineOptions { - StageContainers = result.GetRequiredValue(s_stageContainers), + StageContainers = result.GetRequiredValue(StageContainersArgument), StagingStorageAccount = result.GetRequiredValue(StagingStorageAccountOption), - Internal = result.GetValue(s_internal), + Internal = result.GetValue(UseInternalOption), }); /// @@ -87,12 +87,13 @@ internal static partial class StagingPipelineOptionsExtensions /// public static int GetStagingPipelineRunId(string stageContainer) { - var match = StageContainerRegex.Match(stageContainer); + Match match = StageContainerRegex.Match(stageContainer); if (!match.Success) { throw new ArgumentException( $"Invalid stage container name '{stageContainer}'. Expected format: 'stage-{{buildId}}' (e.g., 'stage-1234567')"); } + return int.Parse(match.Groups[1].Value); } } diff --git a/eng/update-dependencies/Commands/MonitorCommand.cs b/eng/update-dependencies/Commands/MonitorCommand.cs index 14d9a1a0cb..b831602c3d 100644 --- a/eng/update-dependencies/Commands/MonitorCommand.cs +++ b/eng/update-dependencies/Commands/MonitorCommand.cs @@ -20,9 +20,9 @@ public static Command CreateCliCommand(IServiceProvider services) => private static Command CreatePipelineBuildCommand(IServiceProvider services) { - var runId = new PositiveIdArgument("The Azure DevOps pipeline run ID to use as the update source"); + PositiveIdArgument runId = new PositiveIdArgument("The Azure DevOps pipeline run ID to use as the update source"); - var command = new Command("pipeline-build", "Update from an Azure DevOps pipeline run") + Command command = new Command("pipeline-build", "Update from an Azure DevOps pipeline run") { runId }; @@ -32,8 +32,8 @@ private static Command CreatePipelineBuildCommand(IServiceProvider services) command.SetAction(async (parseResult, cancellationToken) => { CreatePullRequestOptions options = CreatePullRequestOptions.Bind(parseResult); - var updater = services.GetRequiredService(); - var runner = services.GetRequiredService(); + MonitorUpdater updater = services.GetRequiredService(); + DependencyUpdateRunner runner = services.GetRequiredService(); DependencyUpdate update = await updater.ResolveFromPipelineBuildAsync( parseResult.GetValue(runId), @@ -47,12 +47,12 @@ private static Command CreatePipelineBuildCommand(IServiceProvider services) private static Command CreateVersionCommand(IServiceProvider services) { - var version = new Argument("version") + Argument version = new Argument("version") { Description = "The product version to use as the update source", }; - var command = new Command("version", "Update to a specific version") + Command command = new Command("version", "Update to a specific version") { version }; @@ -62,8 +62,8 @@ private static Command CreateVersionCommand(IServiceProvider services) command.SetAction(async (parseResult, cancellationToken) => { CreatePullRequestOptions options = CreatePullRequestOptions.Bind(parseResult); - var updater = services.GetRequiredService(); - var runner = services.GetRequiredService(); + MonitorUpdater updater = services.GetRequiredService(); + DependencyUpdateRunner runner = services.GetRequiredService(); DependencyUpdate update = updater.ResolveFromVersion(parseResult.GetRequiredValue(version)); diff --git a/eng/update-dependencies/Commands/PowerShellCommand.cs b/eng/update-dependencies/Commands/PowerShellCommand.cs index 4a53d672a4..6a4c79764a 100644 --- a/eng/update-dependencies/Commands/PowerShellCommand.cs +++ b/eng/update-dependencies/Commands/PowerShellCommand.cs @@ -11,12 +11,12 @@ internal static class PowerShellCommand { private const string VersionSourceName = "PowerShell/PowerShell"; - private static readonly Option s_internal = new("--internal") + private static readonly Option Internal = new("--internal") { Description = "Use the internal (non-public) location for the version", }; - private static readonly Option s_dotnetVersion = new("--dotnet-version") + private static readonly Option DotnetVersion = new("--dotnet-version") { Description = "Only update images for this .NET major.minor version (for example, 9.0)", }; @@ -30,22 +30,22 @@ public static Command CreateCliCommand(IServiceProvider services) => private static Command CreateVersionCommand(IServiceProvider services) { - var version = new Argument("version") + Argument version = new Argument("version") { Description = "The PowerShell version to update to", }; - var baseUrl = new Option("--base-url") + Option baseUrl = new Option("--base-url") { Description = "Use this exact location for the version instead of the default public or internal one", }; - var command = new Command("version", "Update to a specific version") + Command command = new Command("version", "Update to a specific version") { version, baseUrl, - s_internal, - s_dotnetVersion, + Internal, + DotnetVersion, }; CreatePullRequestOptions.AddTo(command); @@ -53,14 +53,14 @@ private static Command CreateVersionCommand(IServiceProvider services) command.SetAction(async (parseResult, cancellationToken) => { CreatePullRequestOptions options = CreatePullRequestOptions.Bind(parseResult); - var updater = services.GetRequiredService(); - var runner = services.GetRequiredService(); + PowerShellUpdater updater = services.GetRequiredService(); + DependencyUpdateRunner runner = services.GetRequiredService(); DependencyUpdate update = updater.ResolveFromVersion( parseResult.GetRequiredValue(version), - parseResult.GetValue(s_internal), + parseResult.GetValue(Internal), parseResult.GetValue(baseUrl), - parseResult.GetValue(s_dotnetVersion)); + parseResult.GetValue(DotnetVersion)); await runner.RunAsync(options, VersionSourceName, update, cancellationToken); }); @@ -70,10 +70,10 @@ private static Command CreateVersionCommand(IServiceProvider services) private static Command CreateLatestCommand(IServiceProvider services) { - var command = new Command("latest", "Update to the latest available version") + Command command = new Command("latest", "Update to the latest available version") { - s_internal, - s_dotnetVersion, + Internal, + DotnetVersion, }; CreatePullRequestOptions.AddTo(command); @@ -81,15 +81,15 @@ private static Command CreateLatestCommand(IServiceProvider services) command.SetAction(async (parseResult, cancellationToken) => { CreatePullRequestOptions options = CreatePullRequestOptions.Bind(parseResult); - var updater = services.GetRequiredService(); - var runner = services.GetRequiredService(); + PowerShellUpdater updater = services.GetRequiredService(); + DependencyUpdateRunner runner = services.GetRequiredService(); ManifestVariables variables = await runner.GetTargetManifestAsync(options, cancellationToken); IReadOnlyList updates = await updater.ResolveLatestAsync( variables, - parseResult.GetValue(s_internal), - parseResult.GetValue(s_dotnetVersion), + parseResult.GetValue(Internal), + parseResult.GetValue(DotnetVersion), cancellationToken); foreach (DependencyUpdate update in updates) diff --git a/eng/update-dependencies/Commands/SyncInternalReleaseCommand.cs b/eng/update-dependencies/Commands/SyncInternalReleaseCommand.cs index f761d98170..312c682c7a 100644 --- a/eng/update-dependencies/Commands/SyncInternalReleaseCommand.cs +++ b/eng/update-dependencies/Commands/SyncInternalReleaseCommand.cs @@ -36,7 +36,7 @@ ILogger logger public static Command CreateCliCommand(IServiceProvider services) { - var command = new Command("sync-internal-release", "Sync release/* branch to internal/release/* branch"); + Command command = new Command("sync-internal-release", "Sync release/* branch to internal/release/* branch"); SyncInternalReleaseOptions.AddTo(command); command.SetAction((result, _) => { @@ -48,7 +48,7 @@ public static Command CreateCliCommand(IServiceProvider services) public async Task ExecuteAsync(SyncInternalReleaseOptions options) { - var remoteUrl = configuration.AzureDevOps.GetRepoUrl(); + string remoteUrl = configuration.AzureDevOps.GetRepoUrl(); // Do not allow syncing starting from an internal branch. if (options.SourceBranch.StartsWith("internal/", StringComparison.OrdinalIgnoreCase)) @@ -58,16 +58,16 @@ public async Task ExecuteAsync(SyncInternalReleaseOptions options) } // Get git identity if available (required for commits, optional for read-only operations) - var gitIdentity = !string.IsNullOrWhiteSpace(configuration.User) && !string.IsNullOrWhiteSpace(configuration.Email) + (string, string)? gitIdentity = !string.IsNullOrWhiteSpace(configuration.User) && !string.IsNullOrWhiteSpace(configuration.Email) ? configuration.GetCommitterIdentity() - : ((string, string)?)null; + : null; - using var repo = await _gitRepoHelperFactory.CreateAndCloneAsync( + using IGitRepoHelper repo = await _gitRepoHelperFactory.CreateAndCloneAsync( remoteUrl, gitIdentity: gitIdentity); // Verify that the source branch exists on the remote. - var sourceBranchExists = await repo.Remote.RemoteBranchExistsAsync(options.SourceBranch); + bool sourceBranchExists = await repo.Remote.RemoteBranchExistsAsync(options.SourceBranch); if (!sourceBranchExists) { throw new InvalidOperationException( @@ -75,7 +75,7 @@ public async Task ExecuteAsync(SyncInternalReleaseOptions options) } // If the target branch doesn't exist, then create it based on the source branch. - var targetBranchExists = await repo.Remote.RemoteBranchExistsAsync(options.TargetBranch); + bool targetBranchExists = await repo.Remote.RemoteBranchExistsAsync(options.TargetBranch); if (!targetBranchExists) { await repo.Remote.CreateRemoteBranchAsync( @@ -84,9 +84,9 @@ await repo.Remote.CreateRemoteBranchAsync( return 0; } - var sourceSha = await repo.Remote.GetRemoteBranchShaAsync(options.SourceBranch) + string sourceSha = await repo.Remote.GetRemoteBranchShaAsync(options.SourceBranch) ?? throw new Exception("Failed to get source branch SHA even though branch exists on remote."); - var targetSha = await repo.Remote.GetRemoteBranchShaAsync(options.TargetBranch) + string targetSha = await repo.Remote.GetRemoteBranchShaAsync(options.TargetBranch) ?? throw new Exception("Failed to get target branch SHA even though branch exists on remote."); // If both branches are at the same commit, then there is nothing to do. @@ -105,11 +105,11 @@ await repo.Remote.CreateRemoteBranchAsync( // We haven't pulled either branch into our working tree yet, so we // reference them with the usual "refs/heads/..." prefix. Assume that // both branches exist on the "origin" remote. - var targetIsAncestorOfSource = await repo.Local.IsAncestorAsync( + bool targetIsAncestorOfSource = await repo.Local.IsAncestorAsync( ancestorRef: $"origin/{options.TargetBranch}", descendantRef: $"origin/{options.SourceBranch}"); - var buildId = _environmentService.GetBuildId() ?? ""; + string buildId = _environmentService.GetBuildId() ?? ""; if (targetIsAncestorOfSource) { @@ -118,7 +118,7 @@ await repo.Remote.CreateRemoteBranchAsync( options.TargetBranch, options.SourceBranch); // "ff" here is an abbreviation for "fast-forward" - just want to keep branch names short - var fastForwardPrBranch = options.CreatePullRequestBranchName( + string fastForwardPrBranch = options.CreatePullRequestBranchName( "ff", buildId); await repo.Remote.CreateRemoteBranchAsync( @@ -153,16 +153,16 @@ await repo.Remote.CreatePullRequestAsync(new( // At this point, we need to verify that we have everything we need to // access internal builds, commit changes, and submit a pull request. ArgumentException.ThrowIfNullOrWhiteSpace(options.StagingStorageAccount); - var commitAuthor = configuration.GetCommitterIdentity(); + (string Name, string Email) commitAuthor = configuration.GetCommitterIdentity(); // Checkout both branches locally so that we can work with them directly. await repo.CheckoutRemoteBranchAsync(options.SourceBranch); await repo.CheckoutRemoteBranchAsync(options.TargetBranch); - var internalBuilds = _internalVersionsService.GetInternalStagingBuilds(repo.Local.LocalPath); + InternalStageContainers internalBuilds = _internalVersionsService.GetInternalStagingBuilds(repo.Local.LocalPath); // Reset the target branch to match the source branch. - var prBranchName = options.CreatePullRequestBranchName( + string prBranchName = options.CreatePullRequestBranchName( name: "sync", buildId: buildId); await repo.Local.CreateAndCheckoutLocalBranchAsync(prBranchName); @@ -173,10 +173,9 @@ await repo.Local.CommitAsync( author: commitAuthor); // Re-apply internal .NET version updates for each recorded stage container. - foreach (var (dockerfileVersion, stageContainer) in internalBuilds.Versions) + foreach ((Shared.DotNetVersion? dockerfileVersion, string? stageContainer) in internalBuilds.Versions) { await ApplyInternalBuildAsync( - options: options, localRepo: repo.Local, stageContainer: stageContainer, stagingStorageAccount: options.StagingStorageAccount, @@ -198,14 +197,15 @@ await repo.Remote.CreatePullRequestAsync(new( /// /// Apply an internal build by invoking the FromStagingPipelineCommand. /// + /// The local repository to update. /// /// Stage container name (e.g., "stage-1234567") to get build information from. /// + /// The storage account containing the staged build. /// /// The identity to use when committing changes. /// private async Task ApplyInternalBuildAsync( - SyncInternalReleaseOptions options, ILocalGitRepoHelper localRepo, string stageContainer, string stagingStorageAccount, @@ -215,7 +215,7 @@ private async Task ApplyInternalBuildAsync( "Ignore any git-related logging output below, because git " + "operations are being managed by a different command."); - var fromStagingPipelineOptions = new FromStagingPipelineOptions + FromStagingPipelineOptions fromStagingPipelineOptions = new FromStagingPipelineOptions { RepoRoot = localRepo.LocalPath, Internal = true, @@ -223,7 +223,7 @@ private async Task ApplyInternalBuildAsync( StagingStorageAccount = stagingStorageAccount, }; - var exitCode = await _updateFromStagingPipeline.ExecuteAsync(fromStagingPipelineOptions); + int exitCode = await _updateFromStagingPipeline.ExecuteAsync(fromStagingPipelineOptions); if (exitCode != 0) { throw new InvalidOperationException( diff --git a/eng/update-dependencies/DependencyUpdateRunner.cs b/eng/update-dependencies/DependencyUpdateRunner.cs index e8a72f9c97..059e53789f 100644 --- a/eng/update-dependencies/DependencyUpdateRunner.cs +++ b/eng/update-dependencies/DependencyUpdateRunner.cs @@ -1,5 +1,5 @@ -// 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. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; using Azure.Identity; @@ -42,14 +42,14 @@ public async Task RunAsync( branchSuffix = branchSuffix.Replace('/', '-'); string branchName = $"{options.TargetBranch}-{branchSuffix}"; - var definition = new PullRequestDefinition( + PullRequestDefinition definition = new PullRequestDefinition( Key: branchName, Title: title, Body: string.Empty, TargetBranch: options.TargetBranch, ApplyChanges: (git, token) => ApplyAsync(git.WorkspaceDirectory, update.ApplyAsync, token)); - var manager = new PullRequestManager(loggerFactory); + PullRequestManager manager = new PullRequestManager(loggerFactory); PullRequestResult result = await manager.CreateOrUpdateAsync( definition, CreatePullRequestEndpoint(configuration), @@ -72,9 +72,9 @@ public async Task GetTargetManifestAsync( // This clone is only read. PullRequestManager makes its own clone when it applies updates. IPullRequestEndpoint endpoint = CreatePullRequestEndpoint(configuration); - var processRunner = new ProcessRunner(loggerFactory.CreateLogger()); + ProcessRunner processRunner = new ProcessRunner(loggerFactory.CreateLogger()); - using var workingCopy = new GitWorkingCopy(processRunner, loggerFactory.CreateLogger()); + using GitWorkingCopy workingCopy = new GitWorkingCopy(processRunner, loggerFactory.CreateLogger()); AutomationIdentity identity = await endpoint.GetIdentityAsync(cancellationToken); await endpoint.CloneTargetAsync(workingCopy, options.TargetBranch, identity, cancellationToken); @@ -89,7 +89,7 @@ public static async Task ApplyAsync( { repoRoot = Path.GetFullPath(repoRoot); string manifestPath = GetManifestPath(repoRoot); - var variables = ManifestVariables.FromFile(manifestPath); + ManifestVariables variables = ManifestVariables.FromFile(manifestPath); string originalContent = variables.Content; await applyUpdate(variables, repoRoot, cancellationToken); @@ -117,16 +117,16 @@ private static IPullRequestEndpoint CreatePullRequestEndpoint( private static GitHubPullRequestEndpoint CreateGitHubPullRequestEndpoint( UpdateDependenciesConfiguration configuration) { - var github = configuration.GitHub; - var identity = new AutomationIdentity(configuration.User, configuration.Email); - var repo = new GitHubRepo(github.Owner, github.Repository); + GitHubConfiguration github = configuration.GitHub; + AutomationIdentity identity = new AutomationIdentity(configuration.User, configuration.Email); + GitHubRepo repo = new GitHubRepo(github.Owner, github.Repository); IGitHubAccessProvider accessProvider; if (!string.IsNullOrEmpty(github.AppClientId) && !string.IsNullOrEmpty(github.AppKeyUri)) { - var credential = new AzureCliCredential(); - var cryptographyClient = new CryptographyClient(new Uri(github.AppKeyUri), credential); + AzureCliCredential credential = new AzureCliCredential(); + CryptographyClient cryptographyClient = new CryptographyClient(new Uri(github.AppKeyUri), credential); accessProvider = new GitHubAppAccessProvider(github.AppClientId, cryptographyClient); } else @@ -140,13 +140,13 @@ private static GitHubPullRequestEndpoint CreateGitHubPullRequestEndpoint( private static AzureDevOpsPullRequestEndpoint CreateAzureDevOpsPullRequestEndpoint( UpdateDependenciesConfiguration configuration) { - var azureDevOps = configuration.AzureDevOps; + AzureDevOpsConfiguration azureDevOps = configuration.AzureDevOps; string organization = new Uri(azureDevOps.Organization).Segments.Last().TrimEnd('/'); - var client = new AzureDevOpsClient( + AzureDevOpsClient client = new AzureDevOpsClient( organization, azureDevOps.Project, azureDevOps.Token); - var identity = new AutomationIdentity(configuration.User, configuration.Email); + AutomationIdentity identity = new AutomationIdentity(configuration.User, configuration.Email); return new AzureDevOpsPullRequestEndpoint( client, diff --git a/eng/update-dependencies/DirectoryStack.cs b/eng/update-dependencies/DirectoryStack.cs index 71f614caa6..2caeeea07f 100644 --- a/eng/update-dependencies/DirectoryStack.cs +++ b/eng/update-dependencies/DirectoryStack.cs @@ -1,5 +1,5 @@ -// 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. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. namespace Microsoft.DotNet.Docker.UpdateDependencies; @@ -25,7 +25,7 @@ internal static class DirectoryStack /// private sealed class DirectoryContext : IDisposable { - private static readonly Stack s_directoryStack = new(); + private static readonly Stack DirectoryStack = new(); private bool _disposed; @@ -51,7 +51,7 @@ public void Dispose() /// private static void Push(string path) { - s_directoryStack.Push(Directory.GetCurrentDirectory()); + DirectoryStack.Push(Directory.GetCurrentDirectory()); Directory.SetCurrentDirectory(path); } @@ -62,9 +62,9 @@ private static void Push(string path) /// private static void Pop() { - if (s_directoryStack.Count > 0) + if (DirectoryStack.Count > 0) { - string previousDirectory = s_directoryStack.Pop(); + string previousDirectory = DirectoryStack.Pop(); Directory.SetCurrentDirectory(previousDirectory); } else diff --git a/eng/update-dependencies/Git/GitRepoHelper.cs b/eng/update-dependencies/Git/GitRepoHelper.cs index 796d3d5ce9..f3275f7714 100644 --- a/eng/update-dependencies/Git/GitRepoHelper.cs +++ b/eng/update-dependencies/Git/GitRepoHelper.cs @@ -38,14 +38,14 @@ ILogger logger /// public async Task CheckoutRemoteBranchAsync(string branchName) { - var currentBranch = await Local.GetCurrentBranchAsync(); + string currentBranch = await Local.GetCurrentBranchAsync(); if (currentBranch == branchName) { _logger.LogInformation("Already on branch {BranchName}", branchName); return; } - var branchExists = await Remote.RemoteBranchExistsAsync(branchName); + bool branchExists = await Remote.RemoteBranchExistsAsync(branchName); if (!branchExists) { throw new InvalidBranchException($"Branch '{branchName}' does not exist on remote."); @@ -58,8 +58,8 @@ public async Task CheckoutRemoteBranchAsync(string branchName) public async Task PushLocalBranchAsync(string branchName) { // Get all remotes and find the one that matches the target repo. - var remotes = await Local.ListAllRemotesAsync(); - var targetRemote = remotes.First(remote => remote.Url == _repoUri); + IEnumerable remotes = await Local.ListAllRemotesAsync(); + GitRemoteInfo targetRemote = remotes.First(remote => remote.Url == _repoUri); _logger.LogInformation("Pushing branch {BranchName} to remote {RemoteUrl}", branchName, targetRemote.Url); await _libGit2Client.Push(Local.LocalPath, branchName, targetRemote.Url); diff --git a/eng/update-dependencies/Git/GitRepoHelperFactory.cs b/eng/update-dependencies/Git/GitRepoHelperFactory.cs index 9a4ce5bca8..430e6a3c2a 100644 --- a/eng/update-dependencies/Git/GitRepoHelperFactory.cs +++ b/eng/update-dependencies/Git/GitRepoHelperFactory.cs @@ -77,11 +77,11 @@ await _gitRepoCloner.CloneAsync( gitDirectory: null); _logger.LogInformation("Cloned '{RepoUri}' to '{LocalCloneDir}'", repoUri, localCloneDir); - var gitRepoHelper = CreateFromLocal(repoUri, localCloneDir); + GitRepoHelper gitRepoHelper = CreateFromLocal(repoUri, localCloneDir); if (gitIdentity is { } identity) { - var localGitRepo = _localGitRepoFactory.Create(new NativePath(localCloneDir)); + ILocalGitRepo localGitRepo = _localGitRepoFactory.Create(new NativePath(localCloneDir)); await localGitRepo.SetConfigValue("user.name", identity.Name); await localGitRepo.SetConfigValue("user.email", identity.Email); _logger.LogInformation( @@ -97,13 +97,13 @@ public GitRepoHelper CreateFromLocal(string repoUri, string? localPath = null) { localPath ??= Directory.GetCurrentDirectory(); - var localGitRepo = _localGitRepoFactory.Create(new NativePath(localPath)); - var localGitRepoHelper = new LocalGitRepoHelper( + ILocalGitRepo localGitRepo = _localGitRepoFactory.Create(new NativePath(localPath)); + LocalGitRepoHelper localGitRepoHelper = new LocalGitRepoHelper( localPath: localPath, localGitRepo: localGitRepo, logger: _serviceProvider.GetRequiredService>()); - var remoteGitRepoHelper = new RemoteGitRepoHelper( + RemoteGitRepoHelper remoteGitRepoHelper = new RemoteGitRepoHelper( repoUri, _remoteFactory.CreateRemoteGitRepo(repoUri), localGitRepo: localGitRepo, diff --git a/eng/update-dependencies/Git/GitValidationExtensions.cs b/eng/update-dependencies/Git/GitValidationExtensions.cs index 82b823b7e3..6ba010d0e7 100644 --- a/eng/update-dependencies/Git/GitValidationExtensions.cs +++ b/eng/update-dependencies/Git/GitValidationExtensions.cs @@ -7,7 +7,7 @@ internal static class GitValidationExtensions { public static async Task EnsureBranchExistsAsync(this IRemoteGitRepoHelper remote, string branchName) { - var branch = await remote.RemoteBranchExistsAsync(branchName); + bool branch = await remote.RemoteBranchExistsAsync(branchName); if (!branch) { throw new InvalidOperationException( diff --git a/eng/update-dependencies/Git/IRemoteGitRepoHelper.cs b/eng/update-dependencies/Git/IRemoteGitRepoHelper.cs index cedeca2cac..eaf75cfd0f 100644 --- a/eng/update-dependencies/Git/IRemoteGitRepoHelper.cs +++ b/eng/update-dependencies/Git/IRemoteGitRepoHelper.cs @@ -18,8 +18,8 @@ internal interface IRemoteGitRepoHelper Task RemoteBranchExistsAsync(string branchName); /// - /// Create a new branch in the remote repository. The - /// will be based off of the . + /// Create a new branch in the remote repository. The + /// will be based off of the . /// /// /// This branch will be created on the remote repository. diff --git a/eng/update-dependencies/Git/LocalGitRepoHelper.cs b/eng/update-dependencies/Git/LocalGitRepoHelper.cs index ca8ed6a674..0146457921 100644 --- a/eng/update-dependencies/Git/LocalGitRepoHelper.cs +++ b/eng/update-dependencies/Git/LocalGitRepoHelper.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.DotNet.DarcLib; +using Microsoft.DotNet.DarcLib.Helpers; using Microsoft.Extensions.Logging; namespace Microsoft.DotNet.Docker.UpdateDependencies.Git; @@ -40,7 +41,7 @@ public Task CheckoutRefAsync(string gitRef) { try { - var result = await _localGitRepo.GetShaForRefAsync(gitRef); + string result = await _localGitRepo.GetShaForRefAsync(gitRef); return result; } catch @@ -50,16 +51,13 @@ public Task CheckoutRefAsync(string gitRef) } /// - public async Task StageAsync(params IEnumerable paths) - { - await _localGitRepo.StageAsync(paths); - } + public async Task StageAsync(params IEnumerable paths) => await _localGitRepo.StageAsync(paths); /// public async Task CommitAsync(string message, (string Name, string Email) author) { - var gitStatusLines = await _localGitRepo.RunGitCommandAsync(["status"]); - var gitStatus = string.Join(Environment.NewLine, gitStatusLines); + ProcessExecutionResult gitStatusLines = await _localGitRepo.RunGitCommandAsync(["status"]); + string gitStatus = string.Join(Environment.NewLine, gitStatusLines); _logger.LogInformation( """ Git status: @@ -68,7 +66,7 @@ public async Task CommitAsync(string message, (string Name, string Email gitStatus); await _localGitRepo.CommitAsync(message, allowEmpty: false, author); - var commitSha = await _localGitRepo.GetShaForRefAsync("HEAD"); + string commitSha = await _localGitRepo.GetShaForRefAsync("HEAD"); _logger.LogInformation( "Created commit {commitSha}, message: '{commitMessage}' ({authorName} <{authorEmail}>)", @@ -80,22 +78,22 @@ public async Task CommitAsync(string message, (string Name, string Email /// public async Task IsAncestorAsync(string ancestorRef, string descendantRef) { - var ancestorCommit = await GetShaForRefAsync(ancestorRef); - var descendantCommit = await GetShaForRefAsync(descendantRef); + string? ancestorCommit = await GetShaForRefAsync(ancestorRef); + string? descendantCommit = await GetShaForRefAsync(descendantRef); if (ancestorCommit is null || descendantCommit is null) { return false; } - var isAncestor = await _localGitRepo.IsAncestorCommit(ancestorCommit, descendantCommit); + bool isAncestor = await _localGitRepo.IsAncestorCommit(ancestorCommit, descendantCommit); return isAncestor; } /// public async Task> ListAllRemotesAsync() { - var remotesOutput = await _localGitRepo.ExecuteGitCommand("remote", "-v"); + ProcessExecutionResult remotesOutput = await _localGitRepo.ExecuteGitCommand("remote", "-v"); /* Example output of `git remote -v`: origin https://github.com/dotnet/runtime (fetch) diff --git a/eng/update-dependencies/Git/RemoteGitRepoFactory.cs b/eng/update-dependencies/Git/RemoteGitRepoFactory.cs index ef5d22b26d..0716d4dfbb 100644 --- a/eng/update-dependencies/Git/RemoteGitRepoFactory.cs +++ b/eng/update-dependencies/Git/RemoteGitRepoFactory.cs @@ -27,7 +27,7 @@ internal sealed class RemoteGitRepoFactory(IServiceProvider serviceProvider) : I /// public IRemoteGitRepo CreateRemoteGitRepo(string remoteUrl) { - var gitRemoteKind = ParseGitRemoteFromUrl(remoteUrl); + GitRemote gitRemoteKind = ParseGitRemoteFromUrl(remoteUrl); return gitRemoteKind switch { diff --git a/eng/update-dependencies/Git/RemoteGitRepoHelper.cs b/eng/update-dependencies/Git/RemoteGitRepoHelper.cs index 3491b5f183..8a5d42d16f 100644 --- a/eng/update-dependencies/Git/RemoteGitRepoHelper.cs +++ b/eng/update-dependencies/Git/RemoteGitRepoHelper.cs @@ -36,7 +36,7 @@ public Task CreateRemoteBranchAsync(string newBranch, string baseBranch) /// public async Task CreatePullRequestAsync(PullRequestCreationInfo request) { - var darcPullRequest = new PullRequest + PullRequest darcPullRequest = new PullRequest { Title = request.Title, Description = request.Body, @@ -48,8 +48,8 @@ public async Task CreatePullRequestAsync(PullRequestCreationInfo request "Creating pull request '{PrTitle}' from branch {PrHeadBranch} to {PrBaseBranch}", darcPullRequest.Title, darcPullRequest.HeadBranch, darcPullRequest.BaseBranch); - var pullRequestInfo = await _remoteGitRepo.CreatePullRequestAsync(RemoteUrl, darcPullRequest); - var pullRequestApiUrl = pullRequestInfo.Url; + PullRequest pullRequestInfo = await _remoteGitRepo.CreatePullRequestAsync(RemoteUrl, darcPullRequest); + string pullRequestApiUrl = pullRequestInfo.Url; _logger.LogInformation( "Created pull request {PullRequestTitle} at {PullRequestUrl}", diff --git a/eng/update-dependencies/GitRemote.cs b/eng/update-dependencies/GitRemote.cs index eea41cd54e..e592acf5d2 100644 --- a/eng/update-dependencies/GitRemote.cs +++ b/eng/update-dependencies/GitRemote.cs @@ -1,5 +1,5 @@ -// 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. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. namespace Microsoft.DotNet.Docker.UpdateDependencies; diff --git a/eng/update-dependencies/ManifestHelper.cs b/eng/update-dependencies/ManifestHelper.cs index 1549294abf..56d841174a 100644 --- a/eng/update-dependencies/ManifestHelper.cs +++ b/eng/update-dependencies/ManifestHelper.cs @@ -21,26 +21,21 @@ public static partial class ManifestHelper /// /// Name of the branch. This is typically "main" or "nightly". /// + /// The version source used to identify the product. + /// Whether the release updates only the SDK. public static IEnumerable GetBaseUrlVariableNames( string dockerfileVersion, string branch, string versionSourceName = "", bool sdkOnlyRelease = false) { - string product; - if (sdkOnlyRelease) - { - product = "sdk"; - } - else - { - product = versionSourceName switch + string product = sdkOnlyRelease + ? "sdk" + : versionSourceName switch { string v when v.Contains("aspire-dashboard") => "aspire-dashboard", _ => "dotnet", }; - } - return [ $"{product}|{dockerfileVersion}|base-url|{branch}", $"{product}|{dockerfileVersion}|base-url|checksums|{branch}", diff --git a/eng/update-dependencies/ManifestVariables.cs b/eng/update-dependencies/ManifestVariables.cs index 3517be8315..a2fff828d4 100644 --- a/eng/update-dependencies/ManifestVariables.cs +++ b/eng/update-dependencies/ManifestVariables.cs @@ -22,8 +22,7 @@ public sealed partial class ManifestVariables { // Token ranges always refer to the original text. Keeping edits separately avoids shifting // later ranges and lets a reverted value recover its original JSON escaping. - private readonly string _originalContent; - private readonly Dictionary _variables = new(StringComparer.Ordinal); + private readonly Dictionary _variables = new Dictionary(StringComparer.Ordinal); /// /// Creates an editable snapshot of a complete manifest. @@ -39,15 +38,15 @@ public sealed partial class ManifestVariables /// public ManifestVariables(string content) { - _originalContent = content; + Content = content; byte[] utf8 = Encoding.UTF8.GetBytes(content); - var readerOptions = new JsonReaderOptions + JsonReaderOptions readerOptions = new JsonReaderOptions { CommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true, }; - var reader = new Utf8JsonReader(utf8, readerOptions); + Utf8JsonReader reader = new Utf8JsonReader(utf8, readerOptions); reader.Read(); RequireToken(reader, JsonTokenType.StartObject); @@ -111,7 +110,7 @@ public string Content { get { - var content = new StringBuilder(); + StringBuilder content = new StringBuilder(); int position = 0; foreach (Variable variable in _variables.Values.OrderBy(variable => variable.OriginalTokenStart)) @@ -121,12 +120,12 @@ public string Content continue; } - content.Append(_originalContent, position, variable.OriginalTokenStart - position); + content.Append(field, position, variable.OriginalTokenStart - position); content.Append(JsonSerializer.Serialize(variable.Value)); position = variable.OriginalTokenStart + variable.OriginalTokenLength; } - content.Append(_originalContent, position, _originalContent.Length - position); + content.Append(field, position, field.Length - position); return content.ToString(); } } @@ -276,7 +275,7 @@ private void ReadVariables(ref Utf8JsonReader reader, byte[] utf8) int gapCharacterCount = Encoding.UTF8.GetCharCount(utf8.AsSpan(bytePosition, gapLength)); int originalTokenStart = characterPosition + gapCharacterCount; int originalTokenLength = Encoding.UTF8.GetCharCount(utf8.AsSpan(byteStart, byteLength)); - var variable = new Variable(value, value, originalTokenStart, originalTokenLength); + Variable variable = new Variable(value, value, originalTokenStart, originalTokenLength); bytePosition = byteStart + byteLength; characterPosition = originalTokenStart + originalTokenLength; diff --git a/eng/update-dependencies/Model/Release/ReleaseConfig.cs b/eng/update-dependencies/Model/Release/ReleaseConfig.cs index 4a6e6ac4c6..bc3ad833d9 100644 --- a/eng/update-dependencies/Model/Release/ReleaseConfig.cs +++ b/eng/update-dependencies/Model/Release/ReleaseConfig.cs @@ -52,13 +52,13 @@ public record ReleaseConfig [MemberNotNullWhen(false, nameof(AspBuild))] public required bool SdkOnly { get; init; } - private static readonly JsonSerializerOptions s_jsonOptions = new() + private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; public override string ToString() => - JsonSerializer.Serialize(this, s_jsonOptions); + JsonSerializer.Serialize(this, JsonOptions); public static ReleaseConfig FromJson(string json) => JsonSerializer.Deserialize(json) diff --git a/eng/update-dependencies/PipelineArtifactProvider.cs b/eng/update-dependencies/PipelineArtifactProvider.cs index 1a85018d1a..fdd91e9d24 100644 --- a/eng/update-dependencies/PipelineArtifactProvider.cs +++ b/eng/update-dependencies/PipelineArtifactProvider.cs @@ -50,7 +50,7 @@ internal class PipelineArtifactProvider( private readonly ILogger _logger = logger; private readonly AzdoHttpClient _azdoHttpClient = azdoHttpClient; - private static readonly IEnumerable s_releaseConfigFiles = + private static readonly IEnumerable ReleaseConfigFiles = [ // Newest known location for release artifact data new PipelineArtifactFile("manifests", "ReleaseConfig.json"), @@ -67,11 +67,11 @@ public async Task GetReleaseConfigAsync( string azdoProject, int stagingPipelineRunId) { - var releaseConfigJson = await GetArtifactTextContentAsync( + string releaseConfigJson = await GetArtifactTextContentAsync( azdoOrganization, azdoProject, stagingPipelineRunId, - s_releaseConfigFiles); + ReleaseConfigFiles); return ReleaseConfig.FromJson(releaseConfigJson); } @@ -97,6 +97,7 @@ public async Task GetArtifactTextContentAsync( { throw new ArgumentException("Azure DevOps organization is required", nameof(azdoOrganization)); } + if (string.IsNullOrWhiteSpace(azdoProject)) { throw new ArgumentException("Azure DevOps project is required", nameof(azdoProject)); @@ -128,9 +129,9 @@ public async Task GetArtifactTextContentAsync( _logger.LogInformation( "Downloading artifact {pipelineArtifact} from {downloadUrl}", pipelineArtifact, downloadUrl); - using var response = await _azdoHttpClient.GetAsync(downloadUrl); + using HttpResponseMessage response = await _azdoHttpClient.GetAsync(downloadUrl); response.EnsureSuccessStatusCode(); - var textContent = await response.Content.ReadAsStringAsync(); + string textContent = await response.Content.ReadAsStringAsync(); return textContent; } catch (Exception e) diff --git a/eng/update-dependencies/PipelinesService.cs b/eng/update-dependencies/PipelinesService.cs index 7855262de4..f2a32d57bc 100644 --- a/eng/update-dependencies/PipelinesService.cs +++ b/eng/update-dependencies/PipelinesService.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.TeamFoundation.Build.WebApi; +using Microsoft.VisualStudio.Services.WebApi; namespace Microsoft.DotNet.Docker.UpdateDependencies; @@ -27,20 +28,20 @@ internal sealed class PipelinesService(IAzdoAuthProvider azdoAuthProvider) : IPi /// public async Task GetArtifactAsync(string azdoOrg, string azdoProject, int buildId, string artifactName) { - var client = GetBuildHttpClient(azdoOrg); + BuildHttpClient client = GetBuildHttpClient(azdoOrg); return await client.GetArtifactAsync(azdoProject, buildId, artifactName); } /// public async Task> GetBuildTagsAsync(string azdoOrg, string azdoProject, int buildId) { - var client = GetBuildHttpClient(azdoOrg); + BuildHttpClient client = GetBuildHttpClient(azdoOrg); return await client.GetBuildTagsAsync(azdoProject, buildId); } private BuildHttpClient GetBuildHttpClient(string azdoOrg) { - var connection = _azdoAuthProvider.GetVssConnection(azdoOrg); + VssConnection connection = _azdoAuthProvider.GetVssConnection(azdoOrg); return connection.GetClient(); } } diff --git a/eng/update-dependencies/Program.cs b/eng/update-dependencies/Program.cs index 66e248d453..e2aa4aa6b2 100644 --- a/eng/update-dependencies/Program.cs +++ b/eng/update-dependencies/Program.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.CommandLine; -using Maestro.Common; using Maestro.Common.Telemetry; using Microsoft.DotNet.DarcLib; using Microsoft.DotNet.DarcLib.Helpers; @@ -19,7 +18,7 @@ using Microsoft.Extensions.Logging; using AzureDevOpsClient = Microsoft.DotNet.DarcLib.AzureDevOpsClient; -var builder = Host.CreateApplicationBuilder(new HostApplicationBuilderSettings +HostApplicationBuilder builder = Host.CreateApplicationBuilder(new HostApplicationBuilderSettings { ContentRootPath = AppContext.BaseDirectory, }); @@ -31,7 +30,7 @@ options.SingleLine = false; }); -var configuration = builder.Configuration +UpdateDependenciesConfiguration configuration = builder.Configuration .GetRequiredSection("UpdateDependencies") .Get() ?? throw new InvalidOperationException("Failed to bind UpdateDependencies configuration."); @@ -49,8 +48,8 @@ // Auth services needed for DarcLib remote git operations services.AddSingleton(sp => { - var azdoTokenProvider = sp.GetRequiredService(); - var gitHubTokenProvider = new ResolvedTokenProvider(configuration.GitHub.Token); + IAzureDevOpsTokenProvider azdoTokenProvider = sp.GetRequiredService(); + ResolvedTokenProvider gitHubTokenProvider = new ResolvedTokenProvider(configuration.GitHub.Token); return new RemoteTokenProvider( azdoTokenProvider: azdoTokenProvider, gitHubTokenProvider: gitHubTokenProvider); @@ -72,7 +71,7 @@ // Process-based git client services.AddSingleton(); // LocalGitClient wants a non-generic ILogger, for some reason. -services.AddSingleton(sp => sp.GetRequiredService().CreateLogger(nameof(LocalGitClient))); +services.AddSingleton(sp => sp.GetRequiredService().CreateLogger(nameof(LocalGitClient))); // Git repo cloner that calls out to the `git` executable. It is lighter on memory // than the LibGit2Sharp-based implementation. services.AddSingleton(); @@ -98,8 +97,8 @@ services.AddSingleton(_ => { - var productHeader = new Octokit.ProductHeaderValue("dotnet-docker-update-dependencies"); - var client = new Octokit.GitHubClient(productHeader); + Octokit.ProductHeaderValue productHeader = new Octokit.ProductHeaderValue("dotnet-docker-update-dependencies"); + Octokit.GitHubClient client = new Octokit.GitHubClient(productHeader); if (!string.IsNullOrWhiteSpace(configuration.GitHub.Token)) { client.Credentials = new Octokit.Credentials(configuration.GitHub.Token); @@ -127,7 +126,7 @@ using IHost host = builder.Build(); -var rootCommand = new RootCommand("Update dotnet-docker dependencies") +RootCommand rootCommand = new RootCommand("Update dotnet-docker dependencies") { DependencyCommand.CreateCliCommand(host.Services), DependencyCommand.CreateCliCommand(host.Services), diff --git a/eng/update-dependencies/ScriptRunner.cs b/eng/update-dependencies/ScriptRunner.cs index ac70b54f66..c015d6661a 100644 --- a/eng/update-dependencies/ScriptRunner.cs +++ b/eng/update-dependencies/ScriptRunner.cs @@ -1,5 +1,5 @@ -// 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. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. using System.ComponentModel; using System.Diagnostics; @@ -28,7 +28,7 @@ private static async Task RunAsync(string scriptPath, string repoRoot, Cancellat cancellationToken.ThrowIfCancellationRequested(); Trace.TraceInformation($"Executing '{scriptPath}'"); - var startInfo = new ProcessStartInfo("pwsh") + ProcessStartInfo startInfo = new ProcessStartInfo("pwsh") { WorkingDirectory = repoRoot, UseShellExecute = false, @@ -36,7 +36,7 @@ private static async Task RunAsync(string scriptPath, string repoRoot, Cancellat startInfo.ArgumentList.Add("-NoProfile"); startInfo.ArgumentList.Add("-File"); startInfo.ArgumentList.Add(scriptPath); - using var process = new Process { StartInfo = startInfo }; + using Process process = new Process { StartInfo = startInfo }; // Support both execution within Windows 10, Nano Server and Linux environments. try diff --git a/eng/update-dependencies/Sync/IInternalVersionsService.cs b/eng/update-dependencies/Sync/IInternalVersionsService.cs index a4e3109446..2b6dc941a8 100644 --- a/eng/update-dependencies/Sync/IInternalVersionsService.cs +++ b/eng/update-dependencies/Sync/IInternalVersionsService.cs @@ -24,6 +24,7 @@ internal interface IInternalVersionsService /// If a version already exists for the same dockerfileVersion, it will be /// overwritten. /// + /// The root of the repository to update. /// .NET build or product version /// the stage container name (e.g., "stage-1234567") void RecordInternalStagingBuild(string repoRoot, DotNetVersion dotNetVersion, string stageContainer); diff --git a/eng/update-dependencies/Sync/InternalStageContainers.cs b/eng/update-dependencies/Sync/InternalStageContainers.cs index bd3a610f9c..cb59661145 100644 --- a/eng/update-dependencies/Sync/InternalStageContainers.cs +++ b/eng/update-dependencies/Sync/InternalStageContainers.cs @@ -22,7 +22,7 @@ internal sealed record InternalStageContainers(ImmutableDictionary public static InternalStageContainers Parse(IEnumerable lines) { - var versions = lines + ImmutableDictionary versions = lines .Select(line => line.Split('=', 2)) .Where(parts => parts.Length == 2) .ToImmutableDictionary( diff --git a/eng/update-dependencies/Sync/InternalVersionsService.cs b/eng/update-dependencies/Sync/InternalVersionsService.cs index 139400c514..25d2d57db4 100644 --- a/eng/update-dependencies/Sync/InternalVersionsService.cs +++ b/eng/update-dependencies/Sync/InternalVersionsService.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.Immutable; using Microsoft.DotNet.Docker.Shared; namespace Microsoft.DotNet.Docker.UpdateDependencies.Sync; @@ -14,15 +13,15 @@ internal sealed class InternalVersionsService : IInternalVersionsService /// public InternalStageContainers GetInternalStagingBuilds(string repoRoot) { - var internalVersionFile = Path.Combine(repoRoot, InternalVersionsFileName); + string internalVersionFile = Path.Combine(repoRoot, InternalVersionsFileName); try { - var fileContents = File.ReadAllLines(internalVersionFile); + string[] fileContents = File.ReadAllLines(internalVersionFile); return InternalStageContainers.Parse(fileContents); } catch (FileNotFoundException) { - return new InternalStageContainers(ImmutableDictionary.Empty); + return new InternalStageContainers([]); } } @@ -39,9 +38,9 @@ public void RecordInternalStagingBuild(string repoRoot, DotNetVersion dotNetVers // 2) lots of regex JSON manipulation which is error-prone and harder to maintain // // So for now, the separate file and format is a compromise. - var builds = GetInternalStagingBuilds(repoRoot); + InternalStageContainers builds = GetInternalStagingBuilds(repoRoot); builds = builds.Add(dotNetVersion, stageContainerName); - var internalVersionFile = Path.Combine(repoRoot, InternalVersionsFileName); + string internalVersionFile = Path.Combine(repoRoot, InternalVersionsFileName); File.WriteAllText(internalVersionFile, builds.ToString()); } } diff --git a/eng/update-dependencies/Updaters/AspireUpdater.cs b/eng/update-dependencies/Updaters/AspireUpdater.cs index 3cab0e6142..e3dd48e064 100644 --- a/eng/update-dependencies/Updaters/AspireUpdater.cs +++ b/eng/update-dependencies/Updaters/AspireUpdater.cs @@ -38,7 +38,7 @@ public Task ResolveFromBarBuildAsync(Build build, Cancellation throw new ArgumentException($"BAR build {build.Id} is not an Aspire build.", nameof(build)); } - var dashboardAssets = build.Assets.Where(asset => + Asset[] dashboardAssets = build.Assets.Where(asset => asset.Name.Contains("aspire-dashboard-linux-x64") || asset.Name.Contains("aspire-dashboard-linux-arm64")) .ToArray(); @@ -65,10 +65,10 @@ private async Task ApplyAsync( string branch = variables.GetValue("branch"); string dashboardBaseUrl = variables.GetValue($"aspire-dashboard|base-url|{branch}"); - var dashboardChecksums = await Task.WhenAll(dashboardAssets + string[] dashboardChecksums = await Task.WhenAll(dashboardAssets .Select(asset => GetChecksumAsync($"{dashboardBaseUrl}/{asset.Name}", cancellationToken))); - var majorMinorVersion = VersionHelper.ResolveMajorMinorVersion(version); + Version majorMinorVersion = VersionHelper.ResolveMajorMinorVersion(version); // Aspire Dashboard builds always use "preview 1"; tags use the stable product version. string productVersion = VersionHelper.ResolveProductVersion(version, isStableRelease: true); @@ -79,7 +79,7 @@ private async Task ApplyAsync( variables.SetValue("aspire-dashboard|minor-tag", majorMinorVersion.ToString(2)); variables.SetValue("aspire-dashboard|major-tag", majorMinorVersion.Major.ToString()); - foreach (var (asset, checksum) in dashboardAssets.Zip(dashboardChecksums)) + foreach ((Asset? asset, string? checksum) in dashboardAssets.Zip(dashboardChecksums)) { string arch = asset.Name.Contains("arm64") ? "arm64" : "x64"; variables.SetValue($"aspire-dashboard|linux|{arch}|sha", checksum); diff --git a/eng/update-dependencies/Updaters/BaseUrlUpdater.cs b/eng/update-dependencies/Updaters/BaseUrlUpdater.cs index 833396373e..27f6679cae 100644 --- a/eng/update-dependencies/Updaters/BaseUrlUpdater.cs +++ b/eng/update-dependencies/Updaters/BaseUrlUpdater.cs @@ -20,8 +20,8 @@ public static void Update( string internalBaseUrl, bool sdkOnlyRelease) { - var upstreamBranch = manifestVariables.GetValue("branch"); - var baseUrlVarNames = ManifestHelper.GetBaseUrlVariableNames( + string upstreamBranch = manifestVariables.GetValue("branch"); + IEnumerable baseUrlVarNames = ManifestHelper.GetBaseUrlVariableNames( dockerfileVersion: dockerfileVersion, branch: upstreamBranch, sdkOnlyRelease: sdkOnlyRelease); diff --git a/eng/update-dependencies/Updaters/ChiselUpdater.cs b/eng/update-dependencies/Updaters/ChiselUpdater.cs index 655a37db0a..f3c8f7ee92 100644 --- a/eng/update-dependencies/Updaters/ChiselUpdater.cs +++ b/eng/update-dependencies/Updaters/ChiselUpdater.cs @@ -1,5 +1,5 @@ -// 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. +// 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 Octokit; @@ -15,12 +15,10 @@ public sealed class ChiselUpdater(IReleasesClient releases, HttpClient httpClien public static string Name => "chisel"; public static string VersionSourceName => "chisel"; - private static readonly string[] s_supportedArchitectures = ["amd64", "arm", "arm64"]; - private static string GetChiselManifestVariable(string product, string arch, string type) - { + private static readonly string[] SupportedArchitectures = ["amd64", "arm", "arm64"]; + private static string GetChiselManifestVariable(string product, string arch, string type) => // Workaround for ambiguous method call, will be fixed with https://github.com/dotnet/csharplang/issues/8374 - return string.Join('|', new string[] { product, "latest", ToManifestArch(arch), type }); - } + string.Join('|', new string[] { product, "latest", ToManifestArch(arch), type }); private static Regex GetAssetRegex(string arch) => new(@"chisel_v\d+\.\d+\.\d+_linux_" + arch + @"\.tar\.gz"); @@ -53,7 +51,7 @@ private async Task ApplyAsync( variables.SetValue(BuildVersionVariable, release.TagName); - foreach (string arch in s_supportedArchitectures) + foreach (string arch in SupportedArchitectures) { string urlVariable = GetChiselManifestVariable(Name, arch, "url"); string shaVariable = GetChiselManifestVariable(Name, arch, "sha384"); diff --git a/eng/update-dependencies/Updaters/DependencyUpdate.cs b/eng/update-dependencies/Updaters/DependencyUpdate.cs index 23b3e3ffad..103a9199f5 100644 --- a/eng/update-dependencies/Updaters/DependencyUpdate.cs +++ b/eng/update-dependencies/Updaters/DependencyUpdate.cs @@ -14,6 +14,7 @@ namespace Microsoft.DotNet.Docker.UpdateDependencies.Updaters; /// The absolute path of the repo to update. This is not always the repo the command was run /// from: when creating a pull request, the update is applied to a temporary clone instead. /// +/// The token used to cancel the update. public delegate Task ApplyUpdateAsync( ManifestVariables variables, string repoRoot, diff --git a/eng/update-dependencies/Updaters/DotNetUpdater.cs b/eng/update-dependencies/Updaters/DotNetUpdater.cs index 5d85da0e86..2c6bc309c7 100644 --- a/eng/update-dependencies/Updaters/DotNetUpdater.cs +++ b/eng/update-dependencies/Updaters/DotNetUpdater.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.DotNet.Docker.UpdateDependencies.Model.Release; using Microsoft.DotNet.DarcLib; +using Microsoft.DotNet.Docker.UpdateDependencies.Model.Release; using Microsoft.DotNet.ProductConstructionService.Client.Models; using Microsoft.Extensions.Logging; using NuGet.Versioning; @@ -24,9 +24,9 @@ public async Task ResolveFromBarBuildAsync(Build build, Cancel logger.LogInformation("Updating .NET from BAR build {BuildId} at {Commit}", build.Id, build.Commit); IEnumerable assets = await barClient.GetAssetsAsync(buildId: build.Id).WaitAsync(cancellationToken); - var versions = ProductVersions.FromVmrBuildAssets(assets); + ProductVersions versions = ProductVersions.FromVmrBuildAssets(assets); string dockerfileVersion = VersionHelper.ResolveMajorMinorVersion(versions.Sdk.Version).ToString(2); - var productVersions = new Dictionary + Dictionary productVersions = new Dictionary { ["dotnet"] = versions.Runtime.Version, ["runtime"] = versions.Runtime.Version, @@ -61,7 +61,7 @@ public Task UpdateFromStagingPipelineAsync( { bool isInternal = !string.IsNullOrEmpty(internalBaseUrl); string dockerfileVersion = VersionHelper.ResolveMajorMinorVersion(release.RuntimeBuild).ToString(2); - var productVersions = new Dictionary(); + Dictionary productVersions = []; // SDK-only releases are almost always one-off updates/bug // fixes on top of an existing release of the Runtime and @@ -78,6 +78,7 @@ public Task UpdateFromStagingPipelineAsync( productVersions["aspnet"] = isInternal ? release.AspBuild : release.Asp; productVersions["aspnet-composite"] = productVersions["aspnet"]; } + productVersions["sdk"] = VersionHelper.GetHighestSdkVersion(isInternal ? release.SdkBuilds : release.Sdks); logger.LogInformation("Resolved .NET product versions: {Versions}", @@ -96,12 +97,12 @@ private void ApplyProductVersions( NuGetConfigUpdater.Update(variables, repoRoot, dockerfileVersion, productVersions["sdk"], !string.IsNullOrEmpty(internalBaseUrl)); BaseUrlUpdater.Update(variables, dockerfileVersion, internalBaseUrl, productVersions.Count == 1); - foreach (var (product, version) in productVersions) + foreach ((string? product, string? version) in productVersions) { string productVersion = ""; if (version is not null) { - var parsedVersion = SemanticVersion.Parse(version); + SemanticVersion parsedVersion = SemanticVersion.Parse(version); productVersion = $"{parsedVersion.Major}.{parsedVersion.Minor}.{parsedVersion.Patch}"; if (parsedVersion.IsPrerelease && parsedVersion.ReleaseLabels.First() is not ("servicing" or "rtm")) { diff --git a/eng/update-dependencies/Updaters/MonitorUpdater.cs b/eng/update-dependencies/Updaters/MonitorUpdater.cs index bb447c4959..034102103b 100644 --- a/eng/update-dependencies/Updaters/MonitorUpdater.cs +++ b/eng/update-dependencies/Updaters/MonitorUpdater.cs @@ -13,7 +13,7 @@ public sealed class MonitorUpdater( UpdateDependenciesConfiguration configuration, ILogger logger) { - private static readonly (string Product, string ArchiveName)[] s_products = + private static readonly (string Product, string ArchiveName)[] Products = [ ("monitor", "dotnet-monitor"), ("monitor-base", "dotnet-monitor-base"), @@ -31,7 +31,7 @@ public async Task ResolveFromPipelineBuildAsync( private async Task GetVersionFromPipelineAsync(int pipelineRunId, CancellationToken cancellationToken) { - var versionFile = new PipelineArtifactFile("Build_Info", "dotnet-monitor.nupkg.buildversion"); + PipelineArtifactFile versionFile = new PipelineArtifactFile("Build_Info", "dotnet-monitor.nupkg.buildversion"); string version = await pipelineArtifactProvider .GetArtifactTextContentAsync( configuration.AzureDevOps.Organization, @@ -46,7 +46,7 @@ private async Task GetVersionFromPipelineAsync(int pipelineRunId, Cancel public DependencyUpdate ResolveFromVersion(string version) { version = version.Trim(); - var parsedVersion = SemanticVersion.Parse(version); + SemanticVersion parsedVersion = SemanticVersion.Parse(version); string dockerfileVersion = $"{parsedVersion.Major}.{parsedVersion.Minor}"; return new DependencyUpdate( @@ -80,16 +80,16 @@ private async Task ApplyAsync( variables.SetValue(baseUrlVariable, $"$(base-url|public|{quality}|{branch})"); variables.SetValue(checksumsBaseUrlVariable, $"$(base-url|public-checksums|{quality}|{branch})"); - foreach (var (product, _) in s_products) + foreach ((string? product, string _) in Products) { UpdateVersion(variables, $"{product}|{dockerfileVersion}|build-version", version); UpdateVersion(variables, $"{product}|{dockerfileVersion}|product-version", productVersion); } - foreach (var (product, archiveName) in s_products) + foreach ((string? product, string? archiveName) in Products) { string prefix = $"{product}|{dockerfileVersion}|"; - var checksumVariables = variables.Names + IEnumerable checksumVariables = variables.Names .Where(name => name.StartsWith(prefix, StringComparison.Ordinal)) .Where(name => name.EndsWith("|sha", StringComparison.Ordinal)); diff --git a/eng/update-dependencies/Updaters/NuGetConfigUpdater.cs b/eng/update-dependencies/Updaters/NuGetConfigUpdater.cs index e2eff00a17..1cd9ade46a 100644 --- a/eng/update-dependencies/Updaters/NuGetConfigUpdater.cs +++ b/eng/update-dependencies/Updaters/NuGetConfigUpdater.cs @@ -60,6 +60,7 @@ private static string ToStringWithDeclaration(XDocument doc) // Using the Save method preserves the XML declaration header. ToString doesn't do that. doc.Save(writer); } + return builder.ToString(); } @@ -176,13 +177,8 @@ private static T GetOrCreateXObject(T? node, XContainer parent, Func creat return node; } - private class Utf8StringWriter : StringWriter + private class Utf8StringWriter(StringBuilder stringBuilder) : StringWriter(stringBuilder) { - public Utf8StringWriter(StringBuilder stringBuilder) - : base(stringBuilder) - { - } - public override Encoding Encoding => Encoding.UTF8; } } diff --git a/eng/update-dependencies/Updaters/PowerShellUpdater.cs b/eng/update-dependencies/Updaters/PowerShellUpdater.cs index 14658a4a1b..1757b40e6b 100644 --- a/eng/update-dependencies/Updaters/PowerShellUpdater.cs +++ b/eng/update-dependencies/Updaters/PowerShellUpdater.cs @@ -160,7 +160,7 @@ private async Task ExistsAsync( return await CreateBlobClient(checksumsUrl).ExistsAsync(cancellationToken); } - using var request = new HttpRequestMessage(HttpMethod.Head, checksumsUrl); + using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Head, checksumsUrl); using HttpResponseMessage response = await httpClient.SendAsync(request, cancellationToken); if (response.StatusCode == HttpStatusCode.NotFound) @@ -176,7 +176,7 @@ private async Task ExistsAsync( private BlobClient CreateBlobClient(string url) { TokenCredential credential = credentialProvider.GetCredential(ServiceConnectionNames.Staging); - var options = new BlobClientOptions { Transport = new HttpClientTransport(httpClient) }; + BlobClientOptions options = new BlobClientOptions { Transport = new HttpClientTransport(httpClient) }; return new BlobClient(new Uri(url), credential, options); } @@ -232,12 +232,12 @@ private static string GetBaseUrlValue(string dotnetVersion, string version, bool // Parses lines of the form " *", where '*' marks binary mode and is optional. private static Dictionary ParseChecksums(string content) { - const StringSplitOptions SplitOptions = StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries; - var checksums = new Dictionary(); + const StringSplitOptions splitOptions = StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries; + Dictionary checksums = []; - foreach (string line in content.Split('\n', SplitOptions)) + foreach (string line in content.Split('\n', splitOptions)) { - if (line.Split(' ', 2, SplitOptions) is not [var hash, var file]) + if (line.Split(' ', 2, splitOptions) is not [var hash, var file]) { throw new FormatException($"Unexpected line in {ChecksumsFileName}: '{line}'"); } @@ -294,9 +294,9 @@ private static SemanticVersion[] GetNextVersions(SemanticVersion version) // 7.6.0-preview.5 -> 7.6.0-preview.6, 7.6.0-rc.1, 7.6.0 // 7.6.0-rc.2 -> 7.6.0-rc.3, 7.6.0 - var stableRelease = new SemanticVersion(version.Major, version.Minor, version.Patch); + SemanticVersion stableRelease = new SemanticVersion(version.Major, version.Minor, version.Patch); - var getPrerelease = (string label, int number) => + SemanticVersion GetPrerelease(string label, int number) => new SemanticVersion(version.Major, version.Minor, version.Patch, $"{label}.{number}"); return version.ReleaseLabels.ToArray() switch @@ -304,13 +304,13 @@ private static SemanticVersion[] GetNextVersions(SemanticVersion version) [] => [new SemanticVersion(version.Major, version.Minor, version.Patch + 1)], ["preview", var number] => [ - getPrerelease("preview", int.Parse(number) + 1), - getPrerelease("rc", 1), + GetPrerelease("preview", int.Parse(number) + 1), + GetPrerelease("rc", 1), stableRelease ], ["rc", var number] => [ - getPrerelease("rc", int.Parse(number) + 1), + GetPrerelease("rc", int.Parse(number) + 1), stableRelease ], _ => throw new FormatException($"Unexpected PowerShell version '{version}'."), diff --git a/eng/update-dependencies/Updaters/RocksToolboxUpdater.cs b/eng/update-dependencies/Updaters/RocksToolboxUpdater.cs index 558fa99836..da7c8614e7 100644 --- a/eng/update-dependencies/Updaters/RocksToolboxUpdater.cs +++ b/eng/update-dependencies/Updaters/RocksToolboxUpdater.cs @@ -1,5 +1,5 @@ -// 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. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. using Octokit; diff --git a/eng/update-dependencies/VariableHelper.cs b/eng/update-dependencies/VariableHelper.cs index ac144d4f2b..cec92644b4 100644 --- a/eng/update-dependencies/VariableHelper.cs +++ b/eng/update-dependencies/VariableHelper.cs @@ -1,5 +1,5 @@ -// 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. +// 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; diff --git a/eng/update-dependencies/VersionHelper.cs b/eng/update-dependencies/VersionHelper.cs index 7e596be363..684398d5a0 100644 --- a/eng/update-dependencies/VersionHelper.cs +++ b/eng/update-dependencies/VersionHelper.cs @@ -23,7 +23,7 @@ public static Version ResolveMajorMinorVersion(string versionString) /// A version that may already be a simple 3-part version (e.g. "8.0.100"), /// or a full build version (e.g. "8.0.100-servicing.12345.6"). /// - /// /// Whether the build version is a stable release. This should be false /// for preview releases only. Release candidates are considered stable. /// @@ -38,7 +38,7 @@ public static string ResolveProductVersion(string buildVersion, bool isStableRel int monikerSeparatorIndex = buildVersion.IndexOf('-'); if (monikerSeparatorIndex >= 0) { - return buildVersion.Substring(0, monikerSeparatorIndex); + return buildVersion[..monikerSeparatorIndex]; } } diff --git a/eng/update-dependencies/update-dependencies.csproj b/eng/update-dependencies/update-dependencies.csproj index f02a06cf5d..6e9dddf0b6 100644 --- a/eng/update-dependencies/update-dependencies.csproj +++ b/eng/update-dependencies/update-dependencies.csproj @@ -3,7 +3,7 @@ Exe net10.0 - latest + 14 enable enable Microsoft.DotNet.Docker.UpdateDependencies diff --git a/tests/Microsoft.DotNet.Docker.Tests/Arch.cs b/tests/Microsoft.DotNet.Docker.Tests/Arch.cs index cbfc0c1ad0..c96cb53c40 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/Arch.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/Arch.cs @@ -1,13 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. -namespace Microsoft.DotNet.Docker.Tests +namespace Microsoft.DotNet.Docker.Tests; + +public enum Arch { - public enum Arch - { - Amd64, - Arm, - Arm64, - } + Amd64, + Arm, + Arm64, } diff --git a/tests/Microsoft.DotNet.Docker.Tests/AspireDashboardImageTests.cs b/tests/Microsoft.DotNet.Docker.Tests/AspireDashboardImageTests.cs index efbf8a522c..5912d6875e 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/AspireDashboardImageTests.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/AspireDashboardImageTests.cs @@ -1,11 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. -using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Microsoft.DotNet.Docker.Tests.TestScenarios; using Xunit; using Xunit.Abstractions; @@ -55,7 +54,7 @@ public void VerifyEnvironmentVariables(ProductImageData imageData) ]; string imageTag = imageData.GetImage(ImageRepo, DockerHelper); - EnvironmentVariableInfo.Validate(expectedVariables, imageTag, imageData, DockerHelper); + EnvironmentVariableInfo.Validate(expectedVariables, imageTag, DockerHelper); } [LinuxImageTheory] @@ -67,7 +66,7 @@ public void VerifyInstalledPackages(ProductImageData imageData) IEnumerable expectedPackages = GetExpectedPackages(imageData with { ImageVariant = DotNetImageVariant.Extra }, ImageRepo); IEnumerable actualPackages = - GetInstalledPackages(imageData, ImageRepo, [ AppPath ]); + GetInstalledPackages(imageData, ImageRepo, [AppPath]); string imageName = imageData.GetImage(ImageRepo, DockerHelper, skipPull: true); ComparePackages(expectedPackages, actualPackages, imageData.IsDistroless, imageName, OutputHelper); diff --git a/tests/Microsoft.DotNet.Docker.Tests/AspireDashboardManifestTests.cs b/tests/Microsoft.DotNet.Docker.Tests/AspireDashboardManifestTests.cs index da5583ee91..a8f8acebf3 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/AspireDashboardManifestTests.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/AspireDashboardManifestTests.cs @@ -1,6 +1,5 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. using System.Linq; using Newtonsoft.Json.Linq; @@ -17,10 +16,7 @@ public sealed class AspireDashboardManifestTests [Theory] [InlineData("", "aspire/dashboard")] [InlineData("/nightly", "aspire/nightly/dashboard")] - public void RepoName_UsesAspireProductFamily(string repoNameModifier, string expectedRepoName) - { - Assert.Equal(expectedRepoName, ImageData.GetRepoName(AspireDashboardId, repoNameModifier)); - } + public void RepoName_UsesAspireProductFamily(string repoNameModifier, string expectedRepoName) => Assert.Equal(expectedRepoName, ImageData.GetRepoName(AspireDashboardId, repoNameModifier)); [Fact] public void Manifest_UsesCanonicalRepoName() diff --git a/tests/Microsoft.DotNet.Docker.Tests/AspnetImageTests.cs b/tests/Microsoft.DotNet.Docker.Tests/AspnetImageTests.cs index 6815e2f297..f1791c98a2 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/AspnetImageTests.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/AspnetImageTests.cs @@ -1,131 +1,109 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; +using Microsoft.DotNet.Docker.Tests.TestScenarios; using Xunit; using Xunit.Abstractions; -namespace Microsoft.DotNet.Docker.Tests +namespace Microsoft.DotNet.Docker.Tests; + +[Trait("Category", "aspnet")] +public class AspnetImageTests(ITestOutputHelper outputHelper) : CommonRuntimeImageTests(outputHelper) { - [Trait("Category", "aspnet")] - public class AspnetImageTests : CommonRuntimeImageTests + protected override DotNetImageRepo ImageRepo => DotNetImageRepo.Aspnet; + + public static IEnumerable GetImageData() => GetImageData(DotNetImageRepo.Aspnet); + + [DotNetTheory] + [MemberData(nameof(GetImageData))] + public async Task VerifyFxDependentAppScenario(ProductImageData imageData) { - public AspnetImageTests(ITestOutputHelper outputHelper) - : base(outputHelper) + if (Config.IsNightlyRepo && imageData.ImageVariant.HasFlag(DotNetImageVariant.Composite)) { + OutputHelper.WriteLine("Skip test due to https://github.com/dotnet/dotnet-docker/issues/4834"); + return; } - protected override DotNetImageRepo ImageRepo => DotNetImageRepo.Aspnet; - - public static IEnumerable GetImageData() => GetImageData(DotNetImageRepo.Aspnet); + WebScenario scenario = new WebScenario.FxDependent(imageData, DockerHelper, OutputHelper); + await scenario.ExecuteAsync(); + } - [DotNetTheory] - [MemberData(nameof(GetImageData))] - public async Task VerifyFxDependentAppScenario(ProductImageData imageData) + [DotNetTheory] + [MemberData(nameof(GetImageData))] + public async Task VerifyGlobalizationScenario(ProductImageData imageData) + { + if (Config.IsNightlyRepo && imageData.ImageVariant.HasFlag(DotNetImageVariant.Composite)) { - if (Config.IsNightlyRepo && imageData.ImageVariant.HasFlag(DotNetImageVariant.Composite)) - { - OutputHelper.WriteLine("Skip test due to https://github.com/dotnet/dotnet-docker/issues/4834"); - return; - } - - WebScenario scenario = new WebScenario.FxDependent(imageData, DockerHelper, OutputHelper); - await scenario.ExecuteAsync(); + OutputHelper.WriteLine("Skip test due to https://github.com/dotnet/dotnet-docker/issues/4834"); + return; } - [DotNetTheory] - [MemberData(nameof(GetImageData))] - public async Task VerifyGlobalizationScenario(ProductImageData imageData) - { - if (Config.IsNightlyRepo && imageData.ImageVariant.HasFlag(DotNetImageVariant.Composite)) - { - OutputHelper.WriteLine("Skip test due to https://github.com/dotnet/dotnet-docker/issues/4834"); - return; - } + await VerifyGlobalizationScenarioBase(imageData); + } - await VerifyGlobalizationScenarioBase(imageData); - } + [WindowsImageTheory] + [MemberData(nameof(GetImageData))] + public async Task VerifyNLSScenario(ProductImageData imageData) => + await VerifyNlsScenarioBase(imageData); + + [DotNetTheory] + [MemberData(nameof(GetImageData))] + public void VerifyEnvironmentVariables(ProductImageData imageData) + { + List variables = []; - [WindowsImageTheory] - [MemberData(nameof(GetImageData))] - public async Task VerifyNLSScenario(ProductImageData imageData) => - await VerifyNlsScenarioBase(imageData); + EnvironmentVariableInfo runtimeVersionInfo = + RuntimeImageTests.GetRuntimeVersionVariableInfo(ImageRepo, imageData, DockerHelper); - [DotNetTheory] - [MemberData(nameof(GetImageData))] - public void VerifyEnvironmentVariables(ProductImageData imageData) + // Skip runtime version check due to https://github.com/dotnet/dotnet-docker/issues/4834. + // Re-enable when fixed. + if (imageData.ImageVariant.HasFlag(DotNetImageVariant.Composite)) { - List variables = []; - - EnvironmentVariableInfo runtimeVersionInfo = - RuntimeImageTests.GetRuntimeVersionVariableInfo(ImageRepo, imageData, DockerHelper); + runtimeVersionInfo = runtimeVersionInfo with { AllowAnyValue = true }; + } - // Skip runtime version check due to https://github.com/dotnet/dotnet-docker/issues/4834. - // Re-enable when fixed. - if (imageData.ImageVariant.HasFlag(DotNetImageVariant.Composite)) - { - runtimeVersionInfo = runtimeVersionInfo with { AllowAnyValue = true }; - } + variables.Add(runtimeVersionInfo); - variables.Add(runtimeVersionInfo); + EnvironmentVariableInfo aspnetVersionVariableInfo = + GetAspnetVersionVariableInfo(ImageRepo, imageData, DockerHelper); + if (aspnetVersionVariableInfo != null) + { + variables.Add(aspnetVersionVariableInfo); + } - EnvironmentVariableInfo aspnetVersionVariableInfo = - GetAspnetVersionVariableInfo(ImageRepo, imageData, DockerHelper); - if (aspnetVersionVariableInfo != null) - { - variables.Add(aspnetVersionVariableInfo); - } + VerifyCommonEnvironmentVariables(imageData, variables); + } - base.VerifyCommonEnvironmentVariables(imageData, variables); - } + [LinuxImageTheory] + [MemberData(nameof(GetImageData))] + public void VerifyInstalledPackages(ProductImageData imageData) => VerifyInstalledPackagesBase(imageData, ImageRepo); - [LinuxImageTheory] - [MemberData(nameof(GetImageData))] - public void VerifyInstalledPackages(ProductImageData imageData) - { - VerifyInstalledPackagesBase(imageData, ImageRepo); - } + [LinuxImageTheory] + [MemberData(nameof(GetImageData))] + public void VerifyInsecureFiles(ProductImageData imageData) => VerifyCommonInsecureFiles(imageData); - [LinuxImageTheory] - [MemberData(nameof(GetImageData))] - public void VerifyInsecureFiles(ProductImageData imageData) - { - base.VerifyCommonInsecureFiles(imageData); - } + [LinuxImageTheory] + [MemberData(nameof(GetImageData))] + public void VerifyShellNotInstalledForDistroless(ProductImageData imageData) => VerifyCommonShellNotInstalledForDistroless(imageData); - [LinuxImageTheory] - [MemberData(nameof(GetImageData))] - public void VerifyShellNotInstalledForDistroless(ProductImageData imageData) - { - base.VerifyCommonShellNotInstalledForDistroless(imageData); - } + [DotNetTheory] + [MemberData(nameof(GetImageData))] + public void VerifyNoSasToken(ProductImageData imageData) => VerifyCommonNoSasToken(imageData); - [DotNetTheory] - [MemberData(nameof(GetImageData))] - public void VerifyNoSasToken(ProductImageData imageData) - { - base.VerifyCommonNoSasToken(imageData); - } + [DotNetTheory] + [MemberData(nameof(GetImageData))] + public void VerifyDefaultUser(ProductImageData imageData) => VerifyCommonDefaultUser(imageData); - [DotNetTheory] - [MemberData(nameof(GetImageData))] - public void VerifyDefaultUser(ProductImageData imageData) - { - VerifyCommonDefaultUser(imageData); - } + public static EnvironmentVariableInfo GetAspnetVersionVariableInfo( + DotNetImageRepo imageRepo, ProductImageData imageData, DockerHelper dockerHelper) + { + string version = imageData.GetProductVersion(imageRepo, DotNetImageRepo.Aspnet, dockerHelper); - public static EnvironmentVariableInfo GetAspnetVersionVariableInfo( - DotNetImageRepo imageRepo, ProductImageData imageData, DockerHelper dockerHelper) + return EnvironmentVariableInfo.Require("ASPNET_VERSION", version) with { - string version = imageData.GetProductVersion(imageRepo, DotNetImageRepo.Aspnet, dockerHelper); - - return EnvironmentVariableInfo.Require("ASPNET_VERSION", version) with - { - IsProductVersion = true - }; - } + IsProductVersion = true + }; } } diff --git a/tests/Microsoft.DotNet.Docker.Tests/AssemblyInfo.cs b/tests/Microsoft.DotNet.Docker.Tests/AssemblyInfo.cs index 70da298f72..6571e2dc21 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/AssemblyInfo.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/AssemblyInfo.cs @@ -1,6 +1,5 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. using Xunit; diff --git a/tests/Microsoft.DotNet.Docker.Tests/CommonRuntimeImageTests.cs b/tests/Microsoft.DotNet.Docker.Tests/CommonRuntimeImageTests.cs index a32d9a4b99..99ed56bf18 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/CommonRuntimeImageTests.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/CommonRuntimeImageTests.cs @@ -1,92 +1,84 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Microsoft.DotNet.Docker.Tests.TestScenarios; using Xunit; using Xunit.Abstractions; -using Microsoft.DotNet.Docker.Tests.TestScenarios; -namespace Microsoft.DotNet.Docker.Tests +namespace Microsoft.DotNet.Docker.Tests; + +public abstract class CommonRuntimeImageTests(ITestOutputHelper outputHelper) : ProductImageTests(outputHelper) { - public abstract class CommonRuntimeImageTests : ProductImageTests + public static IEnumerable GetImageData( + DotNetImageRepo imageRepo, + DotNetImageVariant variant = DotNetImageVariant.None) { - protected CommonRuntimeImageTests(ITestOutputHelper outputHelper) - : base(outputHelper) + return TestData.GetImageData(imageRepo, variant) + .Select(imageData => new object[] { imageData }); + } + + protected void VerifyCommonEnvironmentVariables( + ProductImageData imageData, IEnumerable customVariables = null) + { + List variables = [.. GetCommonEnvironmentVariables()]; + + if (!imageData.IsWindows) { + variables.Add(EnvironmentVariableInfo.Require("APP_UID", imageData.NonRootUID?.ToString())); } - public static IEnumerable GetImageData( - DotNetImageRepo imageRepo, - DotNetImageVariant variant = DotNetImageVariant.None) + variables.Add(EnvironmentVariableInfo.Require("ASPNETCORE_HTTP_PORTS", imageData.DefaultPort.ToString())); + + if (customVariables != null) { - return TestData.GetImageData(imageRepo, variant) - .Select(imageData => new object[] { imageData }); + variables.AddRange(customVariables); } - protected void VerifyCommonEnvironmentVariables( - ProductImageData imageData, IEnumerable customVariables = null) + if (imageData.GlobalizationInvariantMode) { - List variables = new List(); - variables.AddRange(GetCommonEnvironmentVariables()); - - if (!imageData.IsWindows) - { - variables.Add(EnvironmentVariableInfo.Require("APP_UID", imageData.NonRootUID?.ToString())); - } + variables.Add(EnvironmentVariableInfo.Require("DOTNET_SYSTEM_GLOBALIZATION_INVARIANT", "true")); + } - variables.Add(EnvironmentVariableInfo.Require("ASPNETCORE_HTTP_PORTS", imageData.DefaultPort.ToString())); + string imageTag = imageData.GetImage(ImageRepo, DockerHelper); - if (customVariables != null) - { - variables.AddRange(customVariables); - } + EnvironmentVariableInfo.Validate(variables, imageTag, DockerHelper); + } - if (imageData.GlobalizationInvariantMode) - { - variables.Add(EnvironmentVariableInfo.Require("DOTNET_SYSTEM_GLOBALIZATION_INVARIANT", "true")); - } + protected void VerifyCommonShellNotInstalledForDistroless(ProductImageData imageData) + { + if (!imageData.IsDistroless) + { + OutputHelper.WriteLine("Skipping test for non-distroless platform."); + return; + } - string imageTag = imageData.GetImage(ImageRepo, DockerHelper); + string imageTag = imageData.GetImage(ImageRepo, DockerHelper); - EnvironmentVariableInfo.Validate(variables, imageTag, imageData, DockerHelper); - } + // Attempting to execute the container's shell should result in an exception. + // There should be no shell installed in distroless containers. + InvalidOperationException ex = Assert.Throws(() => + DockerHelper.Run( + image: imageTag, + name: imageData.GetIdentifier($"env"), + optionalRunArgs: $"--entrypoint /bin/sh") + ); - protected void VerifyCommonShellNotInstalledForDistroless(ProductImageData imageData) - { - if (!imageData.IsDistroless) - { - OutputHelper.WriteLine("Skipping test for non-distroless platform."); - return; - } - - string imageTag = imageData.GetImage(ImageRepo, DockerHelper); - - // Attempting to execute the container's shell should result in an exception. - // There should be no shell installed in distroless containers. - InvalidOperationException ex = Assert.Throws(() => - DockerHelper.Run( - image: imageTag, - name: imageData.GetIdentifier($"env"), - optionalRunArgs: $"--entrypoint /bin/sh") - ); - - Assert.Contains("Exit code: 127", ex.Message); - } + Assert.Contains("Exit code: 127", ex.Message); + } - protected async Task VerifyGlobalizationScenarioBase(ProductImageData imageData) - { - using var testScenario = new GlobalizationScenario(imageData, ImageRepo, DockerHelper); - await testScenario.ExecuteAsync(); - } + protected async Task VerifyGlobalizationScenarioBase(ProductImageData imageData) + { + using GlobalizationScenario testScenario = new GlobalizationScenario(imageData, ImageRepo, DockerHelper); + await testScenario.ExecuteAsync(); + } - protected async Task VerifyNlsScenarioBase(ProductImageData imageData) - { - using var testScenario = new NlsScenario(imageData, ImageRepo, DockerHelper); - await testScenario.ExecuteAsync(); - } + protected async Task VerifyNlsScenarioBase(ProductImageData imageData) + { + using NlsScenario testScenario = new NlsScenario(imageData, ImageRepo, DockerHelper); + await testScenario.ExecuteAsync(); } } diff --git a/tests/Microsoft.DotNet.Docker.Tests/Config.cs b/tests/Microsoft.DotNet.Docker.Tests/Config.cs index 5e2280b1ca..6e4cb720fd 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/Config.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/Config.cs @@ -1,6 +1,5 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. using System; using System.Collections.Generic; @@ -9,115 +8,114 @@ using System.Text.RegularExpressions; using Newtonsoft.Json.Linq; -namespace Microsoft.DotNet.Docker.Tests +namespace Microsoft.DotNet.Docker.Tests; + +public static class Config { - public static class Config + private const string VariableGroupName = "variable"; + private const string VariablePattern = $"\\$\\((?<{VariableGroupName}>[\\w:\\-.|]+)\\)"; + private static Lazy ManifestVersions { get; } = + new Lazy(() => LoadManifest("manifest.versions.json")); + + public static Lazy Manifest { get; } = new Lazy(() => LoadManifest("manifest.json")); + public static string SourceRepoRoot { get; } = + GetEnvironmentVariableOrDefault("SOURCE_REPO_ROOT", string.Empty); + public static bool IsHttpVerificationDisabled { get; } = + Environment.GetEnvironmentVariable("DISABLE_HTTP_VERIFICATION") != null; + public static bool PullImages { get; } = + !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("PULL_IMAGES")); + public static bool IsNightlyRepo { get; } = GetIsNightlyRepo(); + public static bool IsRunningInContainer { get; } = + !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("RUNNING_TESTS_IN_CONTAINER")); + public static string RepoPrefix { get; } = GetEnvironmentVariableOrDefault("REPO_PREFIX", string.Empty); + public static string Registry { get; } = + GetEnvironmentVariableOrDefault("REGISTRY", (string)Manifest.Value["registry"]); + public static string CacheRegistry { get; } = + GetEnvironmentVariableOrDefault("CACHE_REGISTRY", string.Empty); + public static string[] OsNames { get; } = + GetEnvironmentVariableOrDefault("IMAGE_OS_NAMES", string.Empty) + .Split(",", StringSplitOptions.RemoveEmptyEntries); + public static string SourceBranch { get; } = + GetEnvironmentVariableOrDefault("SOURCE_BRANCH", string.Empty); + public static string InternalAccessToken { get; } = + GetEnvironmentVariableOrDefault("INTERNAL_ACCESS_TOKEN", string.Empty); + public static string[] Paths { get; } = + Environment.GetEnvironmentVariable("DOCKERFILE_PATHS")? + .Split(',', StringSplitOptions.RemoveEmptyEntries) ?? []; + + public static bool IsInternal { get; } = + !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("INTERNAL_TESTING")); + + private static string GetEnvironmentVariableOrDefault(string variableName, string defaultValue) { - private const string VariableGroupName = "variable"; - private const string VariablePattern = $"\\$\\((?<{VariableGroupName}>[\\w:\\-.|]+)\\)"; - private static Lazy ManifestVersions { get; } = - new Lazy(() => LoadManifest("manifest.versions.json")); - - public static Lazy Manifest { get; } = new Lazy(() => LoadManifest("manifest.json")); - public static string SourceRepoRoot { get; } = - GetEnvironmentVariableOrDefault("SOURCE_REPO_ROOT", string.Empty); - public static bool IsHttpVerificationDisabled { get; } = - Environment.GetEnvironmentVariable("DISABLE_HTTP_VERIFICATION") != null; - public static bool PullImages { get; } = - !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("PULL_IMAGES")); - public static bool IsNightlyRepo { get; } = GetIsNightlyRepo(); - public static bool IsRunningInContainer { get; } = - !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("RUNNING_TESTS_IN_CONTAINER")); - public static string RepoPrefix { get; } = GetEnvironmentVariableOrDefault("REPO_PREFIX", string.Empty); - public static string Registry { get; } = - GetEnvironmentVariableOrDefault("REGISTRY", (string)Manifest.Value["registry"]); - public static string CacheRegistry { get; } = - GetEnvironmentVariableOrDefault("CACHE_REGISTRY", string.Empty); - public static string[] OsNames { get; } = - GetEnvironmentVariableOrDefault("IMAGE_OS_NAMES", string.Empty) - .Split(",", StringSplitOptions.RemoveEmptyEntries); - public static string SourceBranch { get; } = - GetEnvironmentVariableOrDefault("SOURCE_BRANCH", string.Empty); - public static string InternalAccessToken { get; } = - GetEnvironmentVariableOrDefault("INTERNAL_ACCESS_TOKEN", string.Empty); - public static string[] Paths { get; } = - Environment.GetEnvironmentVariable("DOCKERFILE_PATHS")? - .Split(',', StringSplitOptions.RemoveEmptyEntries) ?? []; - - public static bool IsInternal { get; } = - !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("INTERNAL_TESTING")); - - private static string GetEnvironmentVariableOrDefault(string variableName, string defaultValue) - { - string value = Environment.GetEnvironmentVariable(variableName); - return !string.IsNullOrEmpty(value) ? value : defaultValue; - } + string value = Environment.GetEnvironmentVariable(variableName); + return !string.IsNullOrEmpty(value) ? value : defaultValue; + } - private static bool GetIsNightlyRepo() - { - string repo = (string)Manifest.Value["repos"][0]["name"]; - return repo.Contains("/nightly/"); - } + private static bool GetIsNightlyRepo() + { + string repo = (string)Manifest.Value["repos"][0]["name"]; + return repo.Contains("/nightly/"); + } - private static JObject LoadManifest(string manifestFile) - { - string manifestPath = Path.Combine(SourceRepoRoot, manifestFile); - string manifestJson = File.ReadAllText(manifestPath); - return JObject.Parse(manifestJson); - } + private static JObject LoadManifest(string manifestFile) + { + string manifestPath = Path.Combine(SourceRepoRoot, manifestFile); + string manifestJson = File.ReadAllText(manifestPath); + return JObject.Parse(manifestJson); + } - public static string GetVariableValue(string variableName) => - GetVariableValue(variableName, (JObject)ManifestVersions.Value["variables"]); + public static string GetVariableValue(string variableName) => + GetVariableValue(variableName, (JObject)ManifestVersions.Value["variables"]); - public static IEnumerable GetVariableNames() => - ((JObject)ManifestVersions.Value["variables"]).Properties().Select(property => property.Name); + public static IEnumerable GetVariableNames() => + ((JObject)ManifestVersions.Value["variables"]).Properties().Select(property => property.Name); - public static bool TryGetVariableValue(string variableName, out string value) + public static bool TryGetVariableValue(string variableName, out string value) + { + JObject variables = (JObject)ManifestVersions.Value["variables"]; + JToken token = variables[variableName]; + if (token is null) { - var variables = (JObject)ManifestVersions.Value["variables"]; - JToken token = variables[variableName]; - if (token is null) - { - value = string.Empty; - return false; - } - - value = ResolveVariables((string)token, variables); - return true; + value = string.Empty; + return false; } - private static string GetVariableValue(string variableName, JObject variables) => - ResolveVariables((string)variables[variableName], variables); + value = ResolveVariables((string)token, variables); + return true; + } + + private static string GetVariableValue(string variableName, JObject variables) => + ResolveVariables((string)variables[variableName], variables); - private static string ResolveVariables(string value, JObject variables) + private static string ResolveVariables(string value, JObject variables) + { + MatchCollection matches = Regex.Matches(value, VariablePattern); + foreach (Match match in matches) { - MatchCollection matches = Regex.Matches(value, VariablePattern); - foreach (Match match in matches) - { - string variableName = match.Groups[VariableGroupName].Value; - string variableValue = GetVariableValue(variableName, variables); - value = value.Replace(match.Value, variableValue); - } - - return value; + string variableName = match.Groups[VariableGroupName].Value; + string variableValue = GetVariableValue(variableName, variables); + value = value.Replace(match.Value, variableValue); } - public static string GetBaseUrl(string dotnetVersion) => - GetVariableValue($"dotnet|{dotnetVersion}|base-url|{Config.SourceBranch}", (JObject)ManifestVersions.Value["variables"]); + return value; + } + + public static string GetBaseUrl(string dotnetVersion) => + GetVariableValue($"dotnet|{dotnetVersion}|base-url|{SourceBranch}", (JObject)ManifestVersions.Value["variables"]); - public static string GetBuildVersion(DotNetImageRepo imageRepo, string dotnetVersion) + public static string GetBuildVersion(DotNetImageRepo imageRepo, string dotnetVersion) + { + if (imageRepo == DotNetImageRepo.Runtime_Deps) { - if (imageRepo == DotNetImageRepo.Runtime_Deps) - { - throw new NotSupportedException("Runtime deps has no associated build version"); - } - - return GetVariableValue( - $"{imageRepo.ToString().ToLower()}|{dotnetVersion}|build-version", - (JObject)ManifestVersions.Value["variables"]); + throw new NotSupportedException("Runtime deps has no associated build version"); } - public static string GetFilterRegexPattern(string filter) => - filter is null ? null : $"^{Regex.Escape(filter).Replace(@"\*", ".*").Replace(@"\?", ".")}$"; + return GetVariableValue( + $"{imageRepo.ToString().ToLower()}|{dotnetVersion}|build-version", + (JObject)ManifestVersions.Value["variables"]); } + + public static string GetFilterRegexPattern(string filter) => + filter is null ? null : $"^{Regex.Escape(filter).Replace(@"\*", ".*").Replace(@"\?", ".")}$"; } diff --git a/tests/Microsoft.DotNet.Docker.Tests/DockerHelper.cs b/tests/Microsoft.DotNet.Docker.Tests/DockerHelper.cs index aabca0ea08..df16592363 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/DockerHelper.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/DockerHelper.cs @@ -1,6 +1,5 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. using System; using System.Collections.Generic; @@ -12,376 +11,358 @@ using Newtonsoft.Json.Linq; using Xunit.Abstractions; -namespace Microsoft.DotNet.Docker.Tests +namespace Microsoft.DotNet.Docker.Tests; + +public class DockerHelper(ITestOutputHelper outputHelper) { - public class DockerHelper + private static readonly Lazy LazyDockerOS = new(GetDockerOS); + public static string DockerOS => LazyDockerOS.Value; + + public static string ContainerWorkDir => IsLinuxContainerModeEnabled ? "/sandbox" : "c:\\sandbox"; + public static bool IsLinuxContainerModeEnabled => string.Equals(DockerOS, "linux", StringComparison.OrdinalIgnoreCase); + public static string TestArtifactsDir { get; } = Path.Combine(Directory.GetCurrentDirectory(), "TestAppArtifacts"); + + private ITestOutputHelper OutputHelper { get; set; } = outputHelper; + +#nullable enable + public void Build( + string tag = "", + string dockerfile = "", + string target = "", + string contextDir = ".", + bool pull = false, + string platform = "", + string output = "", + params string[] buildArgs + ) { - private static readonly Lazy s_dockerOS = new(GetDockerOS); - public static string DockerOS => s_dockerOS.Value; - - public static string ContainerWorkDir => IsLinuxContainerModeEnabled ? "/sandbox" : "c:\\sandbox"; - public static bool IsLinuxContainerModeEnabled => string.Equals(DockerOS, "linux", StringComparison.OrdinalIgnoreCase); - public static string TestArtifactsDir { get; } = Path.Combine(Directory.GetCurrentDirectory(), "TestAppArtifacts"); + List args = []; - private ITestOutputHelper OutputHelper { get; set; } - - public DockerHelper(ITestOutputHelper outputHelper) + // Optional basic flags + if (!string.IsNullOrWhiteSpace(tag)) { - OutputHelper = outputHelper; + args.Add("-t"); + args.Add(tag); } - #nullable enable - public void Build( - string tag = "", - string dockerfile = "", - string target = "", - string contextDir = ".", - bool pull = false, - string platform = "", - string output = "", - params string[] buildArgs - ) + if (!string.IsNullOrWhiteSpace(dockerfile)) { - var args = new List(); - - // Optional basic flags - if (!string.IsNullOrWhiteSpace(tag)) - { - args.Add("-t"); - args.Add(tag); - } - - if (!string.IsNullOrWhiteSpace(dockerfile)) - { - args.Add("-f"); - args.Add(dockerfile); - } + args.Add("-f"); + args.Add(dockerfile); + } - if (!string.IsNullOrWhiteSpace(target)) - { - args.Add("--target"); - args.Add(target); - } + if (!string.IsNullOrWhiteSpace(target)) + { + args.Add("--target"); + args.Add(target); + } - // Build args - if (buildArgs is not null) + // Build args + if (buildArgs is not null) + { + foreach (string buildArg in buildArgs) { - foreach (string buildArg in buildArgs) + if (!string.IsNullOrWhiteSpace(buildArg)) { - if (!string.IsNullOrWhiteSpace(buildArg)) - { - args.Add("--build-arg"); - args.Add(buildArg); - } + args.Add("--build-arg"); + args.Add(buildArg); } } + } - if (!string.IsNullOrWhiteSpace(platform)) - { - args.Add("--platform"); - args.Add(platform); - } + if (!string.IsNullOrWhiteSpace(platform)) + { + args.Add("--platform"); + args.Add(platform); + } - if (!string.IsNullOrWhiteSpace(output)) - { - args.Add("--output"); - args.Add(output); - } + if (!string.IsNullOrWhiteSpace(output)) + { + args.Add("--output"); + args.Add(output); + } - if (pull) - { - args.Add("--pull"); - } + if (pull) + { + args.Add("--pull"); + } - args.Add(contextDir); + args.Add(contextDir); - ExecuteWithLogging($"build {string.Join(' ', args)}"); - } - #nullable disable - - /// - /// Builds a helper image intended to test distroless scenarios. - /// - /// - /// Because distroless containers do not contain a shell, and potentially other packages necessary for testing, - /// this helper image stores the entire root of the distroless filesystem at the specified destination path within - /// the built container image. - /// - public string BuildDistrolessHelper(DotNetImageRepo imageRepo, ProductImageData imageData, string copyDestination, string copyOrigin = "/") + ExecuteWithLogging($"build {string.Join(' ', args)}"); + } +#nullable disable + + /// + /// Builds a helper image intended to test distroless scenarios. + /// + /// + /// Because distroless containers do not contain a shell, and potentially other packages necessary for testing, + /// this helper image stores the entire root of the distroless filesystem at the specified destination path within + /// the built container image. + /// + public string BuildDistrolessHelper(DotNetImageRepo imageRepo, ProductImageData imageData, string copyDestination, string copyOrigin = "/") + { + string dockerfile = Path.Combine(TestArtifactsDir, "Dockerfile.copy"); + string distrolessImageTag = imageData.GetImage(imageRepo, this); + + // Use the runtime-deps image as the target of the filesystem copy. + // Not all images are versioned the same as the mainline .NET products. + // Use the version family (e.g. the .NET product family version) as the + // version of the runtime-deps image get the correct image. + ProductImageData runtimeDepsImageData = new() { - string dockerfile = Path.Combine(TestArtifactsDir, "Dockerfile.copy"); - string distrolessImageTag = imageData.GetImage(imageRepo, this); - - // Use the runtime-deps image as the target of the filesystem copy. - // Not all images are versioned the same as the mainline .NET products. - // Use the version family (e.g. the .NET product family version) as the - // version of the runtime-deps image get the correct image. - ProductImageData runtimeDepsImageData = new() - { - Version = imageData.VersionFamily, - OS = imageData.OS, - Arch = imageData.Arch, - }; - - // Make sure we don't try to get an image that we don't need before we specify that we want the distro-full - // version. The image might not be on disk. The correct, distro-full versino will be pulled in the helper - // image build. - string baseImageTag = runtimeDepsImageData - .GetImage(DotNetImageRepo.Runtime_Deps, this, skipPull: true) - .Replace("-distroless", string.Empty) - .Replace("-chiseled", string.Empty); - - string tag = imageData.GetIdentifier("distroless-helper"); - - Build(tag, dockerfile, null, TestArtifactsDir, false, - platform: imageData.Platform, - buildArgs: - [ - $"copy_image={distrolessImageTag}", - $"base_image={baseImageTag}", - $"copy_origin={copyOrigin}", - $"copy_destination={copyDestination}" - ]); - - return tag; - } + Version = imageData.VersionFamily, + OS = imageData.OS, + Arch = imageData.Arch, + }; + + // Make sure we don't try to get an image that we don't need before we specify that we want the distro-full + // version. The image might not be on disk. The correct, distro-full versino will be pulled in the helper + // image build. + string baseImageTag = runtimeDepsImageData + .GetImage(DotNetImageRepo.Runtime_Deps, this, skipPull: true) + .Replace("-distroless", string.Empty) + .Replace("-chiseled", string.Empty); + + string tag = imageData.GetIdentifier("distroless-helper"); + + Build(tag, dockerfile, null, TestArtifactsDir, false, + platform: imageData.Platform, + buildArgs: + [ + $"copy_image={distrolessImageTag}", + $"base_image={baseImageTag}", + $"copy_origin={copyOrigin}", + $"copy_destination={copyDestination}" + ]); + + return tag; + } - public static bool ContainerExists(string name) => ResourceExists("container", $"-f \"name={name}\""); + public static bool ContainerExists(string name) => ResourceExists("container", $"-f \"name={name}\""); - public static bool ContainerIsRunning(string name) => Execute($"inspect --format=\"{{{{.State.Running}}}}\" {name}") == "true"; + public static bool ContainerIsRunning(string name) => Execute($"inspect --format=\"{{{{.State.Running}}}}\" {name}") == "true"; - public void Copy(string src, string dest) => ExecuteWithLogging($"cp {src} {dest}"); + public void Copy(string src, string dest) => ExecuteWithLogging($"cp {src} {dest}"); - public void DeleteContainer(string container, bool captureLogs = false) + public void DeleteContainer(string container, bool captureLogs = false) + { + if (ContainerExists(container)) { - if (ContainerExists(container)) + if (captureLogs) { - if (captureLogs) - { - ExecuteWithLogging($"logs {container}", ignoreErrors: true); - } + ExecuteWithLogging($"logs {container}", ignoreErrors: true); + } - // If a container is already stopped, running `docker stop` again has no adverse effects. - // This prevents some issues where containers could fail to be forcibly removed while they're running. - // e.g. https://github.com/dotnet/dotnet-docker/issues/5127 - StopContainer(container); + // If a container is already stopped, running `docker stop` again has no adverse effects. + // This prevents some issues where containers could fail to be forcibly removed while they're running. + // e.g. https://github.com/dotnet/dotnet-docker/issues/5127 + StopContainer(container); - ExecuteWithLogging($"container rm -f {container}"); - } + ExecuteWithLogging($"container rm -f {container}"); } + } - public void DeleteImage(string tag) + public void DeleteImage(string tag) + { + if (ImageExists(tag)) { - if (ImageExists(tag)) - { - ExecuteWithLogging($"image rm -f {tag}"); - } + ExecuteWithLogging($"image rm -f {tag}"); } + } - private void StopContainer(string container) + private void StopContainer(string container) + { + if (ContainerExists(container)) { - if (ContainerExists(container)) - { - ExecuteWithLogging($"stop {container}", autoRetry: true); - } + ExecuteWithLogging($"stop {container}", autoRetry: true); } + } - private static string Execute( - string args, bool ignoreErrors = false, bool autoRetry = false, ITestOutputHelper outputHelper = null) + private static string Execute( + string args, bool ignoreErrors = false, bool autoRetry = false, ITestOutputHelper outputHelper = null) + { + (Process Process, string StdOut, string StdErr) result = autoRetry ? ExecuteWithRetry(args, outputHelper, ExecuteProcess) : ExecuteProcess(args, outputHelper); + if (!ignoreErrors && result.Process.ExitCode != 0) { - (Process Process, string StdOut, string StdErr) result; - if (autoRetry) - { - result = ExecuteWithRetry(args, outputHelper, ExecuteProcess); - } - else - { - result = ExecuteProcess(args, outputHelper); - } - - if (!ignoreErrors && result.Process.ExitCode != 0) - { - ProcessStartInfo startInfo = result.Process.StartInfo; - string msg = $"Failed to execute {startInfo.FileName} {startInfo.Arguments}" + - $"{Environment.NewLine}Exit code: {result.Process.ExitCode}" + - $"{Environment.NewLine}Standard Error: {result.StdErr}"; - throw new InvalidOperationException(msg); - } - - return result.StdOut; + ProcessStartInfo startInfo = result.Process.StartInfo; + string msg = $"Failed to execute {startInfo.FileName} {startInfo.Arguments}" + + $"{Environment.NewLine}Exit code: {result.Process.ExitCode}" + + $"{Environment.NewLine}Standard Error: {result.StdErr}"; + throw new InvalidOperationException(msg); } - private static (Process Process, string StdOut, string StdErr) ExecuteProcess( - string args, ITestOutputHelper outputHelper) => ExecuteHelper.ExecuteProcess("docker", args, outputHelper); + return result.StdOut; + } - private string ExecuteWithLogging(string args, bool ignoreErrors = false, bool autoRetry = false) - { - Stopwatch stopwatch = new Stopwatch(); - stopwatch.Start(); + private static (Process Process, string StdOut, string StdErr) ExecuteProcess( + string args, ITestOutputHelper outputHelper) => ExecuteHelper.ExecuteProcess("docker", args, outputHelper); - OutputHelper.WriteLine($"Executing: docker {args}"); - string result = Execute(args, outputHelper: OutputHelper, ignoreErrors: ignoreErrors, autoRetry: autoRetry); + private string ExecuteWithLogging(string args, bool ignoreErrors = false, bool autoRetry = false) + { + Stopwatch stopwatch = new Stopwatch(); + stopwatch.Start(); - stopwatch.Stop(); - OutputHelper.WriteLine($"Execution Elapsed Time: {stopwatch.Elapsed}"); + OutputHelper.WriteLine($"Executing: docker {args}"); + string result = Execute(args, outputHelper: OutputHelper, ignoreErrors: ignoreErrors, autoRetry: autoRetry); - return result; - } + stopwatch.Stop(); + OutputHelper.WriteLine($"Execution Elapsed Time: {stopwatch.Elapsed}"); - private static (Process Process, string StdOut, string StdErr) ExecuteWithRetry( - string args, - ITestOutputHelper outputHelper, - Func executor) - { - const int maxRetries = 5; - const int waitFactor = 5; + return result; + } - int retryCount = 0; + private static (Process Process, string StdOut, string StdErr) ExecuteWithRetry( + string args, + ITestOutputHelper outputHelper, + Func executor) + { + const int maxRetries = 5; + const int waitFactor = 5; - (Process Process, string StdOut, string StdErr) result = executor(args, outputHelper); - while (result.Process.ExitCode != 0) - { - retryCount++; - if (retryCount >= maxRetries) - { - break; - } + int retryCount = 0; - int waitTime = Convert.ToInt32(Math.Pow(waitFactor, retryCount - 1)); - if (outputHelper != null) - { - outputHelper.WriteLine($"Retry {retryCount}/{maxRetries}, retrying in {waitTime} seconds..."); - } - - Thread.Sleep(waitTime * 1000); - result = executor(args, outputHelper); + (Process Process, string StdOut, string StdErr) result = executor(args, outputHelper); + while (result.Process.ExitCode != 0) + { + retryCount++; + if (retryCount >= maxRetries) + { + break; } - return result; + int waitTime = Convert.ToInt32(Math.Pow(waitFactor, retryCount - 1)); + outputHelper?.WriteLine($"Retry {retryCount}/{maxRetries}, retrying in {waitTime} seconds..."); + + Thread.Sleep(waitTime * 1000); + result = executor(args, outputHelper); } - private static string GetDockerOS() => Execute("version -f \"{{ .Server.Os }}\""); + return result; + } + + private static string GetDockerOS() => Execute("version -f \"{{ .Server.Os }}\""); - public string GetImageUser(string image) => ExecuteWithLogging($"inspect -f \"{{{{ .Config.User }}}}\" {image}"); + public string GetImageUser(string image) => ExecuteWithLogging($"inspect -f \"{{{{ .Config.User }}}}\" {image}"); - public IDictionary GetEnvironmentVariables(string image) + public IDictionary GetEnvironmentVariables(string image) + { + string envVarsStr = ExecuteWithLogging($"inspect -f \"{{{{json .Config.Env }}}}\" {image}"); + JArray envVarsArray = (JArray)JsonConvert.DeserializeObject(envVarsStr); + return envVarsArray + .ToDictionary( + item => item.ToString().Split('=')[0], + item => item.ToString().Split('=')[1]); + } + + public string GetContainerAddress(string container) + { + string containerAddress = ExecuteWithLogging("inspect -f \"{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}\" " + container); + if (string.IsNullOrWhiteSpace(containerAddress)) { - string envVarsStr = ExecuteWithLogging($"inspect -f \"{{{{json .Config.Env }}}}\" {image}"); - JArray envVarsArray = (JArray)JsonConvert.DeserializeObject(envVarsStr); - return envVarsArray - .ToDictionary( - item => item.ToString().Split('=')[0], - item => item.ToString().Split('=')[1]); + containerAddress = ExecuteWithLogging("inspect -f \"{{.NetworkSettings.Networks.nat.IPAddress }}\" " + container); } - public string GetContainerAddress(string container) - { - string containerAddress = ExecuteWithLogging("inspect -f \"{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}\" " + container); - if (string.IsNullOrWhiteSpace(containerAddress)){ - containerAddress = ExecuteWithLogging("inspect -f \"{{.NetworkSettings.Networks.nat.IPAddress }}\" " + container); - } + return containerAddress; + } - return containerAddress; - } + public string GetContainerHostPort(string container, int containerPort = 80) => + ExecuteWithLogging( + $"inspect -f \"{{{{(index (index .NetworkSettings.Ports \\\"{containerPort}/tcp\\\") 0).HostPort}}}}\" {container}"); - public string GetContainerHostPort(string container, int containerPort = 80) => - ExecuteWithLogging( - $"inspect -f \"{{{{(index (index .NetworkSettings.Ports \\\"{containerPort}/tcp\\\") 0).HostPort}}}}\" {container}"); + public string GetContainerWorkPath(string relativePath) + { + string separator = IsLinuxContainerModeEnabled ? "/" : "\\"; + return $"{ContainerWorkDir}{separator}{relativePath}"; + } - public string GetContainerWorkPath(string relativePath) + public static bool ImageExists(string tag) => ResourceExists("image", tag); + + public void Pull(string image) => ExecuteWithLogging($"pull {image}", autoRetry: true); + + /// + /// Pulls an image from DockerHub, optionally redirecting it through a + /// cache registry. + /// + /// + /// The image to pull, in the format "repo:tag". Since the image is + /// assumed to be from DockerHub, do not include a registry. + /// + /// + /// A tag for the image that was pulled. Use this value to refer to the + /// image in subsequent operations. Do not use the original value of + /// . + /// + public string PullDockerHubImage(string image) + { + if (!string.IsNullOrEmpty(Config.CacheRegistry)) { - string separator = IsLinuxContainerModeEnabled ? "/" : "\\"; - return $"{ContainerWorkDir}{separator}{relativePath}"; + image = $"{Config.CacheRegistry}/{image}"; } - public static bool ImageExists(string tag) => ResourceExists("image", tag); - - public void Pull(string image) => ExecuteWithLogging($"pull {image}", autoRetry: true); - - /// - /// Pulls an image from DockerHub, optionally redirecting it through a - /// cache registry. - /// - /// - /// The image to pull, in the format "repo:tag". Since the image is - /// assumed to be from DockerHub, do not include a registry. - /// - /// - /// A tag for the image that was pulled. Use this value to refer to the - /// image in subsequent operations. Do not use the original value of - /// . - /// - public string PullDockerHubImage(string image) - { - if (!string.IsNullOrEmpty(Config.CacheRegistry)) - { - image = $"{Config.CacheRegistry}/{image}"; - } - - Pull(image); - return image; - } + Pull(image); + return image; + } - public string GetHistory(string image) => - ExecuteWithLogging($"history --no-trunc --format \"{{{{ .CreatedBy }}}}\" {image}"); + public string GetHistory(string image) => + ExecuteWithLogging($"history --no-trunc --format \"{{{{ .CreatedBy }}}}\" {image}"); - private static bool ResourceExists(string type, string filterArg) - { - string output = Execute($"{type} ls -a -q {filterArg}", true); - return output != ""; - } + private static bool ResourceExists(string type, string filterArg) + { + string output = Execute($"{type} ls -a -q {filterArg}", true); + return output != ""; + } - public string Run( - string image, - string name, - string command = null, - string workdir = null, - string optionalRunArgs = null, - bool detach = false, - string runAsUser = null, - bool skipAutoCleanup = false, - bool useMountedDockerSocket = false, - bool silenceOutput = false, - bool tty = true) + public string Run( + string image, + string name, + string command = null, + string workdir = null, + string optionalRunArgs = null, + bool detach = false, + string runAsUser = null, + bool skipAutoCleanup = false, + bool useMountedDockerSocket = false, + bool silenceOutput = false, + bool tty = true) + { + string cleanupArg = skipAutoCleanup ? string.Empty : " --rm"; + string detachArg = detach ? " -d" : string.Empty; + string ttyArg = detach && tty ? " -t" : string.Empty; + string userArg = runAsUser != null ? $" -u {runAsUser}" : string.Empty; + string workdirArg = workdir == null ? string.Empty : $" -w {workdir}"; + string mountedDockerSocketArg = useMountedDockerSocket ? " -v /var/run/docker.sock:/var/run/docker.sock" : string.Empty; + if (silenceOutput) { - string cleanupArg = skipAutoCleanup ? string.Empty : " --rm"; - string detachArg = detach ? " -d" : string.Empty; - string ttyArg = detach && tty ? " -t" : string.Empty; - string userArg = runAsUser != null ? $" -u {runAsUser}" : string.Empty; - string workdirArg = workdir == null ? string.Empty : $" -w {workdir}"; - string mountedDockerSocketArg = useMountedDockerSocket ? " -v /var/run/docker.sock:/var/run/docker.sock" : string.Empty; - if (silenceOutput) - { - return Execute( - $"run --name {name}{cleanupArg}{workdirArg}{userArg}{detachArg}{ttyArg}{mountedDockerSocketArg} {optionalRunArgs} {image} {command}"); - } - return ExecuteWithLogging( + return Execute( $"run --name {name}{cleanupArg}{workdirArg}{userArg}{detachArg}{ttyArg}{mountedDockerSocketArg} {optionalRunArgs} {image} {command}"); } - /// - /// Creates a file system volume that is backed by memory instead of disk. - /// - public string CreateTmpfsVolume(string name, int? ownerUid = null) - { - // Create volume using the local driver (the default driver), - // which accepts options similar to the 'mount' command. - // - // Additional options are specified to: - // - make this volume an in-memory file system with a unique device name (type=tmpfs, device={guid}}). - // - to set the owner of the root of the file system (o=uid=101). - string optionalArgs = string.Empty; - if (ownerUid.HasValue) - { - optionalArgs += $" --opt o=uid={ownerUid.Value}"; - } - string device = Guid.NewGuid().ToString("D"); - return ExecuteWithLogging($"volume create --opt type=tmpfs --opt device={device}{optionalArgs} {name}"); - } + return ExecuteWithLogging( + $"run --name {name}{cleanupArg}{workdirArg}{userArg}{detachArg}{ttyArg}{mountedDockerSocketArg} {optionalRunArgs} {image} {command}"); + } - public string DeleteVolume(string name) + /// + /// Creates a file system volume that is backed by memory instead of disk. + /// + public string CreateTmpfsVolume(string name, int? ownerUid = null) + { + // Create volume using the local driver (the default driver), + // which accepts options similar to the 'mount' command. + // + // Additional options are specified to: + // - make this volume an in-memory file system with a unique device name (type=tmpfs, device={guid}}). + // - to set the owner of the root of the file system (o=uid=101). + string optionalArgs = string.Empty; + if (ownerUid.HasValue) { - return ExecuteWithLogging($"volume remove {name}"); + optionalArgs += $" --opt o=uid={ownerUid.Value}"; } + + string device = Guid.NewGuid().ToString("D"); + return ExecuteWithLogging($"volume create --opt type=tmpfs --opt device={device}{optionalArgs} {name}"); } + + public string DeleteVolume(string name) => ExecuteWithLogging($"volume remove {name}"); } diff --git a/tests/Microsoft.DotNet.Docker.Tests/DockerRunArgsBuilder.cs b/tests/Microsoft.DotNet.Docker.Tests/DockerRunArgsBuilder.cs index 573bffdc6b..85e368d2e0 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/DockerRunArgsBuilder.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/DockerRunArgsBuilder.cs @@ -1,71 +1,63 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. using System.Globalization; using System.Text; -namespace Microsoft.DotNet.Docker.Tests +namespace Microsoft.DotNet.Docker.Tests; + +internal class DockerRunArgsBuilder { - internal class DockerRunArgsBuilder - { - private readonly StringBuilder _builder = - new StringBuilder(); + private readonly StringBuilder _builder = + new StringBuilder(); - private DockerRunArgsBuilder() - { - } + private DockerRunArgsBuilder() + { + } - public static DockerRunArgsBuilder Create() - { - return new DockerRunArgsBuilder(); - } + public static DockerRunArgsBuilder Create() => new DockerRunArgsBuilder(); - public string Build() - { - return _builder.ToString(); - } + public string Build() => _builder.ToString(); - /// - /// Sets the entrypoint of the container. - /// - public DockerRunArgsBuilder Entrypoint(string entrypoint) - { - _builder.AppendFormat(CultureInfo.InvariantCulture, "--entrypoint {0} ", entrypoint); - return this; - } + /// + /// Sets the entrypoint of the container. + /// + public DockerRunArgsBuilder Entrypoint(string entrypoint) + { + _builder.AppendFormat(CultureInfo.InvariantCulture, "--entrypoint {0} ", entrypoint); + return this; + } - /// - /// Sets the named environment variable with the specified value. - /// - public DockerRunArgsBuilder EnvironmentVariable(string name, string value) - { - _builder.AppendFormat(CultureInfo.InvariantCulture, "-e {0}={1} ", name, value); - return this; - } + /// + /// Sets the named environment variable with the specified value. + /// + public DockerRunArgsBuilder EnvironmentVariable(string name, string value) + { + _builder.AppendFormat(CultureInfo.InvariantCulture, "-e {0}={1} ", name, value); + return this; + } - /// - /// Exposes the container port to an arbitrary port on the host. - /// - public DockerRunArgsBuilder ExposePort(int port) - { - _builder.AppendFormat(CultureInfo.InvariantCulture, "-p {0} ", port); - return this; - } + /// + /// Exposes the container port to an arbitrary port on the host. + /// + public DockerRunArgsBuilder ExposePort(int port) + { + _builder.AppendFormat(CultureInfo.InvariantCulture, "-p {0} ", port); + return this; + } - /// - /// Mounts a volume in the container at the specified path. - /// - public DockerRunArgsBuilder VolumeMount(string name, string path) - { - _builder.AppendFormat(CultureInfo.InvariantCulture, "-v {0}:{1} ", name, path); - return this; - } + /// + /// Mounts a volume in the container at the specified path. + /// + public DockerRunArgsBuilder VolumeMount(string name, string path) + { + _builder.AppendFormat(CultureInfo.InvariantCulture, "-v {0}:{1} ", name, path); + return this; + } - public DockerRunArgsBuilder AsUser(int uid) - { - _builder.AppendFormat(CultureInfo.InvariantCulture, "-u {0} ", uid); - return this; - } + public DockerRunArgsBuilder AsUser(int uid) + { + _builder.AppendFormat(CultureInfo.InvariantCulture, "-u {0} ", uid); + return this; } } diff --git a/tests/Microsoft.DotNet.Docker.Tests/DockerfileHelper.cs b/tests/Microsoft.DotNet.Docker.Tests/DockerfileHelper.cs index d42b98fabc..5360fbe195 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/DockerfileHelper.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/DockerfileHelper.cs @@ -1,6 +1,5 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. using System.IO; using System.Text.RegularExpressions; diff --git a/tests/Microsoft.DotNet.Docker.Tests/DockerfileInfo.cs b/tests/Microsoft.DotNet.Docker.Tests/DockerfileInfo.cs index bd800e3bd3..087e02af88 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/DockerfileInfo.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/DockerfileInfo.cs @@ -1,6 +1,5 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. #nullable enable @@ -27,6 +26,9 @@ namespace Microsoft.DotNet.Docker.Tests; /// /// The architecture directory where the Dockerfile is located. /// +/// The manifest repository containing the Dockerfile. +/// The manifest image containing the Dockerfile. +/// The manifest platform describing the Dockerfile. public sealed partial record DockerfileInfo( string RepoDir, string VersionDir, diff --git a/tests/Microsoft.DotNet.Docker.Tests/DotNetImageRepo.cs b/tests/Microsoft.DotNet.Docker.Tests/DotNetImageRepo.cs index 42ff33899d..ae55fe1266 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/DotNetImageRepo.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/DotNetImageRepo.cs @@ -1,6 +1,5 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. using System; @@ -9,12 +8,12 @@ namespace Microsoft.DotNet.Docker.Tests; [Flags] public enum DotNetImageRepo { - SDK = 1 << 0, - Runtime = 1 << 1, - Runtime_Deps = 1 << 2, - Aspnet = 1 << 3, - Monitor = 1 << 4, - Monitor_Base = 1 << 5, + SDK = 1 << 0, + Runtime = 1 << 1, + Runtime_Deps = 1 << 2, + Aspnet = 1 << 3, + Monitor = 1 << 4, + Monitor_Base = 1 << 5, Aspire_Dashboard = 1 << 6, - Yarp = 1 << 7, + Yarp = 1 << 7, } diff --git a/tests/Microsoft.DotNet.Docker.Tests/DotNetImageVariant.cs b/tests/Microsoft.DotNet.Docker.Tests/DotNetImageVariant.cs index 86ff196fd1..60cbad40d6 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/DotNetImageVariant.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/DotNetImageVariant.cs @@ -1,23 +1,21 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. using System; -namespace Microsoft.DotNet.Docker.Tests +namespace Microsoft.DotNet.Docker.Tests; + +/// +/// The possible variants of a .NET container image. +/// +/// +/// The order of values in this enum enforces the order that the variants must appear in tags if there are multiple. +/// +[Flags] +public enum DotNetImageVariant { - /// - /// The possible variants of a .NET container image. - /// - /// - /// The order of values in this enum enforces the order that the variants must appear in tags if there are multiple. - /// - [Flags] - public enum DotNetImageVariant - { - None = 0, - AOT = 1 << 0, - Composite = 1 << 1, - Extra = 1 << 2, - } + None = 0, + AOT = 1 << 0, + Composite = 1 << 1, + Extra = 1 << 2, } diff --git a/tests/Microsoft.DotNet.Docker.Tests/DotNetTheoryAttribute.cs b/tests/Microsoft.DotNet.Docker.Tests/DotNetTheoryAttribute.cs index 6f6aef7819..2c7496f6bc 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/DotNetTheoryAttribute.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/DotNetTheoryAttribute.cs @@ -5,11 +5,10 @@ using Xunit; using Xunit.Sdk; -namespace Microsoft.DotNet.Docker.Tests +namespace Microsoft.DotNet.Docker.Tests; + +[XunitTestCaseDiscoverer("Microsoft.DotNet.Docker.Tests.DotNetTheoryDiscoverer", "Microsoft.DotNet.Docker.Tests")] +[AttributeUsage(AttributeTargets.Method)] +public class DotNetTheoryAttribute : TheoryAttribute { - [XunitTestCaseDiscoverer("Microsoft.DotNet.Docker.Tests.DotNetTheoryDiscoverer", "Microsoft.DotNet.Docker.Tests")] - [AttributeUsage(AttributeTargets.Method)] - public class DotNetTheoryAttribute : TheoryAttribute - { - } } diff --git a/tests/Microsoft.DotNet.Docker.Tests/DotNetTheoryDiscoverer.cs b/tests/Microsoft.DotNet.Docker.Tests/DotNetTheoryDiscoverer.cs index 4ede7d82be..debdc290bd 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/DotNetTheoryDiscoverer.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/DotNetTheoryDiscoverer.cs @@ -7,26 +7,21 @@ using Xunit.Abstractions; using Xunit.Sdk; -namespace Microsoft.DotNet.Docker.Tests +namespace Microsoft.DotNet.Docker.Tests; + +public class DotNetTheoryDiscoverer(IMessageSink diagnosticMessageSink) : TheoryDiscoverer(diagnosticMessageSink) { - public class DotNetTheoryDiscoverer : TheoryDiscoverer + public override IEnumerable Discover(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo theoryAttribute) { - public DotNetTheoryDiscoverer(IMessageSink diagnosticMessageSink) : base(diagnosticMessageSink) - { - } + IEnumerable results = base.Discover(discoveryOptions, testMethod, theoryAttribute); - public override IEnumerable Discover(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo theoryAttribute) + if (results.Count() == 1 && + results.First() is ExecutionErrorTestCase errorTestCase && + errorTestCase.ErrorMessage.StartsWith("No data found for")) { - IEnumerable results = base.Discover(discoveryOptions, testMethod, theoryAttribute); - - if (results.Count() == 1 && - results.First() is ExecutionErrorTestCase errorTestCase && - errorTestCase.ErrorMessage.StartsWith("No data found for")) - { - return CreateTestCasesForSkippedDataRow(discoveryOptions, testMethod, theoryAttribute, Array.Empty(), errorTestCase.ErrorMessage); - } - - return results; + return CreateTestCasesForSkippedDataRow(discoveryOptions, testMethod, theoryAttribute, [], errorTestCase.ErrorMessage); } + + return results; } } diff --git a/tests/Microsoft.DotNet.Docker.Tests/EnvironmentVariableInfo.cs b/tests/Microsoft.DotNet.Docker.Tests/EnvironmentVariableInfo.cs index 20fb67fe04..6926fed0c7 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/EnvironmentVariableInfo.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/EnvironmentVariableInfo.cs @@ -1,96 +1,89 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. using System.Collections.Generic; using FluentAssertions; using FluentAssertions.Execution; #nullable enable -namespace Microsoft.DotNet.Docker.Tests +namespace Microsoft.DotNet.Docker.Tests; + +public record EnvironmentVariableInfo { - public record EnvironmentVariableInfo - { - public bool AllowAnyValue { get; init; } = false; - public string? ExpectedValue { get; init; } = null; - public string Name { get; init; } - public bool IsProductVersion { get; init; } = false; + public bool AllowAnyValue { get; init; } = false; + public string? ExpectedValue { get; init; } = null; + public string Name { get; init; } + public bool IsProductVersion { get; init; } = false; - /// - /// When true, the variable is expected to NOT be set on the image. - /// - public bool ShouldNotExist { get; init; } = false; + /// + /// When true, the variable is expected to NOT be set on the image. + /// + public bool ShouldNotExist { get; init; } = false; - private EnvironmentVariableInfo(string name) - { - Name = name; - } + private EnvironmentVariableInfo(string name) => Name = name; - /// - /// Requires the named environment variable to be set to . - /// - public static EnvironmentVariableInfo Require(string name, string expectedValue) => - new(name) { ExpectedValue = expectedValue }; + /// + /// Requires the named environment variable to be set to . + /// + public static EnvironmentVariableInfo Require(string name, string expectedValue) => + new(name) { ExpectedValue = expectedValue }; - /// - /// Requires the named environment variable to be set to any non-empty value. - /// - public static EnvironmentVariableInfo Require(string name) => - new(name) { AllowAnyValue = true }; + /// + /// Requires the named environment variable to be set to any non-empty value. + /// + public static EnvironmentVariableInfo Require(string name) => + new(name) { AllowAnyValue = true }; - /// - /// Requires the named environment variable to NOT be set on the image. - /// - public static EnvironmentVariableInfo Forbid(string name) => - new(name) { ShouldNotExist = true }; + /// + /// Requires the named environment variable to NOT be set on the image. + /// + public static EnvironmentVariableInfo Forbid(string name) => + new(name) { ShouldNotExist = true }; - public static void Validate( - IEnumerable expectedVariables, - string imageName, - ImageData imageData, - DockerHelper dockerHelper) - { - IDictionary environmentVariables = dockerHelper.GetEnvironmentVariables(imageName); + public static void Validate( + IEnumerable expectedVariables, + string imageName, + DockerHelper dockerHelper) + { + IDictionary environmentVariables = dockerHelper.GetEnvironmentVariables(imageName); - using (new AssertionScope()) + using (new AssertionScope()) + { + foreach (EnvironmentVariableInfo variable in expectedVariables) { - foreach (EnvironmentVariableInfo variable in expectedVariables) + if (variable.ShouldNotExist) { - if (variable.ShouldNotExist) - { - environmentVariables.Should().NotContainKey( - variable.Name, - because: $"{imageName} should not have the environment variable '{variable.Name}' defined"); - continue; - } + environmentVariables.Should().NotContainKey( + variable.Name, + because: $"{imageName} should not have the environment variable '{variable.Name}' defined"); + continue; + } - string environmentVariable = environmentVariables.Should() - .ContainKey( - variable.Name, - because: $"{imageName} should have the environment variable '{variable.Name}' defined") - .WhoseValue; + string environmentVariable = environmentVariables.Should() + .ContainKey( + variable.Name, + because: $"{imageName} should have the environment variable '{variable.Name}' defined") + .WhoseValue; - if (variable.AllowAnyValue) + if (variable.AllowAnyValue) + { + environmentVariable.Should().NotBeNullOrEmpty( + because: $"environment variable {variable.Name} is allowed to have any value"); + } + else + { + // If we're validating a product version environment variable for a stable build + // we need to trim off the "servicing" or "rtm" part of the version value. + if (variable.IsProductVersion && Config.IsInternal) { - environmentVariable.Should().NotBeNullOrEmpty( - because: $"environment variable {variable.Name} is allowed to have any value"); + environmentVariable = ImageVersion.TrimBuildVersionForRelease(environmentVariable); } - else - { - // If we're validating a product version environment variable for a stable build - // we need to trim off the "servicing" or "rtm" part of the version value. - if (variable.IsProductVersion && Config.IsInternal) - { - environmentVariable = ImageVersion.TrimBuildVersionForRelease(environmentVariable); - } - environmentVariable.Should().Be(variable.ExpectedValue, - because: $"{imageName} should have the environment variable " - + $"'{variable.Name}' set to '{variable.ExpectedValue}'"); - } + environmentVariable.Should().Be(variable.ExpectedValue, + because: $"{imageName} should have the environment variable " + + $"'{variable.Name}' set to '{variable.ExpectedValue}'"); } } - } } } diff --git a/tests/Microsoft.DotNet.Docker.Tests/ExecuteHelper.cs b/tests/Microsoft.DotNet.Docker.Tests/ExecuteHelper.cs index 21ec732f3a..f8fb686f4c 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/ExecuteHelper.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/ExecuteHelper.cs @@ -1,54 +1,52 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. using System.Diagnostics; using System.Text; using Xunit.Abstractions; -namespace Microsoft.DotNet.Docker.Tests +namespace Microsoft.DotNet.Docker.Tests; + +public static class ExecuteHelper { - public static class ExecuteHelper + public static (Process Process, string StdOut, string StdErr) ExecuteProcess( + string fileName, string args, ITestOutputHelper outputHelper) { - public static (Process Process, string StdOut, string StdErr) ExecuteProcess( - string fileName, string args, ITestOutputHelper outputHelper) + Process process = new Process { - Process process = new Process - { - EnableRaisingEvents = true, - StartInfo = - { - FileName = fileName, - Arguments = args, - RedirectStandardOutput = true, - RedirectStandardError = true, - } - }; - - StringBuilder stdOutput = new StringBuilder(); - process.OutputDataReceived += new DataReceivedEventHandler((sender, e) => stdOutput.AppendLine(e.Data)); - - StringBuilder stdError = new StringBuilder(); - process.ErrorDataReceived += new DataReceivedEventHandler((sender, e) => stdError.AppendLine(e.Data)); - - process.Start(); - process.BeginOutputReadLine(); - process.BeginErrorReadLine(); - process.WaitForExit(); - - string output = stdOutput.ToString().Trim(); - if (outputHelper != null && !string.IsNullOrWhiteSpace(output)) + EnableRaisingEvents = true, + StartInfo = { - outputHelper.WriteLine(output); + FileName = fileName, + Arguments = args, + RedirectStandardOutput = true, + RedirectStandardError = true, } + }; - string error = stdError.ToString().Trim(); - if (outputHelper != null && !string.IsNullOrWhiteSpace(error)) - { - outputHelper.WriteLine(error); - } + StringBuilder stdOutput = new StringBuilder(); + process.OutputDataReceived += new DataReceivedEventHandler((sender, e) => stdOutput.AppendLine(e.Data)); + + StringBuilder stdError = new StringBuilder(); + process.ErrorDataReceived += new DataReceivedEventHandler((sender, e) => stdError.AppendLine(e.Data)); + + process.Start(); + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + process.WaitForExit(); - return (process, output, error); + string output = stdOutput.ToString().Trim(); + if (outputHelper != null && !string.IsNullOrWhiteSpace(output)) + { + outputHelper.WriteLine(output); } + + string error = stdError.ToString().Trim(); + if (outputHelper != null && !string.IsNullOrWhiteSpace(error)) + { + outputHelper.WriteLine(error); + } + + return (process, output, error); } } diff --git a/tests/Microsoft.DotNet.Docker.Tests/Extensions/ShouldlyExtensions.cs b/tests/Microsoft.DotNet.Docker.Tests/Extensions/ShouldlyExtensions.cs index a50a6ca63d..1365c077e3 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/Extensions/ShouldlyExtensions.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/Extensions/ShouldlyExtensions.cs @@ -1,6 +1,5 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. using System; using System.Collections.Generic; diff --git a/tests/Microsoft.DotNet.Docker.Tests/FileHelper.cs b/tests/Microsoft.DotNet.Docker.Tests/FileHelper.cs index fcedd66b0b..8432db3a27 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/FileHelper.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/FileHelper.cs @@ -1,127 +1,116 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. using System; -using System.IO; using System.Diagnostics; +using System.IO; using Xunit.Abstractions; -namespace Microsoft.DotNet.Docker.Tests +namespace Microsoft.DotNet.Docker.Tests; + +public static class FileHelper { - public static class FileHelper + public static TempFolderContext UseTempFolder() => new TempFolderContext(); + + public static TempFileContext UseTempFile(string name = null) => new TempFileContext(name); + + /// + /// Returns a value indicating whether the two files are equal. + /// + public static bool CompareFiles(string expectedFilePath, string actualFilePath, ITestOutputHelper outputHelper) { - public static TempFolderContext UseTempFolder() + if (!File.Exists(expectedFilePath)) { - return new TempFolderContext(); + throw new FileNotFoundException($"Expected file not found: {expectedFilePath}"); } - public static TempFileContext UseTempFile(string name = null) + if (!File.Exists(actualFilePath)) { - return new TempFileContext(name); + throw new FileNotFoundException($"Actual file not found: {actualFilePath}"); } - /// - /// Returns a value indicating whether the two files are equal. - /// - public static bool CompareFiles(string expectedFilePath, string actualFilePath, ITestOutputHelper outputHelper) - { - if (!File.Exists(expectedFilePath)) - { - throw new FileNotFoundException($"Expected file not found: {expectedFilePath}"); - } + string diffOutput = DiffFiles(expectedFilePath, actualFilePath, outputHelper); - if (!File.Exists(actualFilePath)) - { - throw new FileNotFoundException($"Actual file not found: {actualFilePath}"); - } + if (!string.IsNullOrEmpty(diffOutput)) + { + return false; + } - string diffOutput = DiffFiles(expectedFilePath, actualFilePath, outputHelper); + return true; + } - if (!string.IsNullOrEmpty(diffOutput)) - { - return false; - } + /// + /// Runs a git diff of two files and returns the output. + /// + public static string DiffFiles(string file1Path, string file2Path, ITestOutputHelper outputHelper) + { + (Process Process, string StdOut, string StdErr) diffResult = + ExecuteHelper.ExecuteProcess("git", $"diff --no-index {file1Path} {file2Path}", outputHelper); - return true; - } + return diffResult.StdOut; + } +} - /// - /// Runs a git diff of two files and returns the output. - /// - public static string DiffFiles(string file1Path, string file2Path, ITestOutputHelper outputHelper) +public sealed class TempFolderContext : IDisposable +{ + public TempFolderContext() + { + do { - (Process Process, string StdOut, string StdErr) diffResult = - ExecuteHelper.ExecuteProcess("git", $"diff --no-index {file1Path} {file2Path}", outputHelper); - - return diffResult.StdOut; + Path = System.IO.Path.Combine( + System.IO.Path.GetTempPath(), + Guid.NewGuid().ToString()); } + while (Directory.Exists(Path)); + + Directory.CreateDirectory(Path); } - public sealed class TempFolderContext : IDisposable + public string Path { get; } + + public void Dispose() => Directory.Delete(Path, true); +} + +public sealed class TempFileContext : IDisposable +{ + private readonly string _folder; + + public TempFileContext(string name = null) { - public TempFolderContext() + string folder; + do { - do - { - Path = System.IO.Path.Combine( - System.IO.Path.GetTempPath(), - Guid.NewGuid().ToString()); - } - while (Directory.Exists(Path)); - - Directory.CreateDirectory(Path); + folder = System.IO.Path.Combine( + System.IO.Path.GetTempPath(), + Guid.NewGuid().ToString()); } + while (Directory.Exists(folder)); - public string Path { get; } - - public void Dispose() + if (name is not null) { - Directory.Delete(Path, true); + _folder = folder; + Directory.CreateDirectory(folder); + Path = System.IO.Path.Combine(folder, name); } + else + { + Path = folder; + } + + File.Create(Path).Dispose(); } - public sealed class TempFileContext : IDisposable - { - private readonly string _folder; + public string Path { get; } - public TempFileContext(string name = null) + public void Dispose() + { + if (_folder is not null) { - string folder; - do - { - folder = System.IO.Path.Combine( - System.IO.Path.GetTempPath(), - Guid.NewGuid().ToString()); - } - while (Directory.Exists(folder)); - - if (name is not null) - { - _folder = folder; - Directory.CreateDirectory(folder); - Path = System.IO.Path.Combine(folder, name); - } - else - { - Path = folder; - } - - File.Create(Path).Dispose(); + Directory.Delete(_folder, true); } - - public string Path { get; } - - public void Dispose() + else { - if (_folder is not null) - { - Directory.Delete(_folder, true); - } - else - { - File.Delete(Path); - } + File.Delete(Path); } } } diff --git a/tests/Microsoft.DotNet.Docker.Tests/GeneratedArtifactTests.cs b/tests/Microsoft.DotNet.Docker.Tests/GeneratedArtifactTests.cs index 7e7e052fe1..c276800efd 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/GeneratedArtifactTests.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/GeneratedArtifactTests.cs @@ -1,6 +1,5 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. using System; using System.Collections.Generic; @@ -18,35 +17,30 @@ namespace Microsoft.DotNet.Docker.Tests; [Trait("Category", "pre-build")] -public class GeneratedArtifactTests +public class GeneratedArtifactTests(ITestOutputHelper outputHelper) { - private static readonly string s_generateDockerfilesScript = + private static readonly string GenerateDockerfilesScript = Path.Combine(Config.SourceRepoRoot, "eng", "dockerfile-templates", "Get-GeneratedDockerfiles.ps1"); - private static readonly string s_generateTagsDocumentationScript = + private static readonly string GenerateTagsDocumentationScript = Path.Combine(Config.SourceRepoRoot, "eng", "readme-templates", "Get-GeneratedReadmes.ps1"); - private ITestOutputHelper OutputHelper { get; } - - public GeneratedArtifactTests(ITestOutputHelper outputHelper) - { - OutputHelper = outputHelper; - } + private ITestOutputHelper OutputHelper { get; } = outputHelper; [Fact] public void VerifyDockerfileTemplates() { - ValidateGeneratedArtifacts(s_generateDockerfilesScript, + ValidateGeneratedArtifacts(GenerateDockerfilesScript, $"The Dockerfiles are out of sync with the templates." + - $"Update the Dockerfiles by running `{s_generateDockerfilesScript}`."); + $"Update the Dockerfiles by running `{GenerateDockerfilesScript}`."); } [Fact] public void VerifyReadmeTemplates() { - ValidateGeneratedArtifacts(s_generateTagsDocumentationScript, + ValidateGeneratedArtifacts(GenerateTagsDocumentationScript, $"The Readmes are out of sync with the templates." + - $"Update the Readmes by running `{s_generateTagsDocumentationScript}`."); + $"Update the Readmes by running `{GenerateTagsDocumentationScript}`."); } [Fact] @@ -60,26 +54,26 @@ public void VerifyInternalDockerfilesOutput() return; } - const string InternalDockerfilesSubfolder = "Baselines"; + const string internalDockerfilesSubfolder = "Baselines"; using TempFolderContext outputDirectory = FileHelper.UseTempFolder(); string dockerfilesOutputDirectory = Path.Combine(outputDirectory.Path, "src"); - string errorMessage = $"Failed to generate Dockerfiles using `{s_generateDockerfilesScript}`."; + string errorMessage = $"Failed to generate Dockerfiles using `{GenerateDockerfilesScript}`."; // Override base URLs to make templates generate internal versions of Dockerfiles - const string InternalBaseUrl = "https://dotnetstage.blob.core.windows.net"; + const string internalBaseUrl = "https://dotnetstage.blob.core.windows.net"; string customImageBuilderArgs = - $" --var 'base-url|public|maintenance|main={InternalBaseUrl}'" + - $" --var 'base-url|public|maintenance|nightly={InternalBaseUrl}'" + - $" --var 'base-url|public|preview|main={InternalBaseUrl}'" + - $" --var 'base-url|public|preview|nightly={InternalBaseUrl}'" + - $" --var 'base-url|public-checksums|maintenance|main={InternalBaseUrl}'" + - $" --var 'base-url|public-checksums|maintenance|nightly={InternalBaseUrl}'" + - $" --var 'base-url|public-checksums|preview|main={InternalBaseUrl}'" + - $" --var 'base-url|public-checksums|preview|nightly={InternalBaseUrl}'"; + $" --var 'base-url|public|maintenance|main={internalBaseUrl}'" + + $" --var 'base-url|public|maintenance|nightly={internalBaseUrl}'" + + $" --var 'base-url|public|preview|main={internalBaseUrl}'" + + $" --var 'base-url|public|preview|nightly={internalBaseUrl}'" + + $" --var 'base-url|public-checksums|maintenance|main={internalBaseUrl}'" + + $" --var 'base-url|public-checksums|maintenance|nightly={internalBaseUrl}'" + + $" --var 'base-url|public-checksums|preview|main={internalBaseUrl}'" + + $" --var 'base-url|public-checksums|preview|nightly={internalBaseUrl}'"; // PowerShell's internal storage uses a different layout than its public storage - const string InternalPowerShellBaseUrl = "https://pscoretestdata.blob.core.windows.net"; + const string internalPowerShellBaseUrl = "https://pscoretestdata.blob.core.windows.net"; foreach (Match match in Config.GetVariableNames() .Select(name => Regex.Match(name, @"^powershell\|(?[\d.]+)\|base-url$")) .Where(match => match.Success)) @@ -87,13 +81,13 @@ public void VerifyInternalDockerfilesOutput() string dotnetVersion = match.Groups["dotnetVersion"].Value; string powershellVersion = Config.GetVariableValue($"powershell|{dotnetVersion}|build-version"); string internalUrl = - $"{InternalPowerShellBaseUrl}/v{powershellVersion.Replace('.', '-')}-nuget/globaltool"; + $"{internalPowerShellBaseUrl}/v{powershellVersion.Replace('.', '-')}-nuget/globaltool"; customImageBuilderArgs += $" --var '{match.Value}={internalUrl}'"; } // Generate internal Dockerfiles ExecuteScript( - s_generateDockerfilesScript, + GenerateDockerfilesScript, $"-Output {outputDirectory.Path} -CustomImageBuilderArgs \"{customImageBuilderArgs}\"", errorMessage); @@ -124,7 +118,7 @@ string DockerfileScrubber(string content) string GetDiscriminator(string dockerfilePath) => dockerfilePath - .Remove(0, dockerfilesOutputDirectory.Length + 1) +[(dockerfilesOutputDirectory.Length + 1)..] .Replace('/', '-') .Replace('\\', '-'); @@ -141,7 +135,7 @@ string MatchTestFilenameGenerator( $"{discriminator[1..]}.{fileType}.{fileExtension}"); Action Configure(string dockerfilePath) => - (ShouldMatchConfigurationBuilder config) => + config => { config // Don't launch diff tool automatically since we have a large number of files @@ -149,7 +143,7 @@ Action Configure(string dockerfilePath) => // Use the caller location (this method) for the test method name, // since we perform the comparison inside an extension method .UseCallerLocation() - .SubFolder(InternalDockerfilesSubfolder) + .SubFolder(internalDockerfilesSubfolder) // Discriminator determines the name of the approved files, // which needs to be unique per Dockerfile .WithDiscriminator(GetDiscriminator(dockerfilePath)) @@ -162,10 +156,7 @@ Action Configure(string dockerfilePath) => path => File.ReadAllText(path).ShouldMatchApproved(configureOptions: Configure(path))); } - private void ValidateGeneratedArtifacts(string generateScriptPath, string errorMessage) - { - ExecuteScript(generateScriptPath, "-Validate", errorMessage); - } + private void ValidateGeneratedArtifacts(string generateScriptPath, string errorMessage) => ExecuteScript(generateScriptPath, "-Validate", errorMessage); private void ExecuteScript(string generateScriptPath, string arguments, string errorMessage) { diff --git a/tests/Microsoft.DotNet.Docker.Tests/HttpClientExtensions.cs b/tests/Microsoft.DotNet.Docker.Tests/HttpClientExtensions.cs index cdc920afab..e81bae3d34 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/HttpClientExtensions.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/HttpClientExtensions.cs @@ -6,15 +6,14 @@ using System.Net.Http; using System.Threading.Tasks; -namespace Microsoft.DotNet.Docker.Tests +namespace Microsoft.DotNet.Docker.Tests; + +internal static class HttpClientExtensions { - internal static class HttpClientExtensions + public static async Task DownloadFileAsync(this HttpClient client, Uri uri, string path) { - public static async Task DownloadFileAsync(this HttpClient client, Uri uri, string path) - { - using Stream stream = await client.GetStreamAsync(uri); - using FileStream fileStream = new(path, FileMode.OpenOrCreate); - await stream.CopyToAsync(fileStream); - } + using Stream stream = await client.GetStreamAsync(uri); + using FileStream fileStream = new(path, FileMode.OpenOrCreate); + await stream.CopyToAsync(fileStream); } } diff --git a/tests/Microsoft.DotNet.Docker.Tests/ImageData.cs b/tests/Microsoft.DotNet.Docker.Tests/ImageData.cs index 2e6fd768dc..44797e80bd 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/ImageData.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/ImageData.cs @@ -1,6 +1,5 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. using System; using System.Collections.Generic; @@ -12,195 +11,187 @@ using Newtonsoft.Json.Linq; using Xunit; -namespace Microsoft.DotNet.Docker.Tests +namespace Microsoft.DotNet.Docker.Tests; + +public abstract record ImageData { - public abstract record ImageData - { - private readonly List _pulledImages = new List(); + private readonly List _pulledImages = []; - public Arch Arch { get; set; } - public bool IsArm => Arch == Arch.Arm || Arch == Arch.Arm64; - public OSInfo OS { get; set; } - public bool IsDistroless => OS.IsDistroless; - public virtual int DefaultPort => 8080; - public virtual int? NonRootUID => IsWindows ? null : 1654; + public Arch Arch { get; set; } + public bool IsArm => Arch is Arch.Arm or Arch.Arm64; + public OSInfo OS { get; set; } + public bool IsDistroless => OS.IsDistroless; + public virtual int DefaultPort => 8080; + public virtual int? NonRootUID => IsWindows ? null : 1654; - private static readonly Lazy s_imageInfoData; + private static readonly Lazy ImageInfoData; - static ImageData() + static ImageData() + { + ImageInfoData = new Lazy(() => { - s_imageInfoData = new Lazy(() => + string imageInfoPath = Environment.GetEnvironmentVariable("IMAGE_INFO_PATH"); + if (!string.IsNullOrEmpty(imageInfoPath)) { - string imageInfoPath = Environment.GetEnvironmentVariable("IMAGE_INFO_PATH"); - if (!string.IsNullOrEmpty(imageInfoPath)) - { - string imageInfoContents = File.ReadAllText(imageInfoPath); - return JsonConvert.DeserializeObject(imageInfoContents); - } + string imageInfoContents = File.ReadAllText(imageInfoPath); + return JsonConvert.DeserializeObject(imageInfoContents); + } - return null; - }); - } + return null; + }); + } - public string Platform + public string Platform + { + get { - get + string os = IsWindows ? "windows" : "linux"; + string arch = Arch.ToString().ToLowerInvariant(); + if (ArchVariant.Length > 0) { - string os = IsWindows ? "windows" : "linux"; - string arch = Arch.ToString().ToLowerInvariant(); - if (ArchVariant.Length > 0) - { - arch += $"/{ArchVariant}"; - } - - return $"{os}/{arch}"; + arch += $"/{ArchVariant}"; } + + return $"{os}/{arch}"; } + } - public string ArchVariant => - Arch switch - { - Arch.Amd64 => string.Empty, - Arch.Arm => "v7", - Arch.Arm64 => "v8", - _ => throw new NotImplementedException() - }; + public string ArchVariant => + Arch switch + { + Arch.Amd64 => string.Empty, + Arch.Arm => "v7", + Arch.Arm64 => "v8", + _ => throw new NotImplementedException() + }; - public bool IsWindows => OS.IsWindows; + public bool IsWindows => OS.IsWindows; - public string Rid + public string Rid + { + get { - get - { - string rid; + string rid; - if (IsWindows) - { - rid = "win-x64"; - } - else + if (IsWindows) + { + rid = "win-x64"; + } + else + { + string arch = Arch switch { - string arch = Arch switch - { - Arch.Arm => "arm", - Arch.Arm64 => "arm64", - Arch.Amd64 => "x64", - _ => throw new NotImplementedException() - }; - string modifier = OS.Family == OSFamily.Alpine ? "musl-" : ""; - rid = $"linux-{modifier}{arch}"; - } - - return rid; + Arch.Arm => "arm", + Arch.Arm64 => "arm64", + Arch.Amd64 => "x64", + _ => throw new NotImplementedException() + }; + string modifier = OS.Family == OSFamily.Alpine ? "musl-" : ""; + rid = $"linux-{modifier}{arch}"; } + + return rid; } + } - public string OsVersion + public string OsVersion + { + get { - get - { - const string PrefixGroup = "Prefix"; - const string VersionGroup = "Version"; - const string LtscPrefix = "ltsc"; - string versionNumber = string.Empty; - Match match = Regex.Match(OS, @$"(-(?<{PrefixGroup}>[a-zA-Z_]*))?(?<{VersionGroup}>\d+.\d+)"); - - if (match.Groups[PrefixGroup].Success && match.Groups[PrefixGroup].Value == LtscPrefix) - { - versionNumber = LtscPrefix; - } + const string prefixGroup = "Prefix"; + const string versionGroup = "Version"; + const string ltscPrefix = "ltsc"; + string versionNumber = string.Empty; + Match match = Regex.Match(OS, @$"(-(?<{prefixGroup}>[a-zA-Z_]*))?(?<{versionGroup}>\d+.\d+)"); - versionNumber += match.Groups[VersionGroup].Value; - return versionNumber; + if (match.Groups[prefixGroup].Success && match.Groups[prefixGroup].Value == ltscPrefix) + { + versionNumber = ltscPrefix; } + + versionNumber += match.Groups[versionGroup].Value; + return versionNumber; } + } - public virtual string GetIdentifier(string type) => GenerateContainerName(type); + public virtual string GetIdentifier(string type) => GenerateContainerName(type); - public static string GenerateContainerName(string prefix) => $"{prefix}-{DateTime.Now.ToFileTime()}"; + public static string GenerateContainerName(string prefix) => $"{prefix}-{DateTime.Now.ToFileTime()}"; - protected void PullImageIfNecessary(string imageName, DockerHelper dockerHelper, bool allowPull = false) + protected void PullImageIfNecessary(string imageName, DockerHelper dockerHelper, bool allowPull = false) + { + if ((Config.PullImages || allowPull) && !_pulledImages.Contains(imageName)) { - if ((Config.PullImages || allowPull) && !_pulledImages.Contains(imageName)) - { - dockerHelper.Pull(imageName); - _pulledImages.Add(imageName); - } - else - { - Assert.True(DockerHelper.ImageExists(imageName), $"`{imageName}` could not be found on disk."); - } + dockerHelper.Pull(imageName); + _pulledImages.Add(imageName); } - - public static string GetRepoNameModifier() => $"{(Config.IsNightlyRepo ? "/nightly" : string.Empty)}"; - - public static string GetImageName(string tag, string repoName, string repoNameModifier = null) + else { - string repo = GetRepoName(repoName, repoNameModifier); - string registry = GetRegistryName(repo, tag); - - return $"{registry}{repo}:{tag}"; + Assert.True(DockerHelper.ImageExists(imageName), $"`{imageName}` could not be found on disk."); } + } - public static string GetRepoName(string repoName, string repoNameModifier = null) => - repoName == "aspire-dashboard" - ? $"aspire{repoNameModifier ?? GetRepoNameModifier()}/dashboard" - : $"dotnet{repoNameModifier ?? GetRepoNameModifier()}/{repoName}"; + public static string GetRepoNameModifier() => $"{(Config.IsNightlyRepo ? "/nightly" : string.Empty)}"; - protected string GetTagName(string tagPrefix, string os, string tagPostfix = null) - { - IEnumerable tagParts = [ tagPrefix, os, tagPostfix, GetArchTagSuffix() ]; - tagParts = tagParts.Where(s => !string.IsNullOrEmpty(s)); - return string.Join('-', tagParts); - } + public static string GetImageName(string tag, string repoName, string repoNameModifier = null) + { + string repo = GetRepoName(repoName, repoNameModifier); + string registry = GetRegistryName(repo, tag); - protected virtual string GetArchTagSuffix() => (Arch == Arch.Amd64 && IsWindows) - ? string.Empty - : GetArchLabel(); + return $"{registry}{repo}:{tag}"; + } - protected string GetArchLabel() => - Arch switch - { - Arch.Amd64 => "amd64", - Arch.Arm => "arm32v7", - Arch.Arm64 => "arm64v8", - _ => throw new NotSupportedException($"Unsupported architecture '{Arch}'") - }; + public static string GetRepoName(string repoName, string repoNameModifier = null) => + repoName == "aspire-dashboard" + ? $"aspire{repoNameModifier ?? GetRepoNameModifier()}/dashboard" + : $"dotnet{repoNameModifier ?? GetRepoNameModifier()}/{repoName}"; - private static string GetRegistryName(string repo, string tag) - { - bool imageExistsInStaging = true; + protected string GetTagName(string tagPrefix, string os, string tagPostfix = null) + { + IEnumerable tagParts = [tagPrefix, os, tagPostfix, GetArchTagSuffix()]; + tagParts = tagParts.Where(s => !string.IsNullOrEmpty(s)); + return string.Join('-', tagParts); + } - // In the case of running this in a local development environment, there would likely be no image info file - // provided. In that case, the assumption is that the images exist in the staging location. + protected virtual string GetArchTagSuffix() => (Arch == Arch.Amd64 && IsWindows) + ? string.Empty + : GetArchLabel(); - if (ImageData.s_imageInfoData.Value != null) - { - JObject repoInfo = (JObject)ImageData.s_imageInfoData.Value - .Value("repos") - .FirstOrDefault(imageInfoRepo => imageInfoRepo["repo"].ToString() == repo); + protected string GetArchLabel() => + Arch switch + { + Arch.Amd64 => "amd64", + Arch.Arm => "arm32v7", + Arch.Arm64 => "arm64v8", + _ => throw new NotSupportedException($"Unsupported architecture '{Arch}'") + }; - if (repoInfo?["images"] != null) - { - imageExistsInStaging = repoInfo.Value("images") - .SelectMany(imageInfo => imageInfo.Value("platforms")) - .Cast() - .Any(platformInfo => platformInfo.Value("simpleTags")?.Any(imageTag => imageTag.ToString() == tag) == true); - } - else - { - imageExistsInStaging = false; - } - } + private static string GetRegistryName(string repo, string tag) + { + bool imageExistsInStaging = true; - return imageExistsInStaging ? $"{Config.Registry}/{Config.RepoPrefix}" : "mcr.microsoft.com/"; - } + // In the case of running this in a local development environment, there would likely be no image info file + // provided. In that case, the assumption is that the images exist in the staging location. - public override string ToString() + if (ImageInfoData.Value != null) { - return GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy) - .Select(propInfo => $"{propInfo.Name}='{propInfo.GetValue(this) ?? ""}'") - .Aggregate((working, next) => $"{working}, {next}"); + JObject repoInfo = (JObject)ImageInfoData.Value + .Value("repos") + .FirstOrDefault(imageInfoRepo => imageInfoRepo["repo"].ToString() == repo); + + imageExistsInStaging = repoInfo?["images"] != null && repoInfo.Value("images") + .SelectMany(imageInfo => imageInfo.Value("platforms")) + .Cast() + .Any(platformInfo => platformInfo.Value("simpleTags")?.Any(imageTag => imageTag.ToString() == tag) == true); } + + return imageExistsInStaging ? $"{Config.Registry}/{Config.RepoPrefix}" : "mcr.microsoft.com/"; + } + + public override string ToString() + { + return GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy) + .Select(propInfo => $"{propInfo.Name}='{propInfo.GetValue(this) ?? ""}'") + .Aggregate((working, next) => $"{working}, {next}"); } } diff --git a/tests/Microsoft.DotNet.Docker.Tests/ImageVersion.cs b/tests/Microsoft.DotNet.Docker.Tests/ImageVersion.cs index f4fcdc674f..30943834b8 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/ImageVersion.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/ImageVersion.cs @@ -1,53 +1,51 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. using System; -namespace Microsoft.DotNet.Docker.Tests +namespace Microsoft.DotNet.Docker.Tests; + +public readonly record struct ImageVersion { - public readonly record struct ImageVersion + private readonly Version _version; + + public static readonly ImageVersion V8_0 = new(new Version(8, 0), isPreview: false); + public static readonly ImageVersion V8_1 = new(new Version(8, 1), isPreview: false); + public static readonly ImageVersion V9_0 = new(new Version(9, 0), isPreview: false); + public static readonly ImageVersion V9_1 = new(new Version(9, 1), isPreview: false); + public static readonly ImageVersion V17_0 = new(new Version(17, 0), isPreview: false); + public static readonly ImageVersion V9_2_Preview = new(new Version(9, 2), isPreview: true); + public static readonly ImageVersion V10_0 = new(new Version(10, 0), isPreview: false); + public static readonly ImageVersion V11_0 = new(new Version(11, 0), isPreview: false); + + public ImageVersion(Version version, bool isPreview) { - private readonly Version _version; - - public static readonly ImageVersion V8_0 = new(new Version(8, 0), isPreview: false); - public static readonly ImageVersion V8_1 = new(new Version(8, 1), isPreview: false); - public static readonly ImageVersion V9_0 = new(new Version(9, 0), isPreview: false); - public static readonly ImageVersion V9_1 = new(new Version(9, 1), isPreview: false); - public static readonly ImageVersion V17_0 = new(new Version(17, 0), isPreview: false); - public static readonly ImageVersion V9_2_Preview = new(new Version(9, 2), isPreview: true); - public static readonly ImageVersion V10_0 = new(new Version(10, 0), isPreview: false); - public static readonly ImageVersion V11_0 = new(new Version(11, 0), isPreview: false); - - public ImageVersion(Version version, bool isPreview) - { - _version = version; - IsPreview = isPreview; - } + _version = version; + IsPreview = isPreview; + } - public int Major => _version.Major; + public int Major => _version.Major; - public bool IsPreview { get; } + public bool IsPreview { get; } - public override string ToString() => _version.ToString(); + public override string ToString() => _version.ToString(); - public string GetTagName() => ToString() + (IsPreview ? "-preview" : string.Empty); + public string GetTagName() => ToString() + (IsPreview ? "-preview" : string.Empty); - public static string TrimBuildVersionForRelease(string buildVersion) + public static string TrimBuildVersionForRelease(string buildVersion) + { + int servicingIndex = buildVersion.IndexOf("-servicing."); + if (servicingIndex != -1) { - int servicingIndex = buildVersion.IndexOf("-servicing."); - if (servicingIndex != -1) - { - buildVersion = buildVersion.Substring(0, servicingIndex); - } - - int rtmIndex = buildVersion.IndexOf("-rtm."); - if (rtmIndex != -1) - { - buildVersion = buildVersion.Substring(0, rtmIndex); - } - - return buildVersion; + buildVersion = buildVersion[..servicingIndex]; } + + int rtmIndex = buildVersion.IndexOf("-rtm."); + if (rtmIndex != -1) + { + buildVersion = buildVersion[..rtmIndex]; + } + + return buildVersion; } } diff --git a/tests/Microsoft.DotNet.Docker.Tests/LinuxImageTheoryAttribute.cs b/tests/Microsoft.DotNet.Docker.Tests/LinuxImageTheoryAttribute.cs index 48558506cd..a9dae2bdbc 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/LinuxImageTheoryAttribute.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/LinuxImageTheoryAttribute.cs @@ -1,17 +1,15 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. -namespace Microsoft.DotNet.Docker.Tests +namespace Microsoft.DotNet.Docker.Tests; + +public class LinuxImageTheoryAttribute : DotNetTheoryAttribute { - public class LinuxImageTheoryAttribute : DotNetTheoryAttribute + public LinuxImageTheoryAttribute() { - public LinuxImageTheoryAttribute() + if (!DockerHelper.IsLinuxContainerModeEnabled) { - if (!DockerHelper.IsLinuxContainerModeEnabled) - { - Skip = "Linux image test not applicable when running in Windows Container mode"; - } + Skip = "Linux image test not applicable when running in Windows Container mode"; } } } diff --git a/tests/Microsoft.DotNet.Docker.Tests/Manifest.cs b/tests/Microsoft.DotNet.Docker.Tests/Manifest.cs index 4b46fbdc24..336c847608 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/Manifest.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/Manifest.cs @@ -1,36 +1,34 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. using System.Collections.Generic; -namespace Microsoft.DotNet.Docker.Tests +namespace Microsoft.DotNet.Docker.Tests; + +// Simplified version of the ImageBuilder manifest model +// See https://github.com/dotnet/docker-tools/tree/main/src/Microsoft.DotNet.ImageBuilder/src/Models/Manifest +public class Manifest { - // Simplified version of the ImageBuilder manifest model - // See https://github.com/dotnet/docker-tools/tree/main/src/Microsoft.DotNet.ImageBuilder/src/Models/Manifest - public class Manifest - { - public List Repos { get; set; } = new List(); - } + public List Repos { get; set; } = []; +} - public class Repo - { - public string Id { get; set; } = string.Empty; - public string Name { get; set; } = string.Empty; - public List Images { get; set; } = new List(); - } +public class Repo +{ + public string Id { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + public List Images { get; set; } = []; +} - public class Image - { - public Dictionary SharedTags { get; set; } = new Dictionary(); - public string ProductVersion { get; set; } = string.Empty; - public string Syndication { get; set; } = string.Empty; - public List Platforms { get; set; } = new List(); - } +public class Image +{ + public Dictionary SharedTags { get; set; } = []; + public string ProductVersion { get; set; } = string.Empty; + public string Syndication { get; set; } = string.Empty; + public List Platforms { get; set; } = []; +} - public class Platform - { - public string Dockerfile { get; set; } = string.Empty; - public Dictionary Tags { get; set; } = new Dictionary(); - } +public class Platform +{ + public string Dockerfile { get; set; } = string.Empty; + public Dictionary Tags { get; set; } = []; } diff --git a/tests/Microsoft.DotNet.Docker.Tests/ManifestHelper.cs b/tests/Microsoft.DotNet.Docker.Tests/ManifestHelper.cs index d8914109ff..824cce653a 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/ManifestHelper.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/ManifestHelper.cs @@ -1,65 +1,65 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. using System; using System.Collections.Generic; -using Newtonsoft.Json; using System.Linq; using System.Text.RegularExpressions; +using Newtonsoft.Json; #nullable enable -namespace Microsoft.DotNet.Docker.Tests +namespace Microsoft.DotNet.Docker.Tests; + +public static class ManifestHelper { - public static class ManifestHelper - { - public static Manifest GetManifest() => - JsonConvert.DeserializeObject(Config.Manifest.Value.ToString()) ?? - throw new Exception("Failed to deserialize manifest"); + public static Manifest GetManifest() => + JsonConvert.DeserializeObject(Config.Manifest.Value.ToString()) ?? + throw new Exception("Failed to deserialize manifest"); - public static string GetResolvedProductVersion(Image image) => - GetVariableValue(image.ProductVersion); + public static string GetResolvedProductVersion(Image image) => + GetVariableValue(image.ProductVersion); - public static IEnumerable GetResolvedProductVersions(Repo repo) => - repo.Images.Select(GetResolvedProductVersion).Distinct(); + public static IEnumerable GetResolvedProductVersions(Repo repo) => + repo.Images.Select(GetResolvedProductVersion).Distinct(); - public static List GetResolvedSharedTags(Image image) => - image.SharedTags.Keys.Select(GetVariableValue).ToList(); + public static List GetResolvedSharedTags(Image image) => + image.SharedTags.Keys.Select(GetVariableValue).ToList(); - public static List GetResolvedTags(this Platform platform) => - platform.Tags.Keys.Select(GetVariableValue).ToList(); + public static List GetResolvedTags(this Platform platform) => + platform.Tags.Keys.Select(GetVariableValue).ToList(); - public static Dictionary> GetDockerfileTags(Repo repo) + public static Dictionary> GetDockerfileTags(Repo repo) + { + Dictionary> dockerfileTags = []; + foreach (Image image in repo.Images) { - Dictionary> dockerfileTags = new Dictionary>(); - foreach (Image image in repo.Images) + foreach (Platform platform in image.Platforms) { - foreach (Platform platform in image.Platforms) + DockerfileInfo dockerfileInfo = DockerfileInfo.Create(platform.Dockerfile, repo, image, platform); + if (!dockerfileTags.TryGetValue(dockerfileInfo, out List? value)) { - DockerfileInfo dockerfileInfo = DockerfileInfo.Create(platform.Dockerfile, repo, image, platform); - if (!dockerfileTags.TryGetValue(dockerfileInfo, out List? value)) - { - value = []; - dockerfileTags[dockerfileInfo] = value; - } - - value.AddRange(GetResolvedTags(platform)); - value.AddRange(GetResolvedSharedTags(image)); + value = []; + dockerfileTags[dockerfileInfo] = value; } + + value.AddRange(GetResolvedTags(platform)); + value.AddRange(GetResolvedSharedTags(image)); } - return dockerfileTags; } - private static string GetVariableValue(string input) + return dockerfileTags; + } + + private static string GetVariableValue(string input) + { + string variablePattern = @"\$\((?.+)\)"; + foreach (Match match in Regex.Matches(input, variablePattern)) { - string variablePattern = @"\$\((?.+)\)"; - foreach (Match match in Regex.Matches(input, variablePattern)) - { - string variableName = Config.GetVariableValue(match.Groups["variable"].Value); - input = input.Replace(match.Value, variableName); - } - return input; + string variableName = Config.GetVariableValue(match.Groups["variable"].Value); + input = input.Replace(match.Value, variableName); } + + return input; } } diff --git a/tests/Microsoft.DotNet.Docker.Tests/MonitorImageTests.cs b/tests/Microsoft.DotNet.Docker.Tests/MonitorImageTests.cs index 76110df924..a271646426 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/MonitorImageTests.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/MonitorImageTests.cs @@ -1,6 +1,5 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. using System; using System.Collections.Generic; @@ -13,771 +12,721 @@ using System.Text; using System.Text.Json; using System.Threading.Tasks; +using Microsoft.DotNet.Docker.Tests.TestScenarios; using Xunit; using Xunit.Abstractions; -namespace Microsoft.DotNet.Docker.Tests -{ - [Trait("Category", "monitor")] - public class MonitorImageTests : CommonRuntimeImageTests - { - private const int DefaultHttpPort = 80; - private const int DefaultArtifactsPort = 52323; - private const int DefaultMetricsPort = 52325; +namespace Microsoft.DotNet.Docker.Tests; - private const string UrlPath_Processes = "processes"; - private const string UrlPath_Metrics = "metrics"; +[Trait("Category", "monitor")] +public class MonitorImageTests(ITestOutputHelper outputHelper) : CommonRuntimeImageTests(outputHelper) +{ + private const int DefaultHttpPort = 80; + private const int DefaultArtifactsPort = 52323; + private const int DefaultMetricsPort = 52325; - private const string Directory_Diag = "/diag"; - private const string Directory_Tmp = "/tmp"; + private const string UrlPath_Processes = "processes"; + private const string UrlPath_Metrics = "metrics"; - private const string File_DiagPort = Directory_Diag + "/port"; + private const string Directory_Diag = "/diag"; + private const string Directory_Tmp = "/tmp"; - /// - /// Command line that is the default command line presented in the image. - /// When specifying additional command arguments, these must be prepended to - /// maintain existing behavior. - /// - private const string Switch_DefaultImageCmd = "collect --urls https://+:52323 --metricUrls http://+:52325"; + private const string File_DiagPort = Directory_Diag + "/port"; - /// - /// Command line switch to disable authentication. By default, - /// dotnet-monitor requires authentication on the artifacts port. - /// - private const string Switch_NoAuthentication = "--no-auth"; + /// + /// Command line that is the default command line presented in the image. + /// When specifying additional command arguments, these must be prepended to + /// maintain existing behavior. + /// + private const string Switch_DefaultImageCmd = "collect --urls https://+:52323 --metricUrls http://+:52325"; - public MonitorImageTests(ITestOutputHelper outputHelper) - : base(outputHelper) - { - } + /// + /// Command line switch to disable authentication. By default, + /// dotnet-monitor requires authentication on the artifacts port. + /// + private const string Switch_NoAuthentication = "--no-auth"; - protected override DotNetImageRepo ImageRepo => DotNetImageRepo.Monitor; + protected override DotNetImageRepo ImageRepo => DotNetImageRepo.Monitor; - public static IEnumerable GetMonitorImageData() => - TestData.GetMonitorImageData() - .Select(imageData => new object[] { imageData }); + public static IEnumerable GetMonitorImageData() => + TestData.GetMonitorImageData() + .Select(imageData => new object[] { imageData }); - /// - /// Gets each dotnet-monitor image paired with each sample aspnetcore image of the same architecture. - /// Allows for testing volume mounts and diagnostic port usage among different distros. - /// - public static IEnumerable GetScenarioData() + /// + /// Gets each dotnet-monitor image paired with each sample aspnetcore image of the same architecture. + /// Allows for testing volume mounts and diagnostic port usage among different distros. + /// + public static IEnumerable GetScenarioData() + { + IList data = []; + foreach (ProductImageData productImageData in TestData.GetMonitorImageData()) { - IList data = new List(); - foreach (ProductImageData ProductImageData in TestData.GetMonitorImageData()) + foreach (SampleImageData sampleImageData in TestData.GetAllSampleImageData()) { - foreach (SampleImageData sampleImageData in TestData.GetAllSampleImageData()) + // Only use published images (do not want to build unpublished images in the tests) + if (!sampleImageData.IsPublished) { - // Only use published images (do not want to build unpublished images in the tests) - if (!sampleImageData.IsPublished) - continue; - - // Only consider the sample image if it has the same architecture. - if (ProductImageData.Arch != sampleImageData.Arch) - continue; + continue; + } - data.Add(new object[] { ProductImageData, sampleImageData }); + // Only consider the sample image if it has the same architecture. + if (productImageData.Arch != sampleImageData.Arch) + { + continue; } + + data.Add([productImageData, sampleImageData]); } - return data; } - [LinuxImageTheory] - [MemberData(nameof(GetMonitorImageData))] - public void VerifyInsecureFiles(ProductImageData imageData) - { - base.VerifyCommonInsecureFiles(imageData); - } + return data; + } - [LinuxImageTheory] - [MemberData(nameof(GetMonitorImageData))] - public void VerifyShellNotInstalledForDistroless(ProductImageData imageData) - { - base.VerifyCommonShellNotInstalledForDistroless(imageData); - } + [LinuxImageTheory] + [MemberData(nameof(GetMonitorImageData))] + public void VerifyInsecureFiles(ProductImageData imageData) => VerifyCommonInsecureFiles(imageData); - [DotNetTheory] - [MemberData(nameof(GetMonitorImageData))] - public void VerifyNoSasToken(ProductImageData imageData) - { - base.VerifyCommonNoSasToken(imageData); - } + [LinuxImageTheory] + [MemberData(nameof(GetMonitorImageData))] + public void VerifyShellNotInstalledForDistroless(ProductImageData imageData) => VerifyCommonShellNotInstalledForDistroless(imageData); - [DotNetTheory] - [MemberData(nameof(GetMonitorImageData))] - public void VerifyDefaultUser(ProductImageData imageData) - { - VerifyCommonDefaultUser(imageData); - } + [DotNetTheory] + [MemberData(nameof(GetMonitorImageData))] + public void VerifyNoSasToken(ProductImageData imageData) => VerifyCommonNoSasToken(imageData); - /// - /// Verifies that the environment variables essential to dotnet-monitor are set correctly. - /// - [LinuxImageTheory] - [MemberData(nameof(GetMonitorImageData))] - public void VerifyEnvironmentVariables(ProductImageData imageData) - { - List variables = - [ - ..GetCommonEnvironmentVariables(), - EnvironmentVariableInfo.Require("ASPNETCORE_HTTP_PORTS", string.Empty), - - // Diagnostics should be disabled - EnvironmentVariableInfo.Require("COMPlus_EnableDiagnostics", "0"), - - // DefaultProcess filter should select a process with a process ID of 1 - EnvironmentVariableInfo.Require("DefaultProcess__Filters__0__Key", "ProcessId"), - EnvironmentVariableInfo.Require("DefaultProcess__Filters__0__Value", "1"), - - // Existing (orphaned) diagnostic port should be delete before starting server - EnvironmentVariableInfo.Require("DiagnosticPort__DeleteEndpointOnStartup", "true"), - - // GC mode should be set to Server - EnvironmentVariableInfo.Require("DOTNET_gcServer", "1"), - - // Console logger format should be JSON and output UTC timestamps without timezone information - EnvironmentVariableInfo.Require("Logging__Console__FormatterName", "json"), - EnvironmentVariableInfo.Require("Logging__Console__FormatterOptions__TimestampFormat", "yyyy-MM-ddTHH:mm:ss.fffffffZ"), - EnvironmentVariableInfo.Require("Logging__Console__FormatterOptions__UseUtcTimestamp", "true"), - ]; - - EnvironmentVariableInfo.Validate( - variables, - imageData.GetImage(ImageRepo, DockerHelper), - imageData, - DockerHelper); - } + [DotNetTheory] + [MemberData(nameof(GetMonitorImageData))] + public void VerifyDefaultUser(ProductImageData imageData) => VerifyCommonDefaultUser(imageData); - /// - /// Tests that the image can run without additional configuration - /// and the metrics endpoint is usable without providing authentication. - /// - [LinuxImageTheory] - [MemberData(nameof(GetMonitorImageData))] - public Task VerifyMonitorDefault(ProductImageData imageData) - { - return VerifyMonitorAsync(imageData, noAuthentication: false); - } + /// + /// Verifies that the environment variables essential to dotnet-monitor are set correctly. + /// + [LinuxImageTheory] + [MemberData(nameof(GetMonitorImageData))] + public void VerifyEnvironmentVariables(ProductImageData imageData) + { + List variables = + [ + ..GetCommonEnvironmentVariables(), + EnvironmentVariableInfo.Require("ASPNETCORE_HTTP_PORTS", string.Empty), + + // Diagnostics should be disabled + EnvironmentVariableInfo.Require("COMPlus_EnableDiagnostics", "0"), + + // DefaultProcess filter should select a process with a process ID of 1 + EnvironmentVariableInfo.Require("DefaultProcess__Filters__0__Key", "ProcessId"), + EnvironmentVariableInfo.Require("DefaultProcess__Filters__0__Value", "1"), + + // Existing (orphaned) diagnostic port should be delete before starting server + EnvironmentVariableInfo.Require("DiagnosticPort__DeleteEndpointOnStartup", "true"), + + // GC mode should be set to Server + EnvironmentVariableInfo.Require("DOTNET_gcServer", "1"), + + // Console logger format should be JSON and output UTC timestamps without timezone information + EnvironmentVariableInfo.Require("Logging__Console__FormatterName", "json"), + EnvironmentVariableInfo.Require("Logging__Console__FormatterOptions__TimestampFormat", "yyyy-MM-ddTHH:mm:ss.fffffffZ"), + EnvironmentVariableInfo.Require("Logging__Console__FormatterOptions__UseUtcTimestamp", "true"), + ]; + + EnvironmentVariableInfo.Validate( + variables, + imageData.GetImage(ImageRepo, DockerHelper), + DockerHelper); + } - /// - /// Tests that the image can run without https enabled, that the artifacts ports - /// respond with Unauthroized, and the metrics endpoint is usable without - /// providing authentication. - /// - [LinuxImageTheory] - [MemberData(nameof(GetMonitorImageData))] - public Task VerifyMonitorNoHttpsUnconfiguredAuth(ProductImageData imageData) - { - return VerifyMonitorAsync( - imageData, - noAuthentication: false, - async containerName => + /// + /// Tests that the image can run without additional configuration + /// and the metrics endpoint is usable without providing authentication. + /// + [LinuxImageTheory] + [MemberData(nameof(GetMonitorImageData))] + public Task VerifyMonitorDefault(ProductImageData imageData) => VerifyMonitorAsync(imageData, noAuthentication: false); + + /// + /// Tests that the image can run without https enabled, that the artifacts ports + /// respond with Unauthroized, and the metrics endpoint is usable without + /// providing authentication. + /// + [LinuxImageTheory] + [MemberData(nameof(GetMonitorImageData))] + public Task VerifyMonitorNoHttpsUnconfiguredAuth(ProductImageData imageData) + { + return VerifyMonitorAsync( + imageData, + noAuthentication: false, + async containerName => + { + if (!Config.IsHttpVerificationDisabled) { - if (!Config.IsHttpVerificationDisabled) - { - // Verify processes returns 401 (Unauthorized) since authentication was not configured. - await WebScenario.VerifyHttpResponseFromContainerAsync( + // Verify processes returns 401 (Unauthorized) since authentication was not configured. + await WebScenario.VerifyHttpResponseFromContainerAsync( + containerName, + DockerHelper, + OutputHelper, + DefaultArtifactsPort, + UrlPath_Processes, + m => VerifyStatusCode(m, HttpStatusCode.Unauthorized)); + } + }, + builder => + // Reset and expose the artifacts port over http (not secure) + builder.MonitorUrl(DefaultArtifactsPort)); + } + + /// + /// Tests that the image can run without https and authenciation, thus the artifacts + /// and the metrics ports are usable without providing authentication. + /// + [LinuxImageTheory] + [MemberData(nameof(GetMonitorImageData))] + public Task VerifyMonitorNoHttpsNoAuth(ProductImageData imageData) + { + return VerifyMonitorAsync( + imageData, + noAuthentication: true, + async containerName => + { + if (!Config.IsHttpVerificationDisabled) + { + // Verify metrics endpoint is accessible and produces zero processes + using HttpResponseMessage processesMessage = + await WebScenario.GetHttpResponseFromContainerAsync( containerName, DockerHelper, OutputHelper, DefaultArtifactsPort, - UrlPath_Processes, - m => VerifyStatusCode(m, HttpStatusCode.Unauthorized)); - } - }, - builder => - { - // Reset and expose the artifacts port over http (not secure) - builder.MonitorUrl(DefaultArtifactsPort); - }); - } + UrlPath_Processes); - /// - /// Tests that the image can run without https and authenciation, thus the artifacts - /// and the metrics ports are usable without providing authentication. - /// - [LinuxImageTheory] - [MemberData(nameof(GetMonitorImageData))] - public Task VerifyMonitorNoHttpsNoAuth(ProductImageData imageData) - { - return VerifyMonitorAsync( - imageData, - noAuthentication: true, - async containerName => - { - if (!Config.IsHttpVerificationDisabled) - { - // Verify metrics endpoint is accessible and produces zero processes - using HttpResponseMessage processesMessage = - await WebScenario.GetHttpResponseFromContainerAsync( - containerName, - DockerHelper, - OutputHelper, - DefaultArtifactsPort, - UrlPath_Processes); - - JsonElement rootElement = GetContentAsJsonElement(processesMessage); - - // Verify returns an empty array (should not detect any processes) - Assert.Equal(JsonValueKind.Array, rootElement.ValueKind); - Assert.Equal(0, rootElement.GetArrayLength()); - } - }, - builder => - { - // Reset and expose the artifacts port over http (not secure) - builder.MonitorUrl(DefaultArtifactsPort); - }); - } + JsonElement rootElement = GetContentAsJsonElement(processesMessage); - /// - /// Tests that the image can run without https enabled and that the artifacts ports - /// are accessible with valid authorization header. - /// - [LinuxImageTheory] - [MemberData(nameof(GetMonitorImageData))] - public Task VerifyMonitorNoHttpsWithAuth(ProductImageData imageData) - { - GenerateKeyOutput output = GenerateKey(imageData); - AuthenticationHeaderValue authorizationHeader = AuthenticationHeaderValue.Parse(output.AuthorizationHeader); + // Verify returns an empty array (should not detect any processes) + Assert.Equal(JsonValueKind.Array, rootElement.ValueKind); + Assert.Equal(0, rootElement.GetArrayLength()); + } + }, + builder => + // Reset and expose the artifacts port over http (not secure) + builder.MonitorUrl(DefaultArtifactsPort)); + } + + /// + /// Tests that the image can run without https enabled and that the artifacts ports + /// are accessible with valid authorization header. + /// + [LinuxImageTheory] + [MemberData(nameof(GetMonitorImageData))] + public Task VerifyMonitorNoHttpsWithAuth(ProductImageData imageData) + { + GenerateKeyOutput output = GenerateKey(imageData); + AuthenticationHeaderValue authorizationHeader = AuthenticationHeaderValue.Parse(output.AuthorizationHeader); - return VerifyMonitorAsync( - imageData, - noAuthentication: false, - async containerName => + return VerifyMonitorAsync( + imageData, + noAuthentication: false, + async containerName => + { + if (!Config.IsHttpVerificationDisabled) { - if (!Config.IsHttpVerificationDisabled) - { - // Verify processes returns 401 (Unauthorized) since authentication was not provided. - await WebScenario.VerifyHttpResponseFromContainerAsync( + // Verify processes returns 401 (Unauthorized) since authentication was not provided. + await WebScenario.VerifyHttpResponseFromContainerAsync( + containerName, + DockerHelper, + OutputHelper, + DefaultArtifactsPort, + UrlPath_Processes, + m => VerifyStatusCode(m, HttpStatusCode.Unauthorized)); + + // Verify processes is accessible using authorization header + using HttpResponseMessage processesMessage = + await WebScenario.GetHttpResponseFromContainerAsync( containerName, DockerHelper, OutputHelper, DefaultArtifactsPort, UrlPath_Processes, - m => VerifyStatusCode(m, HttpStatusCode.Unauthorized)); - - // Verify processes is accessible using authorization header - using HttpResponseMessage processesMessage = - await WebScenario.GetHttpResponseFromContainerAsync( - containerName, - DockerHelper, - OutputHelper, - DefaultArtifactsPort, - UrlPath_Processes, - authorizationHeader: authorizationHeader); - - JsonElement rootElement = GetContentAsJsonElement(processesMessage); - - // Verify returns an empty array (should not detect any processes) - Assert.Equal(JsonValueKind.Array, rootElement.ValueKind); - Assert.Equal(0, rootElement.GetArrayLength()); - } - }, - builder => - { - // Reset and expose the artifacts port over http (not secure) - builder.MonitorUrl(DefaultArtifactsPort); - // Configuration authentication - builder.MonitorApiKey(output.Authentication.MonitorApiKey); - }, - authorizationHeader); - } - - /// - /// Verifies that the image can discover a dotnet process - /// in another container via mounting the /tmp directory. - /// - // Temporarily disabled because of https://github.com/dotnet/dotnet-docker/issues/7191. - // Re-enable this test when that issue is fixed. - [LinuxImageTheory(Skip = "Temporarily disabled because of https://github.com/dotnet/dotnet-docker/issues/7191. Re-enable when that issue is fixed.")] - [MemberData(nameof(GetScenarioData))] - public Task VerifyConnectMode(ProductImageData imageData, SampleImageData sampleData) - { - return VerifyScenarioAsync( - productImageData: imageData, - sampleImageData: sampleData, - shareTmpVolume: true, - listenDiagPortVolume: false, - noAuthentication: true, - async (monitorName, sampleName) => - { - if (!Config.IsHttpVerificationDisabled) - { - using HttpResponseMessage responseMessage = - await WebScenario.GetHttpResponseFromContainerAsync( - monitorName, - DockerHelper, - OutputHelper, - DefaultArtifactsPort, - UrlPath_Processes); - - JsonElement rootElement = GetContentAsJsonElement(responseMessage); - - // Verify returns an array with one element (the sample container process) - Assert.Equal(JsonValueKind.Array, rootElement.ValueKind); - Assert.Equal(1, rootElement.GetArrayLength()); - } - }); - } - - /// - /// Verifies that the image can listen for dotnet processes - /// in other containers by having them connect to the diagnostic port listener. - /// - // Temporarily disabled because of https://github.com/dotnet/dotnet-docker/issues/7191. - // Re-enable this test when that issue is fixed. - [LinuxImageTheory(Skip = "Temporarily disabled because of https://github.com/dotnet/dotnet-docker/issues/7191. Re-enable when that issue is fixed.")] - [MemberData(nameof(GetScenarioData))] - public Task VerifyListenMode(ProductImageData imageData, SampleImageData sampleData) - { - return VerifyScenarioAsync( - productImageData: imageData, - sampleImageData: sampleData, - shareTmpVolume: false, - listenDiagPortVolume: true, - noAuthentication: true, - async (monitorName, sampleName) => - { - if (!Config.IsHttpVerificationDisabled) - { - using HttpResponseMessage responseMessage = - await WebScenario.GetHttpResponseFromContainerAsync( - monitorName, - DockerHelper, - OutputHelper, - DefaultArtifactsPort, - UrlPath_Processes); - - JsonElement rootElement = GetContentAsJsonElement(responseMessage); - - // Verify returns an array with one element (the sample container process) - Assert.Equal(JsonValueKind.Array, rootElement.ValueKind); - Assert.Equal(1, rootElement.GetArrayLength()); - } - }); - } + authorizationHeader: authorizationHeader); - /// - /// Runs a single instance of the dotnet-monitor image. - /// - /// The image data of the dotnet-monitor image. - /// Set to true to disable dotnet-monitor authenication. - /// Callback to test some aspect of the container. - /// Allows for modifying the "docker run" args of the container. - private async Task VerifyMonitorAsync( - ProductImageData imageData, - bool noAuthentication, - Func verifyContainerAsync = null, - Action runArgsCallback = null, - AuthenticationHeaderValue authorizationHeader = null - ) - { - GetNames(imageData, out string monitorImageName, out string monitorContainerName, out _); - try - { - DockerRunArgsBuilder runArgsBuilder = DockerRunArgsBuilder.Create() - .ExposePort(DefaultMetricsPort); + JsonElement rootElement = GetContentAsJsonElement(processesMessage); - if (null != runArgsCallback) - { - runArgsCallback(runArgsBuilder); + // Verify returns an empty array (should not detect any processes) + Assert.Equal(JsonValueKind.Array, rootElement.ValueKind); + Assert.Equal(0, rootElement.GetArrayLength()); } + }, + builder => + { + // Reset and expose the artifacts port over http (not secure) + builder.MonitorUrl(DefaultArtifactsPort); + // Configuration authentication + builder.MonitorApiKey(output.Authentication.MonitorApiKey); + }, + authorizationHeader); + } - DockerHelper.Run( - image: monitorImageName, - name: monitorContainerName, - command: GetMonitorAdditionalArgs(imageData, noAuthentication), - detach: true, - optionalRunArgs: runArgsBuilder.Build(), - skipAutoCleanup: true); - + /// + /// Verifies that the image can discover a dotnet process + /// in another container via mounting the /tmp directory. + /// + // Temporarily disabled because of https://github.com/dotnet/dotnet-docker/issues/7191. + // Re-enable this test when that issue is fixed. + [LinuxImageTheory(Skip = "Temporarily disabled because of https://github.com/dotnet/dotnet-docker/issues/7191. Re-enable when that issue is fixed.")] + [MemberData(nameof(GetScenarioData))] + public Task VerifyConnectMode(ProductImageData imageData, SampleImageData sampleData) + { + return VerifyScenarioAsync( + productImageData: imageData, + sampleImageData: sampleData, + shareTmpVolume: true, + listenDiagPortVolume: false, + noAuthentication: true, + async (monitorName, sampleName) => + { if (!Config.IsHttpVerificationDisabled) { - // Verify metrics endpoint is accessible - using HttpResponseMessage metricsMessage = + using HttpResponseMessage responseMessage = await WebScenario.GetHttpResponseFromContainerAsync( - monitorContainerName, + monitorName, DockerHelper, OutputHelper, - DefaultMetricsPort, - UrlPath_Metrics, - authorizationHeader: authorizationHeader); + DefaultArtifactsPort, + UrlPath_Processes); - string metricsContent = await metricsMessage.Content.ReadAsStringAsync(); + JsonElement rootElement = GetContentAsJsonElement(responseMessage); - // Metrics should not return any content if - // no processes are detected. - Assert.Equal(string.Empty, metricsContent); + // Verify returns an array with one element (the sample container process) + Assert.Equal(JsonValueKind.Array, rootElement.ValueKind); + Assert.Equal(1, rootElement.GetArrayLength()); } + }); + } - if (null != verifyContainerAsync) + /// + /// Verifies that the image can listen for dotnet processes + /// in other containers by having them connect to the diagnostic port listener. + /// + // Temporarily disabled because of https://github.com/dotnet/dotnet-docker/issues/7191. + // Re-enable this test when that issue is fixed. + [LinuxImageTheory(Skip = "Temporarily disabled because of https://github.com/dotnet/dotnet-docker/issues/7191. Re-enable when that issue is fixed.")] + [MemberData(nameof(GetScenarioData))] + public Task VerifyListenMode(ProductImageData imageData, SampleImageData sampleData) + { + return VerifyScenarioAsync( + productImageData: imageData, + sampleImageData: sampleData, + shareTmpVolume: false, + listenDiagPortVolume: true, + noAuthentication: true, + async (monitorName, sampleName) => + { + if (!Config.IsHttpVerificationDisabled) { - await verifyContainerAsync(monitorContainerName); + using HttpResponseMessage responseMessage = + await WebScenario.GetHttpResponseFromContainerAsync( + monitorName, + DockerHelper, + OutputHelper, + DefaultArtifactsPort, + UrlPath_Processes); + + JsonElement rootElement = GetContentAsJsonElement(responseMessage); + + // Verify returns an array with one element (the sample container process) + Assert.Equal(JsonValueKind.Array, rootElement.ValueKind); + Assert.Equal(1, rootElement.GetArrayLength()); } - } - finally - { - DockerHelper.DeleteContainer(monitorContainerName); - } - } + }); + } - /// - /// Runs a single instance of each of the dotnet-monitor and samples images. - /// - /// The image data of the dotnet-monitor image. - /// Set to true to mount the /tmp directory in both containers. - /// - /// Set to true to have the monitor container listen with a diagnostic port listener - /// for diagnostic connections from the samples container. - /// - /// Set to true to disable dotnet-monitor authenication. - /// Callback to test some aspect of the containers. - /// Allows for modifying the "docker run" args of the dotnet-monitor container. - /// Allows for modifying the "docker run" args of the samples container. - private async Task VerifyScenarioAsync( - ProductImageData productImageData, - SampleImageData sampleImageData, - bool shareTmpVolume, - bool listenDiagPortVolume, - bool noAuthentication, - Func verifyContainerAsync, - Action monitorRunArgsCallback = null, - Action sampleRunArgsCallback = null) + /// + /// Runs a single instance of the dotnet-monitor image. + /// + /// The image data of the dotnet-monitor image. + /// Set to true to disable dotnet-monitor authenication. + /// Callback to test some aspect of the container. + /// Allows for modifying the "docker run" args of the container. + /// The authorization header to send to dotnet-monitor. + private async Task VerifyMonitorAsync( + ProductImageData imageData, + bool noAuthentication, + Func verifyContainerAsync = null, + Action runArgsCallback = null, + AuthenticationHeaderValue authorizationHeader = null + ) + { + GetNames(imageData, out string monitorImageName, out string monitorContainerName, out _); + try { - GetNames(productImageData, out string monitorImageName, out string monitorContainerName, out int monitorUser); - GetNames(sampleImageData, out string sampleImageName, out string sampleContainerName, out int sampleUser); + DockerRunArgsBuilder runArgsBuilder = DockerRunArgsBuilder.Create() + .ExposePort(DefaultMetricsPort); - DockerRunArgsBuilder monitorArgsBuilder = DockerRunArgsBuilder.Create() - .MonitorUrl(DefaultArtifactsPort); + runArgsCallback?.Invoke(runArgsBuilder); - DockerRunArgsBuilder sampleArgsBuilder = DockerRunArgsBuilder.Create() - .ExposePort(DefaultHttpPort); + DockerHelper.Run( + image: monitorImageName, + name: monitorContainerName, + command: GetMonitorAdditionalArgs(noAuthentication), + detach: true, + optionalRunArgs: runArgsBuilder.Build(), + skipAutoCleanup: true); - string diagPortVolumeName = null; - string tmpVolumeName = null; - - try + if (!Config.IsHttpVerificationDisabled) { - int? volumeUid = AdjustMonitorUserAndCalculateVolumeOwner( - listenDiagPortVolume, - sampleUser, - monitorUser, - monitorArgsBuilder); - - // Create a volume for the two containers to share the /tmp directory. - if (shareTmpVolume) - { - tmpVolumeName = DockerHelper.CreateTmpfsVolume(UniqueName("tmpvol"), volumeUid); + // Verify metrics endpoint is accessible + using HttpResponseMessage metricsMessage = + await WebScenario.GetHttpResponseFromContainerAsync( + monitorContainerName, + DockerHelper, + OutputHelper, + DefaultMetricsPort, + UrlPath_Metrics, + authorizationHeader: authorizationHeader); + + string metricsContent = await metricsMessage.Content.ReadAsStringAsync(); + + // Metrics should not return any content if + // no processes are detected. + Assert.Equal(string.Empty, metricsContent); + } - monitorArgsBuilder.VolumeMount(tmpVolumeName, Directory_Tmp); + if (null != verifyContainerAsync) + { + await verifyContainerAsync(monitorContainerName); + } + } + finally + { + DockerHelper.DeleteContainer(monitorContainerName); + } + } - sampleArgsBuilder.VolumeMount(tmpVolumeName, Directory_Tmp); - } + /// + /// Runs a single instance of each of the dotnet-monitor and samples images. + /// + /// The image data of the dotnet-monitor image. + /// The image data of the sample image. + /// Set to true to mount the /tmp directory in both containers. + /// + /// Set to true to have the monitor container listen with a diagnostic port listener + /// for diagnostic connections from the samples container. + /// + /// Set to true to disable dotnet-monitor authenication. + /// Callback to test some aspect of the containers. + /// Allows for modifying the "docker run" args of the dotnet-monitor container. + /// Allows for modifying the "docker run" args of the samples container. + private async Task VerifyScenarioAsync( + ProductImageData productImageData, + SampleImageData sampleImageData, + bool shareTmpVolume, + bool listenDiagPortVolume, + bool noAuthentication, + Func verifyContainerAsync, + Action monitorRunArgsCallback = null, + Action sampleRunArgsCallback = null) + { + GetNames(productImageData, out string monitorImageName, out string monitorContainerName, out int monitorUser); + GetNames(sampleImageData, out string sampleImageName, out string sampleContainerName, out int sampleUser); - // Create a volume so that the dotnet-monitor container can provide a - // diagnostic listening port to the samples container so that the samples - // process can connect to the dotnet-monitor process. - if (listenDiagPortVolume) - { - diagPortVolumeName = DockerHelper.CreateTmpfsVolume(UniqueName("diagportvol"), volumeUid); + DockerRunArgsBuilder monitorArgsBuilder = DockerRunArgsBuilder.Create() + .MonitorUrl(DefaultArtifactsPort); - monitorArgsBuilder.VolumeMount(diagPortVolumeName, Directory_Diag); - monitorArgsBuilder.MonitorListen(File_DiagPort); + DockerRunArgsBuilder sampleArgsBuilder = DockerRunArgsBuilder.Create() + .ExposePort(DefaultHttpPort); - sampleArgsBuilder.VolumeMount(diagPortVolumeName, Directory_Diag); - sampleArgsBuilder.RuntimeSuspend(File_DiagPort); - } + string diagPortVolumeName = null; + string tmpVolumeName = null; - // Allow modification of the "docker run" args of the monitor container - if (null != monitorRunArgsCallback) - { - monitorRunArgsCallback(monitorArgsBuilder); - } + try + { + int? volumeUid = AdjustMonitorUserAndCalculateVolumeOwner( + listenDiagPortVolume, + sampleUser, + monitorUser, + monitorArgsBuilder); + + // Create a volume for the two containers to share the /tmp directory. + if (shareTmpVolume) + { + tmpVolumeName = DockerHelper.CreateTmpfsVolume(UniqueName("tmpvol"), volumeUid); - // Allow modification of the "docker run" args of the samples container - if (null != sampleRunArgsCallback) - { - sampleRunArgsCallback(sampleArgsBuilder); - } + monitorArgsBuilder.VolumeMount(tmpVolumeName, Directory_Tmp); - // Run the sample container - DockerHelper.Run( - image: sampleImageName, - name: sampleContainerName, - detach: true, - optionalRunArgs: sampleArgsBuilder.Build(), - skipAutoCleanup: true); - - // Run the dotnet-monitor container - DockerHelper.Run( - image: monitorImageName, - name: monitorContainerName, - command: GetMonitorAdditionalArgs(productImageData, noAuthentication), - detach: true, - optionalRunArgs: monitorArgsBuilder.Build(), - skipAutoCleanup: true); - - await verifyContainerAsync( - monitorContainerName, - sampleContainerName); + sampleArgsBuilder.VolumeMount(tmpVolumeName, Directory_Tmp); } - finally - { - DockerHelper.DeleteContainer(monitorContainerName); - DockerHelper.DeleteContainer(sampleContainerName); + // Create a volume so that the dotnet-monitor container can provide a + // diagnostic listening port to the samples container so that the samples + // process can connect to the dotnet-monitor process. + if (listenDiagPortVolume) + { + diagPortVolumeName = DockerHelper.CreateTmpfsVolume(UniqueName("diagportvol"), volumeUid); - if (!string.IsNullOrEmpty(diagPortVolumeName)) - { - DockerHelper.DeleteVolume(diagPortVolumeName); - } + monitorArgsBuilder.VolumeMount(diagPortVolumeName, Directory_Diag); + monitorArgsBuilder.MonitorListen(File_DiagPort); - if (!string.IsNullOrEmpty(tmpVolumeName)) - { - DockerHelper.DeleteVolume(tmpVolumeName); - } + sampleArgsBuilder.VolumeMount(diagPortVolumeName, Directory_Diag); + sampleArgsBuilder.RuntimeSuspend(File_DiagPort); } - } - private static int? AdjustMonitorUserAndCalculateVolumeOwner(bool listenMode, int sampleUid, int monitorUid, DockerRunArgsBuilder monitorArgsBuilder) + // Allow modification of the "docker run" args of the monitor container + monitorRunArgsCallback?.Invoke(monitorArgsBuilder); + + // Allow modification of the "docker run" args of the samples container + sampleRunArgsCallback?.Invoke(sampleArgsBuilder); + + // Run the sample container + DockerHelper.Run( + image: sampleImageName, + name: sampleContainerName, + detach: true, + optionalRunArgs: sampleArgsBuilder.Build(), + skipAutoCleanup: true); + + // Run the dotnet-monitor container + DockerHelper.Run( + image: monitorImageName, + name: monitorContainerName, + command: GetMonitorAdditionalArgs(noAuthentication), + detach: true, + optionalRunArgs: monitorArgsBuilder.Build(), + skipAutoCleanup: true); + + await verifyContainerAsync( + monitorContainerName, + sampleContainerName); + } + finally { - // Make sure volume is accessible by sample app without modifying the sample container - // and ensure monitor app can access it, even if needing to change its user. This is - // done by making the volume owned by the least privileged user; if they are the same user or - // are different non-root users, defer to the user of the sample image. + DockerHelper.DeleteContainer(monitorContainerName); - if (!IsRoot(sampleUid)) - { - if (sampleUid != monitorUid) - { - // If sample is connecting to monitor OR (monitor is connecting to sample AND is not running as root), - // change monitor to run as same user as sample. In general, a client has to either be root or - // the same user as the server for UDS connections. - if (listenMode || !IsRoot(monitorUid)) - { - monitorArgsBuilder.AsUser(sampleUid); - } - } + DockerHelper.DeleteContainer(sampleContainerName); - return sampleUid; - } - else if (!IsRoot(monitorUid)) + if (!string.IsNullOrEmpty(diagPortVolumeName)) { - // Sample is root; monitor is non-root - - if (listenMode) - { - // Monitor has UDS to which sample will connect, which it can without changes since - // it is running as root; use monitor user as volume owner - return monitorUid; - } - else - { - // Sample has UDS to which monitor will connect, which requires monitor to run as - // root as well; use sample user as volume owner - monitorArgsBuilder.AsUser(sampleUid); - - return sampleUid; - } + DockerHelper.DeleteVolume(diagPortVolumeName); } - // Both sample and monitor run as root: no volume ownership change necessary - Debug.Assert(IsRoot(sampleUid) && IsRoot(monitorUid)); - return null; - - static bool IsRoot(int uid) + if (!string.IsNullOrEmpty(tmpVolumeName)) { - return 0 == uid; + DockerHelper.DeleteVolume(tmpVolumeName); } } + } - private static string UniqueName(string name) - { - return $"{name}-{DateTime.Now.ToFileTime()}"; - } + private static int? AdjustMonitorUserAndCalculateVolumeOwner(bool listenMode, int sampleUid, int monitorUid, DockerRunArgsBuilder monitorArgsBuilder) + { + // Make sure volume is accessible by sample app without modifying the sample container + // and ensure monitor app can access it, even if needing to change its user. This is + // done by making the volume owned by the least privileged user; if they are the same user or + // are different non-root users, defer to the user of the sample image. - private static SampleImageData GetSampleImageData(ProductImageData imageData) + if (!IsRoot(sampleUid)) { - return TestData.GetSampleImageData() - .First(d => d.IsPublished = true && d.Arch == imageData.Arch); - } + if (sampleUid != monitorUid) + { + // If sample is connecting to monitor OR (monitor is connecting to sample AND is not running as root), + // change monitor to run as same user as sample. In general, a client has to either be root or + // the same user as the server for UDS connections. + if (listenMode || !IsRoot(monitorUid)) + { + monitorArgsBuilder.AsUser(sampleUid); + } + } - private static string GetMonitorAdditionalArgs(ProductImageData imageData, bool noAuthentication) + return sampleUid; + } + else if (!IsRoot(monitorUid)) { - const char spaceChar = ' '; - - // This flag should be set when we are adding args to the command line. - // The CMD block defined in the Dockerfile already contains the default args, but if we pass any args via - // the commandline args, then Docker overrides the entire CMD instruction from the DockerFile. When this - // happens, we need to add the args back. If additional flags are added to this method, `noAuthentication` - // should be replaced with something like: `(noAuthentication || myNewFlag || mySetting != Setting.Default)` - bool addDefaultArgs = noAuthentication; + // Sample is root; monitor is non-root - // Standard here is to have the built command line always end with a space, so it needs to start with one - StringBuilder builtCommandline = new(spaceChar); - - if (addDefaultArgs) + if (listenMode) { - builtCommandline.AppendFormat("{0} ", Switch_DefaultImageCmd); + // Monitor has UDS to which sample will connect, which it can without changes since + // it is running as root; use monitor user as volume owner + return monitorUid; } - - if (noAuthentication) + else { - builtCommandline.AppendFormat("{0} ", Switch_NoAuthentication); + // Sample has UDS to which monitor will connect, which requires monitor to run as + // root as well; use sample user as volume owner + monitorArgsBuilder.AsUser(sampleUid); + + return sampleUid; } + } - string cmdsResult = builtCommandline.ToString().Trim(spaceChar); + // Both sample and monitor run as root: no volume ownership change necessary + Debug.Assert(IsRoot(sampleUid) && IsRoot(monitorUid)); + return null; - return cmdsResult; - } + static bool IsRoot(int uid) => 0 == uid; + } - private void GetNames(ProductImageData imageData, out string imageName, out string containerName, out int userIdentifier) - { - imageName = imageData.GetImage(ImageRepo, DockerHelper); - containerName = imageData.GetIdentifier("monitortest"); - // If user is empty, then image is running as same as docker daemon (typically root) - userIdentifier = string.IsNullOrEmpty(DockerHelper.GetImageUser(imageName)) ? 0 : imageData.NonRootUID.Value; - } + private static string UniqueName(string name) => $"{name}-{DateTime.Now.ToFileTime()}"; - private void GetNames(SampleImageData imageData, out string imageName, out string containerName, out int userIdentifier) - { - // Need to allow pulling of the sample image since these are not built in the same pipeline - // as the other images; otherwise, these tests will fail due to lack of sample image. - string tag = imageData.GetTagNameBase(SampleImageType.Aspnetapp); - imageName = imageData.GetImage(tag, DockerHelper, allowPull: true); - containerName = imageData.GetIdentifier("monitortest-sample"); - // If user is empty, then image is running as same as docker daemon (typically root) - userIdentifier = string.IsNullOrEmpty(DockerHelper.GetImageUser(imageName)) ? 0 : imageData.NonRootUID.Value; - } + private static string GetMonitorAdditionalArgs(bool noAuthentication) + { + const char spaceChar = ' '; - private void VerifyStatusCode(HttpResponseMessage message, HttpStatusCode statusCode) - { - if (message.StatusCode != statusCode) - { - throw new HttpRequestException($"Expected status code {statusCode}", null, statusCode); - } - } + // This flag should be set when we are adding args to the command line. + // The CMD block defined in the Dockerfile already contains the default args, but if we pass any args via + // the commandline args, then Docker overrides the entire CMD instruction from the DockerFile. When this + // happens, we need to add the args back. If additional flags are added to this method, `noAuthentication` + // should be replaced with something like: `(noAuthentication || myNewFlag || mySetting != Setting.Default)` + bool addDefaultArgs = noAuthentication; - private static JsonElement GetContentAsJsonElement(HttpResponseMessage message) + // Standard here is to have the built command line always end with a space, so it needs to start with one + StringBuilder builtCommandline = new(spaceChar); + + if (addDefaultArgs) { - using (Stream stream = message.Content.ReadAsStream()) - { - return JsonDocument.Parse(stream).RootElement; - } + builtCommandline.AppendFormat("{0} ", Switch_DefaultImageCmd); } - private GenerateKeyOutput GenerateKey(ProductImageData imageData) + if (noAuthentication) { - GetNames(imageData, out string monitorImageName, out string monitorContainerName, out _); - try - { - DockerRunArgsBuilder runArgsBuilder = DockerRunArgsBuilder.Create() - .Entrypoint("dotnet-monitor"); + builtCommandline.AppendFormat("{0} ", Switch_NoAuthentication); + } - string json = DockerHelper.Run( - image: monitorImageName, - name: monitorContainerName, - command: "generatekey -o machinejson", - optionalRunArgs: runArgsBuilder.Build()); + string cmdsResult = builtCommandline.ToString().Trim(spaceChar); - GenerateKeyOutput output = JsonSerializer.Deserialize(json); + return cmdsResult; + } - Assert.NotNull(output?.Authentication?.MonitorApiKey?.PublicKey); - Assert.NotNull(output?.Authentication?.MonitorApiKey?.Subject); - Assert.NotNull(output?.AuthorizationHeader); + private void GetNames(ProductImageData imageData, out string imageName, out string containerName, out int userIdentifier) + { + imageName = imageData.GetImage(ImageRepo, DockerHelper); + containerName = imageData.GetIdentifier("monitortest"); + // If user is empty, then image is running as same as docker daemon (typically root) + userIdentifier = string.IsNullOrEmpty(DockerHelper.GetImageUser(imageName)) ? 0 : imageData.NonRootUID.Value; + } - return output; - } - finally - { - DockerHelper.DeleteContainer(monitorContainerName); - } - } + private void GetNames(SampleImageData imageData, out string imageName, out string containerName, out int userIdentifier) + { + // Need to allow pulling of the sample image since these are not built in the same pipeline + // as the other images; otherwise, these tests will fail due to lack of sample image. + string tag = imageData.GetTagNameBase(SampleImageType.Aspnetapp); + imageName = imageData.GetImage(tag, DockerHelper, allowPull: true); + containerName = imageData.GetIdentifier("monitortest-sample"); + // If user is empty, then image is running as same as docker daemon (typically root) + userIdentifier = string.IsNullOrEmpty(DockerHelper.GetImageUser(imageName)) ? 0 : imageData.NonRootUID.Value; } - internal static class MonitorDockerRunArgsBuilderExtensions + private void VerifyStatusCode(HttpResponseMessage message, HttpStatusCode statusCode) { - // dotnet-monitor variables - internal const string EnvVar_Authentication_MonitorApiKey_PublicKey = "DotnetMonitor_Authentication__MonitorApiKey__PublicKey"; - internal const string EnvVar_Authentication_MonitorApiKey_Subject = "DotnetMonitor_Authentication__MonitorApiKey__Subject"; - internal const string EnvVar_DiagnosticPort_ConnectionMode = "DotnetMonitor_DiagnosticPort__ConnectionMode"; - internal const string EnvVar_DiagnosticPort_EndpointName = "DotnetMonitor_DiagnosticPort__EndpointName"; - internal const string EnvVar_Metrics_Enabled = "DotnetMonitor_Metrics__Enabled"; - internal const string EnvVar_Urls = "DotnetMonitor_Urls"; - - // runtime variables - internal const string EnvVar_DiagnosticPorts = "DOTNET_DiagnosticPorts"; - - public static DockerRunArgsBuilder MonitorApiKey(this DockerRunArgsBuilder builder, MonitorApiKeyOptions options) + if (message.StatusCode != statusCode) { - return builder - .EnvironmentVariable(EnvVar_Authentication_MonitorApiKey_PublicKey, options.PublicKey) - .EnvironmentVariable(EnvVar_Authentication_MonitorApiKey_Subject, options.Subject); + throw new HttpRequestException($"Expected status code {statusCode}", null, statusCode); } + } - /// - /// Disables the metrics endpoint in dotnet-monitor. - /// - public static DockerRunArgsBuilder MonitorDisableMetrics(this DockerRunArgsBuilder builder) - { - return builder.EnvironmentVariable(EnvVar_Metrics_Enabled, "false"); - } + private static JsonElement GetContentAsJsonElement(HttpResponseMessage message) + { + using Stream stream = message.Content.ReadAsStream(); + return JsonDocument.Parse(stream).RootElement; + } - /// - /// Places dotnet-monitor into listen mode, allowing dotnet processes to connect - /// to its diagnostic port listener. - /// - public static DockerRunArgsBuilder MonitorListen(this DockerRunArgsBuilder builder, string endpointName) + private GenerateKeyOutput GenerateKey(ProductImageData imageData) + { + GetNames(imageData, out string monitorImageName, out string monitorContainerName, out _); + try { - return builder - .EnvironmentVariable(EnvVar_DiagnosticPort_ConnectionMode, "Listen") - .EnvironmentVariable(EnvVar_DiagnosticPort_EndpointName, endpointName); - } + DockerRunArgsBuilder runArgsBuilder = DockerRunArgsBuilder.Create() + .Entrypoint("dotnet-monitor"); - /// - /// Sets the artifacts url with the port and exposes the port on the container. - /// - public static DockerRunArgsBuilder MonitorUrl(this DockerRunArgsBuilder builder, int port) - { - return builder.ExposePort(port) - .EnvironmentVariable(EnvVar_Urls, WildcardUrl(port)); - } + string json = DockerHelper.Run( + image: monitorImageName, + name: monitorContainerName, + command: "generatekey -o machinejson", + optionalRunArgs: runArgsBuilder.Build()); - /// - /// Suspends a dotnet runtime until it can connect to a diagnostic port listener - /// at the specified endpoint name. - /// - public static DockerRunArgsBuilder RuntimeSuspend(this DockerRunArgsBuilder builder, string endpointName) - { - return builder.EnvironmentVariable(EnvVar_DiagnosticPorts, $"{endpointName},suspend"); - } + GenerateKeyOutput output = JsonSerializer.Deserialize(json); + + Assert.NotNull(output?.Authentication?.MonitorApiKey?.PublicKey); + Assert.NotNull(output?.Authentication?.MonitorApiKey?.Subject); + Assert.NotNull(output?.AuthorizationHeader); - private static string WildcardUrl(int port) + return output; + } + finally { - return $"http://*:{port}"; + DockerHelper.DeleteContainer(monitorContainerName); } } +} + +internal static class MonitorDockerRunArgsBuilderExtensions +{ + // dotnet-monitor variables + internal const string EnvVar_Authentication_MonitorApiKey_PublicKey = "DotnetMonitor_Authentication__MonitorApiKey__PublicKey"; + internal const string EnvVar_Authentication_MonitorApiKey_Subject = "DotnetMonitor_Authentication__MonitorApiKey__Subject"; + internal const string EnvVar_DiagnosticPort_ConnectionMode = "DotnetMonitor_DiagnosticPort__ConnectionMode"; + internal const string EnvVar_DiagnosticPort_EndpointName = "DotnetMonitor_DiagnosticPort__EndpointName"; + internal const string EnvVar_Metrics_Enabled = "DotnetMonitor_Metrics__Enabled"; + internal const string EnvVar_Urls = "DotnetMonitor_Urls"; + + // runtime variables + internal const string EnvVar_DiagnosticPorts = "DOTNET_DiagnosticPorts"; + + public static DockerRunArgsBuilder MonitorApiKey(this DockerRunArgsBuilder builder, MonitorApiKeyOptions options) + { + return builder + .EnvironmentVariable(EnvVar_Authentication_MonitorApiKey_PublicKey, options.PublicKey) + .EnvironmentVariable(EnvVar_Authentication_MonitorApiKey_Subject, options.Subject); + } /// - /// Represents the structured output of a "dotnet-monitor generatekey -o machinejson" invocation. + /// Disables the metrics endpoint in dotnet-monitor. /// - internal sealed class GenerateKeyOutput - { - public AuthenticationOptions Authentication { get; set; } + public static DockerRunArgsBuilder MonitorDisableMetrics(this DockerRunArgsBuilder builder) => builder.EnvironmentVariable(EnvVar_Metrics_Enabled, "false"); - public string AuthorizationHeader { get; set; } + /// + /// Places dotnet-monitor into listen mode, allowing dotnet processes to connect + /// to its diagnostic port listener. + /// + public static DockerRunArgsBuilder MonitorListen(this DockerRunArgsBuilder builder, string endpointName) + { + return builder + .EnvironmentVariable(EnvVar_DiagnosticPort_ConnectionMode, "Listen") + .EnvironmentVariable(EnvVar_DiagnosticPort_EndpointName, endpointName); } - internal sealed class AuthenticationOptions + /// + /// Sets the artifacts url with the port and exposes the port on the container. + /// + public static DockerRunArgsBuilder MonitorUrl(this DockerRunArgsBuilder builder, int port) { - public MonitorApiKeyOptions MonitorApiKey { get; set; } + return builder.ExposePort(port) + .EnvironmentVariable(EnvVar_Urls, WildcardUrl(port)); } - internal sealed class MonitorApiKeyOptions - { - public string PublicKey { get; set; } + /// + /// Suspends a dotnet runtime until it can connect to a diagnostic port listener + /// at the specified endpoint name. + /// + public static DockerRunArgsBuilder RuntimeSuspend(this DockerRunArgsBuilder builder, string endpointName) => builder.EnvironmentVariable(EnvVar_DiagnosticPorts, $"{endpointName},suspend"); - public string Subject { get; set; } - } + private static string WildcardUrl(int port) => $"http://*:{port}"; +} + +/// +/// Represents the structured output of a "dotnet-monitor generatekey -o machinejson" invocation. +/// +internal sealed class GenerateKeyOutput +{ + public AuthenticationOptions Authentication { get; set; } + + public string AuthorizationHeader { get; set; } +} + +internal sealed class AuthenticationOptions +{ + public MonitorApiKeyOptions MonitorApiKey { get; set; } +} + +internal sealed class MonitorApiKeyOptions +{ + public string PublicKey { get; set; } + + public string Subject { get; set; } } diff --git a/tests/Microsoft.DotNet.Docker.Tests/OS.cs b/tests/Microsoft.DotNet.Docker.Tests/OS.cs index 3c23a6837c..79004cc040 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/OS.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/OS.cs @@ -1,6 +1,5 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. #nullable enable diff --git a/tests/Microsoft.DotNet.Docker.Tests/OSInfo.cs b/tests/Microsoft.DotNet.Docker.Tests/OSInfo.cs index b954828323..5b793085d8 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/OSInfo.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/OSInfo.cs @@ -1,6 +1,5 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. using System; diff --git a/tests/Microsoft.DotNet.Docker.Tests/PowerShellTests.cs b/tests/Microsoft.DotNet.Docker.Tests/PowerShellTests.cs index f4bcdc187d..9d3fd075ec 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/PowerShellTests.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/PowerShellTests.cs @@ -1,20 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.IO; using System.Linq; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Reflection; -using System.Security.Cryptography; -using System.Text; -using System.Threading.Tasks; -using Polly; -using Polly.Retry; using Xunit; using Xunit.Abstractions; @@ -22,13 +11,8 @@ namespace Microsoft.DotNet.Docker.Tests; [Trait("Category", "sdk")] -public class PowerShellTests : ProductImageTests +public class PowerShellTests(ITestOutputHelper outputHelper) : ProductImageTests(outputHelper) { - public PowerShellTests(ITestOutputHelper outputHelper) - : base(outputHelper) - { - } - protected override DotNetImageRepo ImageRepo => DotNetImageRepo.SDK; public static IEnumerable GetImageData() diff --git a/tests/Microsoft.DotNet.Docker.Tests/ProductImageData.cs b/tests/Microsoft.DotNet.Docker.Tests/ProductImageData.cs index 66f574e5fb..151a3e5e9f 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/ProductImageData.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/ProductImageData.cs @@ -1,221 +1,217 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; -namespace Microsoft.DotNet.Docker.Tests +namespace Microsoft.DotNet.Docker.Tests; + +public record ProductImageData : ImageData { - public record ProductImageData : ImageData - { - private OSInfo _sdkOS; - private string _osTag; - private string _osDir; - private ImageVersion? _versionFamily; + private OSInfo _sdkOS; + private ImageVersion? _versionFamily; - public bool HasCustomSdk => _sdkOS is not null; + public bool HasCustomSdk => _sdkOS is not null; - public bool GlobalizationInvariantMode => !SupportsGlobalization; + public bool GlobalizationInvariantMode => !SupportsGlobalization; - // PowerShell does not support Arm-based Alpine - public bool SupportsPowerShell => !(OS.Family == OSFamily.Alpine && IsArm); + // PowerShell does not support Arm-based Alpine + public bool SupportsPowerShell => !(OS.Family == OSFamily.Alpine && IsArm); - /// - /// Indicates whether the SDK version of the image supports `dnx` and - /// should have it installed. - /// - public bool SupportsDnx => VersionFamily != ImageVersion.V8_0 && VersionFamily != ImageVersion.V9_0; + /// + /// Indicates whether the SDK version of the image supports `dnx` and + /// should have it installed. + /// + public bool SupportsDnx => VersionFamily != ImageVersion.V8_0 && VersionFamily != ImageVersion.V9_0; - public OSInfo SdkOS - { - get => HasCustomSdk ? _sdkOS : OS; - init => _sdkOS = value; - } + public OSInfo SdkOS + { + get => HasCustomSdk ? _sdkOS : OS; + init => _sdkOS = value; + } - public DotNetImageVariant SdkImageVariant { get; init; } = DotNetImageVariant.None; + public DotNetImageVariant SdkImageVariant { get; init; } = DotNetImageVariant.None; - public string OSTag - { - get => _osTag ?? OS; - init => _osTag = value; - } + public string OSTag + { + get => field ?? OS; + init; + } - public string OSDir - { - get => _osDir ?? OSTag; - init => _osDir = value; - } + public string OSDir + { + get => field ?? OSTag; + init; + } - public ImageVersion Version { get; init; } + public ImageVersion Version { get; init; } - public ImageVersion VersionFamily - { - get { return _versionFamily.GetValueOrDefault(Version); } - init { _versionFamily = value; } - } + public ImageVersion VersionFamily + { + get => _versionFamily.GetValueOrDefault(Version); init => _versionFamily = value; + } - public DotNetImageVariant ImageVariant { get; init;} + public DotNetImageVariant ImageVariant { get; init; } - public DotNetImageRepo SupportedImageRepos { get; init; } = DotNetImageRepo.Runtime_Deps - | DotNetImageRepo.Runtime - | DotNetImageRepo.Aspnet - | DotNetImageRepo.SDK; + public DotNetImageRepo SupportedImageRepos { get; init; } = DotNetImageRepo.Runtime_Deps + | DotNetImageRepo.Runtime + | DotNetImageRepo.Aspnet + | DotNetImageRepo.SDK; - public string VersionString => Version.ToString(); + public string VersionString => Version.ToString(); - private bool SupportsGlobalization + private bool SupportsGlobalization + { + get { - get - { - bool isSizeFocusedImage = IsDistroless || OS.Family == OSFamily.Alpine; - return ImageVariant.HasFlag(DotNetImageVariant.Extra) || !isSizeFocusedImage; - } + bool isSizeFocusedImage = IsDistroless || OS.Family == OSFamily.Alpine; + return ImageVariant.HasFlag(DotNetImageVariant.Extra) || !isSizeFocusedImage; } + } - public string GetDockerfilePath(DotNetImageRepo imageRepo) + public string GetDockerfilePath(DotNetImageRepo imageRepo) + { + // Aspire Dashboard Dockerfiles are not organized by product version or OS. + if (imageRepo == DotNetImageRepo.Aspire_Dashboard) { - // Aspire Dashboard Dockerfiles are not organized by product version or OS. - if (imageRepo == DotNetImageRepo.Aspire_Dashboard) - { - return $"{GetRepoSrcPath(imageRepo)}/{GetArchLabel()}"; - } + return $"{GetRepoSrcPath(imageRepo)}/{GetArchLabel()}"; + } - IEnumerable pathComponents = - [ - GetRepoSrcPath(imageRepo), - Version.ToString(), - OSDir + GetVariantSuffix(), - GetArchLabel() - ]; + IEnumerable pathComponents = + [ + GetRepoSrcPath(imageRepo), + Version.ToString(), + OSDir + GetVariantSuffix(), + GetArchLabel() + ]; - // Don't use Path.Join since it will use Windows path separators when run locally. - return string.Join('/', pathComponents); - } + // Don't use Path.Join since it will use Windows path separators when run locally. + return string.Join('/', pathComponents); + } - private string GetVariantSuffix() => - ImageVariant == DotNetImageVariant.None ? "" : $"-{GetImageVariantName(ImageVariant)}"; + private string GetVariantSuffix() => + ImageVariant == DotNetImageVariant.None ? "" : $"-{GetImageVariantName(ImageVariant)}"; - public override string GetIdentifier(string type) => $"{VersionString}-{base.GetIdentifier(type)}"; + public override string GetIdentifier(string type) => $"{VersionString}-{base.GetIdentifier(type)}"; - public static string GetRepoName(DotNetImageRepo imageRepo) => imageRepo switch - { - DotNetImageRepo.Monitor_Base => "monitor/base", - _ => Enum.GetName(imageRepo).ToLowerInvariant().Replace('_', '-') - }; + public static string GetRepoName(DotNetImageRepo imageRepo) => imageRepo switch + { + DotNetImageRepo.Monitor_Base => "monitor/base", + _ => Enum.GetName(imageRepo).ToLowerInvariant().Replace('_', '-') + }; - private static string GetRepoSrcPath(DotNetImageRepo imageRepo) => "src/" + imageRepo switch - { - DotNetImageRepo.Monitor_Base => "monitor-base", - _ => GetRepoName(imageRepo) - }; + private static string GetRepoSrcPath(DotNetImageRepo imageRepo) => "src/" + imageRepo switch + { + DotNetImageRepo.Monitor_Base => "monitor-base", + _ => GetRepoName(imageRepo) + }; - public static string GetImageVariantName(DotNetImageVariant imageVariant) + public static string GetImageVariantName(DotNetImageVariant imageVariant) + { + IEnumerable imageVariants = []; + foreach (DotNetImageVariant enumValue in Enum.GetValues(typeof(DotNetImageVariant))) { - IEnumerable imageVariants = []; - foreach (DotNetImageVariant enumValue in Enum.GetValues(typeof(DotNetImageVariant))) + if (enumValue != DotNetImageVariant.None && imageVariant.HasFlag(enumValue)) { - if (enumValue != DotNetImageVariant.None && imageVariant.HasFlag(enumValue)) - { - imageVariants = imageVariants.Append(Enum.GetName(typeof(DotNetImageVariant), enumValue).ToLowerInvariant()); - } + imageVariants = imageVariants.Append(Enum.GetName(typeof(DotNetImageVariant), enumValue).ToLowerInvariant()); } - return string.Join('-', imageVariants); } - public string GetImage(DotNetImageRepo imageRepo, DockerHelper dockerHelper, bool skipPull = false) - { - // ASP.NET composite includes its own runtime that we want to test. - if (ImageVariant.HasFlag(DotNetImageVariant.Composite) && imageRepo == DotNetImageRepo.Runtime) - { - imageRepo = DotNetImageRepo.Aspnet; - } - - if (imageRepo != DotNetImageRepo.SDK && !SupportedImageRepos.HasFlag(imageRepo)) - { - throw new ArgumentOutOfRangeException(nameof(imageRepo), - $"Unsupported image type '{imageRepo}' for Image Variant '{ImageVariant}'"); - } - - string tag = GetTagName(imageRepo); - string imageName = GetImageName(tag, GetRepoName(imageRepo)); - - if (!skipPull) - { - // Pull the image to ensure it exists - PullImageIfNecessary(imageName, dockerHelper); - } + return string.Join('-', imageVariants); + } - return imageName; + public string GetImage(DotNetImageRepo imageRepo, DockerHelper dockerHelper, bool skipPull = false) + { + // ASP.NET composite includes its own runtime that we want to test. + if (ImageVariant.HasFlag(DotNetImageVariant.Composite) && imageRepo == DotNetImageRepo.Runtime) + { + imageRepo = DotNetImageRepo.Aspnet; } - public string GetProductVersion(DotNetImageRepo imageRepoToUse, DotNetImageRepo productVersionRepo, DockerHelper dockerHelper) + if (imageRepo != DotNetImageRepo.SDK && !SupportedImageRepos.HasFlag(imageRepo)) { - string imageName = GetImage(imageRepoToUse, dockerHelper); - return GetProductVersion(imageName, productVersionRepo, dockerHelper); + throw new ArgumentOutOfRangeException(nameof(imageRepo), + $"Unsupported image type '{imageRepo}' for Image Variant '{ImageVariant}'"); } - public string GetProductVersion(string imageName, DotNetImageRepo productVersionRepo, DockerHelper dockerHelper) - { - string containerName = GetIdentifier($"GetProductVersion-{productVersionRepo}"); + string tag = GetTagName(imageRepo); + string imageName = GetImageName(tag, GetRepoName(imageRepo)); - return productVersionRepo switch - { - DotNetImageRepo.SDK => dockerHelper.Run(imageName, containerName, "dotnet --version"), - DotNetImageRepo.Runtime => GetRuntimeVersion(imageName, containerName, "Microsoft.NETCore.App", dockerHelper), - DotNetImageRepo.Aspnet => GetRuntimeVersion(imageName, containerName, "Microsoft.AspNetCore.App", dockerHelper), - _ => throw new NotSupportedException($"Unsupported image type '{productVersionRepo}'"), - }; + if (!skipPull) + { + // Pull the image to ensure it exists + PullImageIfNecessary(imageName, dockerHelper); } - private string GetRuntimeVersion(string imageName, string containerName, string runtimeName, DockerHelper dockerHelper) - { - const string versionGroupName = "Version"; + return imageName; + } - string runtimeListing = dockerHelper.Run(imageName, containerName, FormatDotnetCommand("--list-runtimes")); - Regex versionRegex = new Regex($"{runtimeName} (?<{versionGroupName}>[^\\s]+) "); - Match match = versionRegex.Match(runtimeListing); - return match.Success ? match.Groups[versionGroupName].Value : string.Empty; - } + public string GetProductVersion(DotNetImageRepo imageRepoToUse, DotNetImageRepo productVersionRepo, DockerHelper dockerHelper) + { + string imageName = GetImage(imageRepoToUse, dockerHelper); + return GetProductVersion(imageName, productVersionRepo, dockerHelper); + } - private string FormatDotnetCommand(string command) + public string GetProductVersion(string imageName, DotNetImageRepo productVersionRepo, DockerHelper dockerHelper) + { + string containerName = GetIdentifier($"GetProductVersion-{productVersionRepo}"); + + return productVersionRepo switch { - // For distroless, dotnet will be the default entrypoint so we don't need to specify "dotnet" in the command. - // See https://github.com/dotnet/dotnet-docker/issues/3866 - if (IsDistroless) - { - return command; - } + DotNetImageRepo.SDK => dockerHelper.Run(imageName, containerName, "dotnet --version"), + DotNetImageRepo.Runtime => GetRuntimeVersion(imageName, containerName, "Microsoft.NETCore.App", dockerHelper), + DotNetImageRepo.Aspnet => GetRuntimeVersion(imageName, containerName, "Microsoft.AspNetCore.App", dockerHelper), + _ => throw new NotSupportedException($"Unsupported image type '{productVersionRepo}'"), + }; + } - return $"dotnet {command}"; - } + private string GetRuntimeVersion(string imageName, string containerName, string runtimeName, DockerHelper dockerHelper) + { + const string versionGroupName = "Version"; + + string runtimeListing = dockerHelper.Run(imageName, containerName, FormatDotnetCommand("--list-runtimes")); + Regex versionRegex = new Regex($"{runtimeName} (?<{versionGroupName}>[^\\s]+) "); + Match match = versionRegex.Match(runtimeListing); + return match.Success ? match.Groups[versionGroupName].Value : string.Empty; + } - private string GetTagName(DotNetImageRepo imageRepo) + private string FormatDotnetCommand(string command) + { + // For distroless, dotnet will be the default entrypoint so we don't need to specify "dotnet" in the command. + // See https://github.com/dotnet/dotnet-docker/issues/3866 + if (IsDistroless) { - ImageVersion imageVersion; - string os; - string variant = ImageRepoIsSupported(imageRepo) && ImageVariant != DotNetImageVariant.None - ? GetImageVariantName(ImageVariant) - : ""; + return command; + } - switch (imageRepo) - { - case DotNetImageRepo.SDK: - imageVersion = Version; - os = SdkOS; - variant = GetImageVariantName(SdkImageVariant); - break; - default: - imageVersion = Version; - os = OSTag; - break; - } + return $"dotnet {command}"; + } - return GetTagName(imageVersion.GetTagName(), os, variant); + private string GetTagName(DotNetImageRepo imageRepo) + { + ImageVersion imageVersion; + string os; + string variant = ImageRepoIsSupported(imageRepo) && ImageVariant != DotNetImageVariant.None + ? GetImageVariantName(ImageVariant) + : ""; + + switch (imageRepo) + { + case DotNetImageRepo.SDK: + imageVersion = Version; + os = SdkOS; + variant = GetImageVariantName(SdkImageVariant); + break; + default: + imageVersion = Version; + os = OSTag; + break; } - public bool ImageRepoIsSupported(DotNetImageRepo imageRepo) => SupportedImageRepos.HasFlag(imageRepo); + return GetTagName(imageVersion.GetTagName(), os, variant); } + + public bool ImageRepoIsSupported(DotNetImageRepo imageRepo) => SupportedImageRepos.HasFlag(imageRepo); } diff --git a/tests/Microsoft.DotNet.Docker.Tests/ProductImageTests.cs b/tests/Microsoft.DotNet.Docker.Tests/ProductImageTests.cs index 1d4b06543b..7fa243b705 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/ProductImageTests.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/ProductImageTests.cs @@ -1,433 +1,414 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. using System; using System.Collections.Generic; -using System.IO; using System.Linq; -using System.Text.Json; using System.Text.Json.Nodes; using System.Text.RegularExpressions; using FluentAssertions; using Xunit; using Xunit.Abstractions; -namespace Microsoft.DotNet.Docker.Tests +namespace Microsoft.DotNet.Docker.Tests; + +public abstract class ProductImageTests { - public abstract class ProductImageTests + protected ProductImageTests(ITestOutputHelper outputHelper) { - protected ProductImageTests(ITestOutputHelper outputHelper) - { - DockerHelper = new DockerHelper(outputHelper); - OutputHelper = outputHelper; - SyftHelper = new SyftHelper(DockerHelper, OutputHelper); - } + DockerHelper = new DockerHelper(outputHelper); + OutputHelper = outputHelper; + SyftHelper = new SyftHelper(DockerHelper, OutputHelper); + } + + protected DockerHelper DockerHelper { get; } + protected ITestOutputHelper OutputHelper { get; } + protected SyftHelper SyftHelper { get; } + protected abstract DotNetImageRepo ImageRepo { get; } - protected DockerHelper DockerHelper { get; } - protected ITestOutputHelper OutputHelper { get; } - protected SyftHelper SyftHelper { get; } - protected abstract DotNetImageRepo ImageRepo { get; } + /// + /// Verifies that a SAS token isn't accidentally leaked through the Docker history of the image. + /// + protected void VerifyCommonNoSasToken(ProductImageData imageData) + { + string imageTag = imageData.GetImage(ImageRepo, DockerHelper); + string historyContents = DockerHelper.GetHistory(imageTag); + Match match = Regex.Match(historyContents, @"=\?(sig|\S+&sig)=\S+"); + Assert.False(match.Success, $"A SAS token was detected in the Docker history of '{imageTag}'."); + } - /// - /// Verifies that a SAS token isn't accidentally leaked through the Docker history of the image. - /// - protected void VerifyCommonNoSasToken(ProductImageData imageData) + protected void VerifyCommonInsecureFiles(ProductImageData imageData) + { + if (imageData.OS.Contains("alpine") && imageData.IsArm) { - string imageTag = imageData.GetImage(ImageRepo, DockerHelper); - string historyContents = DockerHelper.GetHistory(imageTag); - Match match = Regex.Match(historyContents, @"=\?(sig|\S+&sig)=\S+"); - Assert.False(match.Success, $"A SAS token was detected in the Docker history of '{imageTag}'."); + return; } - protected void VerifyCommonInsecureFiles(ProductImageData imageData) + if (imageData.OS.Contains("resolute")) { - if (imageData.OS.Contains("alpine") && imageData.IsArm) - { - return; - } - - if (imageData.OS.Contains("resolute")) - { - OutputHelper.WriteLine("Skipping insecure files check for Ubuntu 26.04 (resolute)." - + " Remove this check when https://github.com/dotnet/dotnet-docker/issues/7054 is resolved."); - return; - } - - string rootFsPath = imageData.IsDistroless ? "/rootfs" : "/"; - - string worldWritableDirectoriesWithoutStickyBitCmd = $@"find {rootFsPath} -xdev -type d \( -perm -0002 -a ! -perm -1000 \)"; - string worldWritableFilesCmd = $"find {rootFsPath} -xdev -type f -perm -o+w"; - string noUserOrGroupFilesCmd; - if (imageData.OS.Contains("alpine")) - { - // BusyBox in Alpine doesn't support the more convenient -nouser and -nogroup options for the find command - noUserOrGroupFilesCmd = $@"find {rootFsPath} -xdev -exec stat -c %U-%n {{}} \+ | {{ grep ^UNKNOWN || true; }}"; - } - else - { - noUserOrGroupFilesCmd = $@"find {rootFsPath} -xdev \( -nouser -o -nogroup \)"; - } + OutputHelper.WriteLine("Skipping insecure files check for Ubuntu 26.04 (resolute)." + + " Remove this check when https://github.com/dotnet/dotnet-docker/issues/7054 is resolved."); + return; + } - string command = $"-c \"{worldWritableDirectoriesWithoutStickyBitCmd} && {worldWritableFilesCmd} && {noUserOrGroupFilesCmd}\""; + string rootFsPath = imageData.IsDistroless ? "/rootfs" : "/"; - string imageTag; - if (imageData.IsDistroless) - { - imageTag = DockerHelper.BuildDistrolessHelper(ImageRepo, imageData, rootFsPath); - } - else - { - imageTag = imageData.GetImage(ImageRepo, DockerHelper); - } - - string output = DockerHelper.Run( - image: imageTag, - name: imageData.GetIdentifier($"InsecureFiles-{ImageRepo}"), - command: command, - runAsUser: "root", - optionalRunArgs: "--entrypoint /bin/sh" - ); - - string rootFsPathWithTrailingSlash = rootFsPath; - if (!rootFsPathWithTrailingSlash.EndsWith("/")) - { - rootFsPathWithTrailingSlash += "/"; - } - - IEnumerable lines = output - .ReplaceLineEndings() - .Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries) - // Writable files in the /tmp/.dotnet directory are allowed for global mutexes - .Where(line => !line.StartsWith($"{rootFsPathWithTrailingSlash}tmp/.dotnet")) - // Exclude the non-root user's home directory. It will show up as a directory without a user or group - // because we're not examining the distroless container directly, but rather copying its contents into - // a container with a shell but doesn't have the non-root user defined. - .Where(line => !imageData.IsDistroless || line != $"{rootFsPathWithTrailingSlash}home/app"); - - Assert.Empty(lines); + string worldWritableDirectoriesWithoutStickyBitCmd = $@"find {rootFsPath} -xdev -type d \( -perm -0002 -a ! -perm -1000 \)"; + string worldWritableFilesCmd = $"find {rootFsPath} -xdev -type f -perm -o+w"; + string noUserOrGroupFilesCmd; + if (imageData.OS.Contains("alpine")) + { + // BusyBox in Alpine doesn't support the more convenient -nouser and -nogroup options for the find command + noUserOrGroupFilesCmd = $@"find {rootFsPath} -xdev -exec stat -c %U-%n {{}} \+ | {{ grep ^UNKNOWN || true; }}"; + } + else + { + noUserOrGroupFilesCmd = $@"find {rootFsPath} -xdev \( -nouser -o -nogroup \)"; } - protected void VerifyCommonDefaultUser(ProductImageData imageData) + string command = $"-c \"{worldWritableDirectoriesWithoutStickyBitCmd} && {worldWritableFilesCmd} && {noUserOrGroupFilesCmd}\""; + + string imageTag = imageData.IsDistroless + ? DockerHelper.BuildDistrolessHelper(ImageRepo, imageData, rootFsPath) + : imageData.GetImage(ImageRepo, DockerHelper); + string output = DockerHelper.Run( + image: imageTag, + name: imageData.GetIdentifier($"InsecureFiles-{ImageRepo}"), + command: command, + runAsUser: "root", + optionalRunArgs: "--entrypoint /bin/sh" + ); + + string rootFsPathWithTrailingSlash = rootFsPath; + if (!rootFsPathWithTrailingSlash.EndsWith("/")) { - string imageTag = imageData.GetImage(ImageRepo, DockerHelper); - string actualUser = DockerHelper.GetImageUser(imageTag); + rootFsPathWithTrailingSlash += "/"; + } - string expectedUser; - if (imageData.IsDistroless && ImageRepo != DotNetImageRepo.SDK) - { - expectedUser = imageData.NonRootUID.ToString(); - } + IEnumerable lines = output + .ReplaceLineEndings() + .Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries) + // Writable files in the /tmp/.dotnet directory are allowed for global mutexes + .Where(line => !line.StartsWith($"{rootFsPathWithTrailingSlash}tmp/.dotnet")) + // Exclude the non-root user's home directory. It will show up as a directory without a user or group + // because we're not examining the distroless container directly, but rather copying its contents into + // a container with a shell but doesn't have the non-root user defined. + .Where(line => !imageData.IsDistroless || line != $"{rootFsPathWithTrailingSlash}home/app"); + + Assert.Empty(lines); + } + + protected void VerifyCommonDefaultUser(ProductImageData imageData) + { + string imageTag = imageData.GetImage(ImageRepo, DockerHelper); + string actualUser = DockerHelper.GetImageUser(imageTag); + + string expectedUser = imageData.IsDistroless && ImageRepo != DotNetImageRepo.SDK + ? imageData.NonRootUID.ToString() // For Windows, only Nano Server defines a user, which seems wrong. // I've logged https://dev.azure.com/microsoft/OS/_workitems/edit/40146885 for this. - else if (imageData.OS.Family == OSFamily.NanoServer) - { - expectedUser = "ContainerUser"; - } - else - { - expectedUser = string.Empty; - } + : imageData.OS.Family == OSFamily.NanoServer ? "ContainerUser" : string.Empty; + Assert.Equal(expectedUser, actualUser); - Assert.Equal(expectedUser, actualUser); + VerifyNonRootUID(imageData); + } - VerifyNonRootUID(imageData); + protected void VerifyNonRootUID(ProductImageData imageData) + { + if (imageData.IsWindows) + { + OutputHelper.WriteLine("UID check is only relevant for Linux images"); + return; } - protected void VerifyNonRootUID(ProductImageData imageData) + string imageTag = imageData.GetImage(ImageRepo, DockerHelper); + string rootPath = "/"; + + if (imageData.IsDistroless) { - if (imageData.IsWindows) - { - OutputHelper.WriteLine("UID check is only relevant for Linux images"); - return; - } + rootPath = "/rootfs/"; + imageTag = DockerHelper.BuildDistrolessHelper(ImageRepo, imageData, rootPath); + } - string imageTag = imageData.GetImage(ImageRepo, DockerHelper); - string rootPath = "/"; + string command = $"-c \"grep '^app' {rootPath}etc/passwd | cut -d: -f3\""; - if (imageData.IsDistroless) - { - rootPath = "/rootfs/"; - imageTag = DockerHelper.BuildDistrolessHelper(ImageRepo, imageData, rootPath); - } + string uidString = DockerHelper.Run( + image: imageTag, + command: command, + name: imageData.GetIdentifier($"VerifyUID-{ImageRepo}"), + optionalRunArgs: $"--entrypoint /bin/sh" + ); - string command = $"-c \"grep '^app' {rootPath}etc/passwd | cut -d: -f3\""; + int uid = int.Parse(uidString); - string uidString = DockerHelper.Run( - image: imageTag, - command: command, - name: imageData.GetIdentifier($"VerifyUID-{ImageRepo}"), - optionalRunArgs: $"--entrypoint /bin/sh" - ); + // UIDs below 1000 are reserved for system accounts + Assert.True(uid >= 1000); + // Debian has a UID_MAX of 60000 + Assert.True(uid <= 60000); + } - int uid = int.Parse(uidString); + public static IEnumerable GetCommonEnvironmentVariables() + { + yield return EnvironmentVariableInfo.Require("DOTNET_RUNNING_IN_CONTAINER", "true"); + } - // UIDs below 1000 are reserved for system accounts - Assert.True(uid >= 1000); - // Debian has a UID_MAX of 60000 - Assert.True(uid <= 60000); + /// + /// Verifies that the packages installed are correct and scannable by security tools. + /// + internal void VerifyInstalledPackagesBase( + ProductImageData imageData, + DotNetImageRepo imageRepo, + IEnumerable extraExcludePaths = null) + { + if (imageData.OS.IsUnstable) + { + OutputHelper.WriteLine( + $"Skipping installed-packages verification for unstable OS '{imageData.OS.DisplayName}'." + + " The package set for pre-release distros is expected to change and is not asserted by tests."); + return; } - public static IEnumerable GetCommonEnvironmentVariables() + if (imageData.OS == OS.ResoluteChiseled) { - yield return EnvironmentVariableInfo.Require("DOTNET_RUNNING_IN_CONTAINER", "true"); + OutputHelper.WriteLine( + "Temporarily skipping installed-packages verification for Ubuntu 26.04 chiseled images." + + " Re-enable when images include the fix for https://github.com/canonical/rocks-toolbox/issues/52."); + return; } - /// - /// Verifies that the packages installed are correct and scannable by security tools. - /// - internal void VerifyInstalledPackagesBase( - ProductImageData imageData, - DotNetImageRepo imageRepo, - IEnumerable extraExcludePaths = null) - { - if (imageData.OS.IsUnstable) - { - OutputHelper.WriteLine( - $"Skipping installed-packages verification for unstable OS '{imageData.OS.DisplayName}'." - + " The package set for pre-release distros is expected to change and is not asserted by tests."); - return; - } + IEnumerable expectedPackages = GetExpectedPackages(imageData, imageRepo); + IEnumerable actualPackages = GetInstalledPackages(imageData, imageRepo, extraExcludePaths); - if (imageData.OS == OS.ResoluteChiseled) - { - OutputHelper.WriteLine( - "Temporarily skipping installed-packages verification for Ubuntu 26.04 chiseled images." - + " Re-enable when images include the fix for https://github.com/canonical/rocks-toolbox/issues/52."); - return; - } + string imageName = imageData.GetImage(imageRepo, DockerHelper, skipPull: true); + ComparePackages(expectedPackages, actualPackages, imageData.IsDistroless, imageName, OutputHelper); + } - IEnumerable expectedPackages = GetExpectedPackages(imageData, imageRepo); - IEnumerable actualPackages = GetInstalledPackages(imageData, imageRepo, extraExcludePaths); + internal static void ComparePackages( + IEnumerable expectedPackages, + IEnumerable actualPackages, + bool isDistroless, + string imageName, + ITestOutputHelper outputHelper) + { + outputHelper.WriteLine($"Expected Packages: [ {string.Join(", ", expectedPackages)} ]"); + outputHelper.WriteLine($"Actual Packages: [ {string.Join(", ", actualPackages)} ]"); - string imageName = imageData.GetImage(imageRepo, DockerHelper, skipPull: true); - ComparePackages(expectedPackages, actualPackages, imageData.IsDistroless, imageName, OutputHelper); + // Verify we only include strictly necessary packages in distroless images + if (isDistroless) + { + actualPackages.Should().BeEquivalentTo(expectedPackages, because: $"image {imageName} is distroless"); + return; } - internal static void ComparePackages( - IEnumerable expectedPackages, - IEnumerable actualPackages, - bool isDistroless, - string imageName, - ITestOutputHelper outputHelper) - { - outputHelper.WriteLine($"Expected Packages: [ {string.Join(", ", expectedPackages)} ]"); - outputHelper.WriteLine($"Actual Packages: [ {string.Join(", ", actualPackages)} ]"); + // Verify satisfy .NET dependencies on non-distroless images. + // There will be additional packages from the distro. + expectedPackages.Should().BeSubsetOf(actualPackages, because: $"image {imageName} is not distroless"); + } - // Verify we only include strictly necessary packages in distroless images - if (isDistroless) - { - actualPackages.Should().BeEquivalentTo(expectedPackages, because: $"image {imageName} is distroless"); - return; - } + /// + /// Gets a list of all unique linux packages in the image. + /// + /// The image data of the image to scan. + /// The repository of the image to scan. + /// + /// These paths will be excluded from image scanning. + /// + /// + internal IEnumerable GetInstalledPackages( + ProductImageData imageData, + DotNetImageRepo imageRepo, + IEnumerable extraExcludePaths = null) + { + JsonNode output = SyftHelper.Scan(imageData, imageRepo, extraExcludePaths); + + return ((JsonArray)output["artifacts"]) + .Select(artifact => artifact["name"]?.ToString()) + // Syft can sometimes detect duplicates of packages if they have a distro-specific version suffix. Syft + // will report the distro package version and the binary package version as separate. + // + // openssl 1.1.1k binary + // openssl 1.1.1k-29.azl3 rpm + // ^ Azure Linux specific version + // + // So we need to remove duplicates. + .Distinct(); + } - // Verify satisfy .NET dependencies on non-distroless images. - // There will be additional packages from the distro. - expectedPackages.Should().BeSubsetOf(actualPackages, because: $"image {imageName} is not distroless"); - } + internal static IEnumerable GetExpectedPackages(ProductImageData imageData, DotNetImageRepo imageRepo) + { + IEnumerable expectedPackages = GetRuntimeDepsPackages(imageData); - /// - /// Gets a list of all unique linux packages in the image. - /// - /// - /// These paths will be excluded from image scanning. - /// - /// - internal IEnumerable GetInstalledPackages( - ProductImageData imageData, - DotNetImageRepo imageRepo, - IEnumerable extraExcludePaths = null) + if (imageData.IsDistroless) { - JsonNode output = SyftHelper.Scan(imageData, imageRepo, extraExcludePaths); - - return ((JsonArray)output["artifacts"]) - .Select(artifact => artifact["name"]?.ToString()) - // Syft can sometimes detect duplicates of packages if they have a distro-specific version suffix. Syft - // will report the distro package version and the binary package version as separate. - // - // openssl 1.1.1k binary - // openssl 1.1.1k-29.azl3 rpm - // ^ Azure Linux specific version - // - // So we need to remove duplicates. - .Distinct(); + expectedPackages = [.. expectedPackages, .. GetDistrolessBasePackages(imageData)]; } - internal static IEnumerable GetExpectedPackages(ProductImageData imageData, DotNetImageRepo imageRepo) + if (imageData.ImageVariant.HasFlag(DotNetImageVariant.Extra) || imageRepo == DotNetImageRepo.SDK) { - IEnumerable expectedPackages = GetRuntimeDepsPackages(imageData); + expectedPackages = [.. expectedPackages, .. GetExtraPackages(imageData)]; + } - if (imageData.IsDistroless) - { - expectedPackages = [..expectedPackages, ..GetDistrolessBasePackages(imageData)]; - } + return expectedPackages.Distinct().OrderBy(s => s); + } - if (imageData.ImageVariant.HasFlag(DotNetImageVariant.Extra) || imageRepo == DotNetImageRepo.SDK) + private static IEnumerable GetDistrolessBasePackages(ProductImageData imageData) => imageData switch + { + { OS.Family: OSFamily.AzureLinux } => new[] { - expectedPackages = [..expectedPackages, ..GetExtraPackages(imageData)]; - } - - return expectedPackages.Distinct().OrderBy(s => s); + "SymCrypt", + "SymCrypt-OpenSSL", + "azurelinux-release", + "distroless-packages-minimal", + "filesystem", + "prebuilt-ca-certificates", + "tzdata" + }, + { OS.Family: OSFamily.Ubuntu } => + [ + "base-files" + ], + _ => throw new NotSupportedException($"Unsupported distroless OS '{imageData.OS}'") + }; + + private static IEnumerable GetRuntimeDepsPackages(ProductImageData imageData) + { + IEnumerable packages = imageData switch + { + { OS.Family: OSFamily.AzureLinux } => + [ + "glibc", + "libgcc", + "openssl", + "openssl-libs", + "libstdc++" + ], + { OS: { Family: OSFamily.Ubuntu, Version: "22.04" } } => + [ + "ca-certificates", + "gcc-12-base", + "libc6", + "libgcc-s1", + "libssl3", + "openssl", + "libstdc++6" + ], + { OS: var os } when os == OS.ResoluteChiseled => + [ + "ca-certificates", + "gcc-16-base", + ..GetResoluteChiseledArchSpecificPackages(imageData), + "libc6", + "libgcc-s1", + "libssl3t64", + "libstdc++6", + "libzstd", + "libzstd1", + "openssl", + "zlib", + "zlib1g" + ], + { OS: { Family: OSFamily.Ubuntu, Version: "26.04" } } => + [ + "ca-certificates", + "gcc-16-base", + "libc6", + "libgcc-s1", + "libssl3t64", + "openssl", + "libstdc++6" + ], + { OS: var os } when os == OS.NobleChiseled => + [ + "ca-certificates", + "gcc-14-base", + "gcc-14", + "libc6", + "libgcc-s1", + "libssl3t64", + "openssl", + "libstdc++6" + ], + { OS: { Family: OSFamily.Ubuntu, Version: "24.04" } } => + [ + "ca-certificates", + "gcc-14-base", + "libc6", + "libgcc-s1", + "libssl3t64", + "openssl", + "libstdc++6" + ], + { OS.Family: OSFamily.Alpine } => + [ + "ca-certificates-bundle", + "libgcc", + "libssl3", + "libstdc++" + ], + { OS: var os } when os == OS.BookwormSlim => + [ + "ca-certificates", + "libc6", + "libgcc-s1", + "libicu72", + "libssl3", + "tzdata", + "libstdc++6" + ], + _ => throw new NotSupportedException($"Unknown runtime-deps packages for OS '{imageData.OS}'") + }; + + // zlib is not required for .NET 9+ + // - https://github.com/dotnet/dotnet-docker/issues/5687 + if (imageData.Version.Major == 8) + { + packages = [.. packages, GetZLibPackage(imageData.OS)]; } - private static IEnumerable GetDistrolessBasePackages(ProductImageData imageData) => imageData switch - { - { OS.Family: OSFamily.AzureLinux } => new[] - { - "SymCrypt", - "SymCrypt-OpenSSL", - "azurelinux-release", - "distroless-packages-minimal", - "filesystem", - "prebuilt-ca-certificates", - "tzdata" - }, - { OS.Family: OSFamily.Ubuntu } => new[] - { - "base-files" - }, - _ => throw new NotSupportedException($"Unsupported distroless OS '{imageData.OS}'") - }; - - private static IEnumerable GetRuntimeDepsPackages(ProductImageData imageData) { - IEnumerable packages = imageData switch - { - { OS.Family: OSFamily.AzureLinux } => - [ - "glibc", - "libgcc", - "openssl", - "openssl-libs", - "libstdc++" - ], - { OS: { Family: OSFamily.Ubuntu, Version: "22.04"} } => - [ - "ca-certificates", - "gcc-12-base", - "libc6", - "libgcc-s1", - "libssl3", - "openssl", - "libstdc++6" - ], - { OS: var os } when os == OS.ResoluteChiseled => - [ - "ca-certificates", - "gcc-16-base", - ..GetResoluteChiseledArchSpecificPackages(imageData), - "libc6", - "libgcc-s1", - "libssl3t64", - "libstdc++6", - "libzstd", - "libzstd1", - "openssl", - "zlib", - "zlib1g" - ], - { OS: { Family: OSFamily.Ubuntu, Version: "26.04" } } => - [ - "ca-certificates", - "gcc-16-base", - "libc6", - "libgcc-s1", - "libssl3t64", - "openssl", - "libstdc++6" - ], - { OS: var os } when os == OS.NobleChiseled => - [ - "ca-certificates", - "gcc-14-base", - "gcc-14", - "libc6", - "libgcc-s1", - "libssl3t64", - "openssl", - "libstdc++6" - ], - { OS: { Family: OSFamily.Ubuntu, Version: "24.04" } } => - [ - "ca-certificates", - "gcc-14-base", - "libc6", - "libgcc-s1", - "libssl3t64", - "openssl", - "libstdc++6" - ], - { OS.Family: OSFamily.Alpine } => - [ - "ca-certificates-bundle", - "libgcc", - "libssl3", - "libstdc++" - ], - { OS: var os } when os == OS.BookwormSlim => - [ - "ca-certificates", - "libc6", - "libgcc-s1", - "libicu72", - "libssl3", - "tzdata", - "libstdc++6" - ], - _ => throw new NotSupportedException($"Unknown runtime-deps packages for OS '{imageData.OS}'") - }; - - // zlib is not required for .NET 9+ - // - https://github.com/dotnet/dotnet-docker/issues/5687 - if (imageData.Version.Major == 8) - { - packages = [..packages, GetZLibPackage(imageData.OS)]; - } - - return packages; - } + return packages; + } - private static string GetZLibPackage(OSInfo os) - { - OSFamily[] unversionedZLibOSFamilies = [OSFamily.Alpine, OSFamily.AzureLinux]; - return unversionedZLibOSFamilies.Contains(os.Family) ? "zlib" : "zlib1g"; - } + private static string GetZLibPackage(OSInfo os) + { + OSFamily[] unversionedZLibOSFamilies = [OSFamily.Alpine, OSFamily.AzureLinux]; + return unversionedZLibOSFamilies.Contains(os.Family) ? "zlib" : "zlib1g"; + } - /// - /// Syft detects gcc-16 as an additional binary package on amd64 resolute chiseled images. - /// - private static IEnumerable GetResoluteChiseledArchSpecificPackages(ProductImageData imageData) => - imageData.Arch == Arch.Amd64 ? ["gcc-16"] : []; + /// + /// Syft detects gcc-16 as an additional binary package on amd64 resolute chiseled images. + /// + private static IEnumerable GetResoluteChiseledArchSpecificPackages(ProductImageData imageData) => + imageData.Arch == Arch.Amd64 ? ["gcc-16"] : []; - private static IEnumerable GetExtraPackages(ProductImageData imageData) => imageData switch + private static IEnumerable GetExtraPackages(ProductImageData imageData) => imageData switch + { + { IsDistroless: true, OS.Family: OSFamily.AzureLinux } => new[] { - { IsDistroless: true, OS.Family: OSFamily.AzureLinux } => new[] - { - "icu", - "tzdata" - }, - { OS: var os } when os == OS.ResoluteChiseled => new[] - { - "icu", - "libicu78", - "tzdata" - }, - { OS: var os } when os == OS.NobleChiseled => new[] - { - "libicu74", - "tzdata" - }, - { OS: var os } when os == OS.JammyChiseled => new[] - { - "libicu70", - "tzdata" - }, - { OS.Family: OSFamily.Alpine } => new[] - { - "icu-data-full", - "icu-libs", - "tzdata" - }, - _ => Array.Empty() - }; - } + "icu", + "tzdata" + }, + { OS: var os } when os == OS.ResoluteChiseled => + [ + "icu", + "libicu78", + "tzdata" + ], + { OS: var os } when os == OS.NobleChiseled => + [ + "libicu74", + "tzdata" + ], + { OS: var os } when os == OS.JammyChiseled => + [ + "libicu70", + "tzdata" + ], + { OS.Family: OSFamily.Alpine } => + [ + "icu-data-full", + "icu-libs", + "tzdata" + ], + _ => [] + }; } diff --git a/tests/Microsoft.DotNet.Docker.Tests/RuntimeDepsImageTests.cs b/tests/Microsoft.DotNet.Docker.Tests/RuntimeDepsImageTests.cs index 4903389b32..ff3ed9c4c7 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/RuntimeDepsImageTests.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/RuntimeDepsImageTests.cs @@ -1,184 +1,159 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. using System.Collections.Generic; using System.Text.Json.Nodes; using System.Threading.Tasks; +using Microsoft.DotNet.Docker.Tests.TestScenarios; using Shouldly; using Xunit; using Xunit.Abstractions; -namespace Microsoft.DotNet.Docker.Tests -{ - [Trait("Category", "runtime-deps")] - public class RuntimeDepsImageTests : CommonRuntimeImageTests - { - public RuntimeDepsImageTests(ITestOutputHelper outputHelper) - : base(outputHelper) - { - } +namespace Microsoft.DotNet.Docker.Tests; - protected override DotNetImageRepo ImageRepo => DotNetImageRepo.Runtime_Deps; +[Trait("Category", "runtime-deps")] +public class RuntimeDepsImageTests(ITestOutputHelper outputHelper) : CommonRuntimeImageTests(outputHelper) +{ + protected override DotNetImageRepo ImageRepo => DotNetImageRepo.Runtime_Deps; - public static IEnumerable GetImageData() => - GetImageData(DotNetImageRepo.Runtime_Deps); + public static IEnumerable GetImageData() => + GetImageData(DotNetImageRepo.Runtime_Deps); - public static IEnumerable GetAotImageData() => - GetImageData(DotNetImageRepo.Runtime_Deps, DotNetImageVariant.AOT); + public static IEnumerable GetAotImageData() => + GetImageData(DotNetImageRepo.Runtime_Deps, DotNetImageVariant.AOT); - [LinuxImageTheory] - [MemberData(nameof(GetImageData))] - public async Task VerifySelfContainedConsoleScenario(ProductImageData imageData) + [LinuxImageTheory] + [MemberData(nameof(GetImageData))] + public async Task VerifySelfContainedConsoleScenario(ProductImageData imageData) + { + if (imageData.ImageVariant.HasFlag(DotNetImageVariant.AOT)) { - if (imageData.ImageVariant.HasFlag(DotNetImageVariant.AOT)) - { - OutputHelper.WriteLine( - $"Test is not applicable to AOT images. See {nameof(VerifyAotWebScenario)} instead."); - return; - } - - ConsoleAppScenario testScenario = - new ConsoleAppScenario.SelfContained(imageData, DockerHelper, OutputHelper); - await testScenario.ExecuteAsync(); + OutputHelper.WriteLine( + $"Test is not applicable to AOT images. See {nameof(VerifyAotWebScenario)} instead."); + return; } - [LinuxImageTheory] - [MemberData(nameof(GetImageData))] - public async Task VerifySelfContainedWebScenario(ProductImageData imageData) - { - if (imageData.ImageVariant.HasFlag(DotNetImageVariant.AOT)) - { - OutputHelper.WriteLine( - $"Test is not applicable to AOT images. See {nameof(VerifyAotWebScenario)} instead."); - return; - } - - WebScenario testScenario = new WebScenario.SelfContained(imageData, DockerHelper, OutputHelper); - await testScenario.ExecuteAsync(); - } + ConsoleAppScenario testScenario = + new ConsoleAppScenario.SelfContained(imageData, DockerHelper, OutputHelper); + await testScenario.ExecuteAsync(); + } - [LinuxImageTheory] - [MemberData(nameof(GetAotImageData))] - public async Task VerifyAotWebScenario(ProductImageData imageData) + [LinuxImageTheory] + [MemberData(nameof(GetImageData))] + public async Task VerifySelfContainedWebScenario(ProductImageData imageData) + { + if (imageData.ImageVariant.HasFlag(DotNetImageVariant.AOT)) { - if (imageData.Arch == Arch.Arm) - { - OutputHelper.WriteLine("Skipping test due to https://github.com/dotnet/docker-tools/issues/1177. " - + "ImageBuilder is unable to queue arm32 AOT images together with the arm64 AOT SDKs. " - + "Re-enable once fixed."); - return; - } - - WebScenario scenario = new WebScenario.Aot(imageData, DockerHelper, OutputHelper); - await scenario.ExecuteAsync(); + OutputHelper.WriteLine( + $"Test is not applicable to AOT images. See {nameof(VerifyAotWebScenario)} instead."); + return; } - [LinuxImageTheory] - [MemberData(nameof(GetImageData))] - public void VerifyEnvironmentVariables(ProductImageData imageData) - { - base.VerifyCommonEnvironmentVariables(imageData); - } + WebScenario testScenario = new WebScenario.SelfContained(imageData, DockerHelper, OutputHelper); + await testScenario.ExecuteAsync(); + } - [LinuxImageTheory] - [MemberData(nameof(GetImageData))] - public void VerifyInsecureFiles(ProductImageData imageData) + [LinuxImageTheory] + [MemberData(nameof(GetAotImageData))] + public async Task VerifyAotWebScenario(ProductImageData imageData) + { + if (imageData.Arch == Arch.Arm) { - base.VerifyCommonInsecureFiles(imageData); + OutputHelper.WriteLine("Skipping test due to https://github.com/dotnet/docker-tools/issues/1177. " + + "ImageBuilder is unable to queue arm32 AOT images together with the arm64 AOT SDKs. " + + "Re-enable once fixed."); + return; } - [LinuxImageTheory] - [MemberData(nameof(GetImageData))] - public void VerifyShellNotInstalledForDistroless(ProductImageData imageData) - { - base.VerifyCommonShellNotInstalledForDistroless(imageData); - } + WebScenario scenario = new WebScenario.Aot(imageData, DockerHelper, OutputHelper); + await scenario.ExecuteAsync(); + } - [LinuxImageTheory] - [MemberData(nameof(GetImageData))] - public void VerifyNoSasToken(ProductImageData imageData) - { - base.VerifyCommonNoSasToken(imageData); - } + [LinuxImageTheory] + [MemberData(nameof(GetImageData))] + public void VerifyEnvironmentVariables(ProductImageData imageData) => VerifyCommonEnvironmentVariables(imageData); - [LinuxImageTheory] - [MemberData(nameof(GetImageData))] - public void VerifyDefaultUser(ProductImageData imageData) - { - VerifyCommonDefaultUser(imageData); - } + [LinuxImageTheory] + [MemberData(nameof(GetImageData))] + public void VerifyInsecureFiles(ProductImageData imageData) => VerifyCommonInsecureFiles(imageData); - [LinuxImageTheory] - [MemberData(nameof(GetImageData))] - public void VerifyDistrolessOSReleaseInfo(ProductImageData imageData) - { - if (!imageData.IsDistroless) - { - OutputHelper.WriteLine("This test is only relevant to distroless images."); - return; - } - - var osReleaseInfo = GetOSReleaseInfo(imageData, ImageRepo); - OutputHelper.WriteLine($"OS Release Info: {osReleaseInfo}"); - osReleaseInfo.ShouldNotBeEmpty(); - } + [LinuxImageTheory] + [MemberData(nameof(GetImageData))] + public void VerifyShellNotInstalledForDistroless(ProductImageData imageData) => VerifyCommonShellNotInstalledForDistroless(imageData); + + [LinuxImageTheory] + [MemberData(nameof(GetImageData))] + public void VerifyNoSasToken(ProductImageData imageData) => VerifyCommonNoSasToken(imageData); - /// - /// Verifies the presence of the Chisel manifest in Ubuntu Chiseled images. Chisel manifest documentation: - /// https://discourse.ubuntu.com/t/chisel-manifest-is-supported-in-newly-released-v1-0-0/48944 - /// - [LinuxImageTheory] - [MemberData(nameof(GetImageData))] - public void VerifyChiselManifest(ProductImageData imageData) + [LinuxImageTheory] + [MemberData(nameof(GetImageData))] + public void VerifyDefaultUser(ProductImageData imageData) => VerifyCommonDefaultUser(imageData); + + [LinuxImageTheory] + [MemberData(nameof(GetImageData))] + public void VerifyDistrolessOSReleaseInfo(ProductImageData imageData) + { + if (!imageData.IsDistroless) { - if (!(imageData.OS.Family == OSFamily.Ubuntu && imageData.IsDistroless)) - { - OutputHelper.WriteLine("Test is only relevant to Ubuntu Chiseled images"); - return; - } - - // https://github.com/dotnet/dotnet-docker/issues/5973#issuecomment-2501510550 - bool shouldContainManifest = imageData.Version.Major != 8 && imageData.Version.Major != 9; - - const string RootFs = "/rootfs"; - const string ChiselManifestFileName = "/var/lib/chisel/manifest.wall"; - - // Setup a distroless helper image to inspect the filesystem of the Chiseled image - string distrolessHelperImageTag = DockerHelper.BuildDistrolessHelper(ImageRepo, imageData, RootFs); - - // Check for the presence of the Chisel manifest by listing the files in the directory - // and then verifying the output. - string actualOutput = DockerHelper.Run( - image: distrolessHelperImageTag, - name: imageData.GetIdentifier(nameof(VerifyChiselManifest)), - command: $"find {RootFs}/var/lib/ -path *chisel* -type f"); - - if (shouldContainManifest) - { - Assert.Contains(ChiselManifestFileName, actualOutput); - } - else - { - Assert.DoesNotContain(ChiselManifestFileName, actualOutput); - } + OutputHelper.WriteLine("This test is only relevant to distroless images."); + return; } - private string GetOSReleaseInfo( - ProductImageData imageData, - DotNetImageRepo imageRepo) + string osReleaseInfo = GetOSReleaseInfo(imageData, ImageRepo); + OutputHelper.WriteLine($"OS Release Info: {osReleaseInfo}"); + osReleaseInfo.ShouldNotBeEmpty(); + } + + /// + /// Verifies the presence of the Chisel manifest in Ubuntu Chiseled images. Chisel manifest documentation: + /// https://discourse.ubuntu.com/t/chisel-manifest-is-supported-in-newly-released-v1-0-0/48944 + /// + [LinuxImageTheory] + [MemberData(nameof(GetImageData))] + public void VerifyChiselManifest(ProductImageData imageData) + { + if (!(imageData.OS.Family == OSFamily.Ubuntu && imageData.IsDistroless)) { - JsonNode output = SyftHelper.Scan(imageData, imageRepo); - JsonObject distro = (JsonObject)output["distro"]; - return (string)distro["version"]; + OutputHelper.WriteLine("Test is only relevant to Ubuntu Chiseled images"); + return; } + // https://github.com/dotnet/dotnet-docker/issues/5973#issuecomment-2501510550 + bool shouldContainManifest = imageData.Version.Major is not 8 and not 9; - [LinuxImageTheory] - [MemberData(nameof(GetImageData))] - public void VerifyInstalledPackages(ProductImageData imageData) + const string rootFs = "/rootfs"; + const string chiselManifestFileName = "/var/lib/chisel/manifest.wall"; + + // Setup a distroless helper image to inspect the filesystem of the Chiseled image + string distrolessHelperImageTag = DockerHelper.BuildDistrolessHelper(ImageRepo, imageData, rootFs); + + // Check for the presence of the Chisel manifest by listing the files in the directory + // and then verifying the output. + string actualOutput = DockerHelper.Run( + image: distrolessHelperImageTag, + name: imageData.GetIdentifier(nameof(VerifyChiselManifest)), + command: $"find {rootFs}/var/lib/ -path *chisel* -type f"); + + if (shouldContainManifest) + { + Assert.Contains(chiselManifestFileName, actualOutput); + } + else { - VerifyInstalledPackagesBase(imageData, ImageRepo); + Assert.DoesNotContain(chiselManifestFileName, actualOutput); } } + + private string GetOSReleaseInfo( + ProductImageData imageData, + DotNetImageRepo imageRepo) + { + JsonNode output = SyftHelper.Scan(imageData, imageRepo); + JsonObject distro = (JsonObject)output["distro"]; + return (string)distro["version"]; + } + + [LinuxImageTheory] + [MemberData(nameof(GetImageData))] + public void VerifyInstalledPackages(ProductImageData imageData) => VerifyInstalledPackagesBase(imageData, ImageRepo); } diff --git a/tests/Microsoft.DotNet.Docker.Tests/RuntimeImageTests.cs b/tests/Microsoft.DotNet.Docker.Tests/RuntimeImageTests.cs index cbfc7a7522..ace91def95 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/RuntimeImageTests.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/RuntimeImageTests.cs @@ -1,110 +1,86 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. -using System; using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; using Microsoft.DotNet.Docker.Tests.TestScenarios; using Xunit; using Xunit.Abstractions; -namespace Microsoft.DotNet.Docker.Tests +namespace Microsoft.DotNet.Docker.Tests; + +[Trait("Category", "runtime")] +public class RuntimeImageTests(ITestOutputHelper outputHelper) : CommonRuntimeImageTests(outputHelper) { - [Trait("Category", "runtime")] - public class RuntimeImageTests : CommonRuntimeImageTests + protected override DotNetImageRepo ImageRepo => DotNetImageRepo.Runtime; + + public static IEnumerable GetImageData() => GetImageData(DotNetImageRepo.Runtime); + + [DotNetTheory] + [MemberData(nameof(GetImageData))] + public async Task VerifyFxDependentAppScenario(ProductImageData imageData) { - public RuntimeImageTests(ITestOutputHelper outputHelper) - : base(outputHelper) - { - } + ConsoleAppScenario testScenario = new ConsoleAppScenario.FxDependent(imageData, DockerHelper, OutputHelper); + await testScenario.ExecuteAsync(); + } - protected override DotNetImageRepo ImageRepo => DotNetImageRepo.Runtime; + [DotNetTheory] + [MemberData(nameof(GetImageData))] + public async Task VerifyTestProjectScenario(ProductImageData imageData) + { + ConsoleAppScenario testScenario = new ConsoleAppScenario.TestProject(imageData, DockerHelper, OutputHelper); + await testScenario.ExecuteAsync(); + } - public static IEnumerable GetImageData() => GetImageData(DotNetImageRepo.Runtime); + [DotNetTheory] + [MemberData(nameof(GetImageData))] + public async Task VerifyGlobalizationScenario(ProductImageData imageData) => + await VerifyGlobalizationScenarioBase(imageData); - [DotNetTheory] - [MemberData(nameof(GetImageData))] - public async Task VerifyFxDependentAppScenario(ProductImageData imageData) - { - ConsoleAppScenario testScenario = new ConsoleAppScenario.FxDependent(imageData, DockerHelper, OutputHelper); - await testScenario.ExecuteAsync(); - } + [WindowsImageTheory] + [MemberData(nameof(GetImageData))] + public async Task VerifyNLSScenario(ProductImageData imageData) => + await VerifyNlsScenarioBase(imageData); - [DotNetTheory] - [MemberData(nameof(GetImageData))] - public async Task VerifyTestProjectScenario(ProductImageData imageData) - { - ConsoleAppScenario testScenario = new ConsoleAppScenario.TestProject(imageData, DockerHelper, OutputHelper); - await testScenario.ExecuteAsync(); - } - - [DotNetTheory] - [MemberData(nameof(GetImageData))] - public async Task VerifyGlobalizationScenario(ProductImageData imageData) => - await VerifyGlobalizationScenarioBase(imageData); - - [WindowsImageTheory] - [MemberData(nameof(GetImageData))] - public async Task VerifyNLSScenario(ProductImageData imageData) => - await VerifyNlsScenarioBase(imageData); - - [DotNetTheory] - [MemberData(nameof(GetImageData))] - public void VerifyEnvironmentVariables(ProductImageData imageData) - { - List variables = new List - { - GetRuntimeVersionVariableInfo(ImageRepo, imageData, DockerHelper) - }; + [DotNetTheory] + [MemberData(nameof(GetImageData))] + public void VerifyEnvironmentVariables(ProductImageData imageData) + { + List variables = + [ + GetRuntimeVersionVariableInfo(ImageRepo, imageData, DockerHelper) + ]; - base.VerifyCommonEnvironmentVariables(imageData, variables); - } + VerifyCommonEnvironmentVariables(imageData, variables); + } - [LinuxImageTheory] - [MemberData(nameof(GetImageData))] - public void VerifyInstalledPackages(ProductImageData imageData) - { - VerifyInstalledPackagesBase(imageData, ImageRepo); - } + [LinuxImageTheory] + [MemberData(nameof(GetImageData))] + public void VerifyInstalledPackages(ProductImageData imageData) => VerifyInstalledPackagesBase(imageData, ImageRepo); - [LinuxImageTheory] - [MemberData(nameof(GetImageData))] - public void VerifyInsecureFiles(ProductImageData imageData) - { - base.VerifyCommonInsecureFiles(imageData); - } + [LinuxImageTheory] + [MemberData(nameof(GetImageData))] + public void VerifyInsecureFiles(ProductImageData imageData) => VerifyCommonInsecureFiles(imageData); - [LinuxImageTheory] - [MemberData(nameof(GetImageData))] - public void VerifyShellNotInstalledForDistroless(ProductImageData imageData) - { - base.VerifyCommonShellNotInstalledForDistroless(imageData); - } + [LinuxImageTheory] + [MemberData(nameof(GetImageData))] + public void VerifyShellNotInstalledForDistroless(ProductImageData imageData) => VerifyCommonShellNotInstalledForDistroless(imageData); - [DotNetTheory] - [MemberData(nameof(GetImageData))] - public void VerifyNoSasToken(ProductImageData imageData) - { - base.VerifyCommonNoSasToken(imageData); - } + [DotNetTheory] + [MemberData(nameof(GetImageData))] + public void VerifyNoSasToken(ProductImageData imageData) => VerifyCommonNoSasToken(imageData); - [DotNetTheory] - [MemberData(nameof(GetImageData))] - public void VerifyDefaultUser(ProductImageData imageData) - { - VerifyCommonDefaultUser(imageData); - } + [DotNetTheory] + [MemberData(nameof(GetImageData))] + public void VerifyDefaultUser(ProductImageData imageData) => VerifyCommonDefaultUser(imageData); - public static EnvironmentVariableInfo GetRuntimeVersionVariableInfo( - DotNetImageRepo imageRepo, ProductImageData imageData, DockerHelper dockerHelper) + public static EnvironmentVariableInfo GetRuntimeVersionVariableInfo( + DotNetImageRepo imageRepo, ProductImageData imageData, DockerHelper dockerHelper) + { + string version = imageData.GetProductVersion(imageRepo, DotNetImageRepo.Runtime, dockerHelper); + return EnvironmentVariableInfo.Require("DOTNET_VERSION", version) with { - string version = imageData.GetProductVersion(imageRepo, DotNetImageRepo.Runtime, dockerHelper); - return EnvironmentVariableInfo.Require("DOTNET_VERSION", version) with - { - IsProductVersion = true - }; - } + IsProductVersion = true + }; } } diff --git a/tests/Microsoft.DotNet.Docker.Tests/SampleImageData.cs b/tests/Microsoft.DotNet.Docker.Tests/SampleImageData.cs index 006ff5ef3c..5d127cfb83 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/SampleImageData.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/SampleImageData.cs @@ -1,50 +1,48 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. using System; -namespace Microsoft.DotNet.Docker.Tests -{ - public record SampleImageData : ImageData - { - /// - /// Gets or sets a value indicating that this sample is published as a Docker image. - /// - public bool IsPublished { get; set; } +namespace Microsoft.DotNet.Docker.Tests; - public string DockerfileSuffix { get; set; } +public record SampleImageData : ImageData +{ + /// + /// Gets or sets a value indicating that this sample is published as a Docker image. + /// + public bool IsPublished { get; set; } - public string GetImage(SampleImageType imageType, DockerHelper dockerHelper, bool allowPull = false) - { - string tagPrefix = GetTagNameBase(imageType); - string os = OS; + public string DockerfileSuffix { get; set; } - string tag = GetTagName(tagPrefix, os); - if (!IsPublished) - { - tag += "-local"; - } + public string GetImage(SampleImageType imageType, DockerHelper dockerHelper, bool allowPull = false) + { + string tagPrefix = GetTagNameBase(imageType); + string os = OS; - return GetImage(tag, dockerHelper, allowPull); + string tag = GetTagName(tagPrefix, os); + if (!IsPublished) + { + tag += "-local"; } - public string GetImage(string tag, DockerHelper dockerHelper, bool allowPull = false) - { - string imageName = GetImageName(tag); + return GetImage(tag, dockerHelper, allowPull); + } - if (IsPublished) - { - PullImageIfNecessary(imageName, dockerHelper, allowPull); - } + public string GetImage(string tag, DockerHelper dockerHelper, bool allowPull = false) + { + string imageName = GetImageName(tag); - return imageName; + if (IsPublished) + { + PullImageIfNecessary(imageName, dockerHelper, allowPull); } - public string GetTagNameBase(SampleImageType imageType) => - Enum.GetName(typeof(SampleImageType), imageType).ToLowerInvariant(); - - public static string GetImageName(string tag) => - GetImageName(tag, "samples", string.Empty); + return imageName; } + + public string GetTagNameBase(SampleImageType imageType) => + Enum.GetName(typeof(SampleImageType), imageType).ToLowerInvariant(); + + public static string GetImageName(string tag) => + GetImageName(tag, "samples", string.Empty); } diff --git a/tests/Microsoft.DotNet.Docker.Tests/SampleImageTests.cs b/tests/Microsoft.DotNet.Docker.Tests/SampleImageTests.cs index ee2c6b3e5e..baf618fde6 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/SampleImageTests.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/SampleImageTests.cs @@ -1,6 +1,5 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. using System; using System.Collections.Generic; @@ -8,191 +7,183 @@ using System.Linq; using System.Threading.Tasks; using System.Xml.Linq; +using Microsoft.DotNet.Docker.Tests.TestScenarios; using Xunit; using Xunit.Abstractions; -namespace Microsoft.DotNet.Docker.Tests +namespace Microsoft.DotNet.Docker.Tests; + +[Trait("Category", "sample")] +public class SampleImageTests(ITestOutputHelper outputHelper) { - [Trait("Category", "sample")] - public class SampleImageTests - { - private static readonly string s_samplesPath = Path.Combine(Config.SourceRepoRoot, "samples"); + private static readonly string SamplesPath = Path.Combine(Config.SourceRepoRoot, "samples"); - public SampleImageTests(ITestOutputHelper outputHelper) - { - OutputHelper = outputHelper; - DockerHelper = new DockerHelper(outputHelper); - } + protected DockerHelper DockerHelper { get; } = new DockerHelper(outputHelper); - protected DockerHelper DockerHelper { get; } + protected ITestOutputHelper OutputHelper { get; } = outputHelper; - protected ITestOutputHelper OutputHelper { get; } + public static IEnumerable GetImageData() + { + return TestData.GetSampleImageData() + .Select(imageData => new object[] { imageData }); + } - public static IEnumerable GetImageData() + [DotNetTheory] + [MemberData(nameof(GetImageData))] + public async Task VerifyDotnetSample(SampleImageData imageData) + { + if (imageData.DockerfileSuffix == "windowsservercore-iis") { - return TestData.GetSampleImageData() - .Select(imageData => new object[] { imageData }); + return; } - [DotNetTheory] - [MemberData(nameof(GetImageData))] - public async Task VerifyDotnetSample(SampleImageData imageData) + await VerifySampleAsync(imageData, SampleImageType.Dotnetapp, (image, containerName) => { - if (imageData.DockerfileSuffix == "windowsservercore-iis") - { - return; - } - - await VerifySampleAsync(imageData, SampleImageType.Dotnetapp, (image, containerName) => - { - string output = DockerHelper.Run(image, containerName); - Assert.True(output.Contains("42") || output.StartsWith("Hello")); + string output = DockerHelper.Run(image, containerName); + Assert.True(output.Contains("42") || output.StartsWith("Hello")); - ValidateEnvironmentVariables(imageData, image, SampleImageType.Dotnetapp); + ValidateEnvironmentVariables(imageData, image, SampleImageType.Dotnetapp); - return Task.CompletedTask; - }); - } + return Task.CompletedTask; + }); + } - [DotNetTheory] - [MemberData(nameof(GetImageData))] - public async Task VerifyAspnetSample(SampleImageData imageData) + [DotNetTheory] + [MemberData(nameof(GetImageData))] + public async Task VerifyAspnetSample(SampleImageData imageData) + { + await VerifySampleAsync(imageData, SampleImageType.Aspnetapp, async (image, containerName) => { - await VerifySampleAsync(imageData, SampleImageType.Aspnetapp, async (image, containerName) => - { - int port = imageData.DockerfileSuffix == "windowsservercore-iis" ? 80 : imageData.DefaultPort; + int port = imageData.DockerfileSuffix == "windowsservercore-iis" ? 80 : imageData.DefaultPort; - try - { - DockerHelper.Run( - image: image, - name: containerName, - detach: true, - optionalRunArgs: $"-p {port}", - skipAutoCleanup: true); - - if (!Config.IsHttpVerificationDisabled) - { - await WebScenario.VerifyHttpResponseFromContainerAsync(containerName, DockerHelper, OutputHelper, port); - } - - ValidateEnvironmentVariables(imageData, image, SampleImageType.Aspnetapp); - } - finally + try + { + DockerHelper.Run( + image: image, + name: containerName, + detach: true, + optionalRunArgs: $"-p {port}", + skipAutoCleanup: true); + + if (!Config.IsHttpVerificationDisabled) { - DockerHelper.DeleteContainer(containerName); + await WebScenario.VerifyHttpResponseFromContainerAsync(containerName, DockerHelper, OutputHelper, port); } - }); - } - [Fact] - public void VerifyMultiProjectAppSample() - { - // MultiProjectApp sample doesn't currently support building on Windows. - if (!DockerHelper.IsLinuxContainerModeEnabled) + ValidateEnvironmentVariables(imageData, image, SampleImageType.Aspnetapp); + } + finally { - return; + DockerHelper.DeleteContainer(containerName); } + }); + } - string appTag = SampleImageData.GetImageName("complexapp-local-app"); - string testTag = SampleImageData.GetImageName("complexapp-local-test"); - string sampleFolder = Path.Combine(s_samplesPath, "MultiProjectApp"); - string dockerfilePath = $"{sampleFolder}/Dockerfile"; - string testContainerName = ImageData.GenerateContainerName("sample-complex-test"); - string tempDir = null; - try + [Fact] + public void VerifyMultiProjectAppSample() + { + // MultiProjectApp sample doesn't currently support building on Windows. + if (!DockerHelper.IsLinuxContainerModeEnabled) + { + return; + } + + string appTag = SampleImageData.GetImageName("complexapp-local-app"); + string testTag = SampleImageData.GetImageName("complexapp-local-test"); + string sampleFolder = Path.Combine(SamplesPath, "MultiProjectApp"); + string dockerfilePath = $"{sampleFolder}/Dockerfile"; + string testContainerName = ImageData.GenerateContainerName("sample-complex-test"); + string tempDir = null; + try + { + // Test that the app works + DockerHelper.Build(appTag, dockerfilePath, contextDir: sampleFolder, pull: Config.PullImages); + string containerName = ImageData.GenerateContainerName("sample-complex"); + string output = DockerHelper.Run(appTag, containerName); + Assert.StartsWith("string: The quick brown fox jumps over the lazy dog", output); + + // Run the app's tests + DockerHelper.Build(testTag, dockerfilePath, target: "test", contextDir: sampleFolder); + DockerHelper.Run(testTag, testContainerName, skipAutoCleanup: true); + + // Copy the test log from the container to the host + tempDir = Directory.CreateDirectory( + Path.Combine(Path.GetTempPath(), Path.GetRandomFileName())).FullName; + DockerHelper.Copy($"{testContainerName}:/source/Tests/TestResults", tempDir); + string testLogFile = new DirectoryInfo($"{tempDir}/TestResults").GetFiles("*.trx").First().FullName; + + // Open the test log file and verify the tests passed + XDocument doc = XDocument.Load(testLogFile); + XElement summary = doc.Root.Element(XName.Get("ResultSummary", doc.Root.Name.NamespaceName)); + Assert.Equal("Completed", summary.Attribute("outcome").Value); + XElement counters = summary.Element(XName.Get("Counters", doc.Root.Name.NamespaceName)); + Assert.Equal("3", counters.Attribute("total").Value); + Assert.Equal("3", counters.Attribute("passed").Value); + } + finally + { + if (tempDir != null) { - // Test that the app works - DockerHelper.Build(appTag, dockerfilePath, contextDir: sampleFolder, pull: Config.PullImages); - string containerName = ImageData.GenerateContainerName("sample-complex"); - string output = DockerHelper.Run(appTag, containerName); - Assert.StartsWith("string: The quick brown fox jumps over the lazy dog", output); - - // Run the app's tests - DockerHelper.Build(testTag, dockerfilePath, target: "test", contextDir: sampleFolder); - DockerHelper.Run(testTag, testContainerName, skipAutoCleanup: true); - - // Copy the test log from the container to the host - tempDir = Directory.CreateDirectory( - Path.Combine(Path.GetTempPath(), Path.GetRandomFileName())).FullName; - DockerHelper.Copy($"{testContainerName}:/source/Tests/TestResults", tempDir); - string testLogFile = new DirectoryInfo($"{tempDir}/TestResults").GetFiles("*.trx").First().FullName; - - // Open the test log file and verify the tests passed - XDocument doc = XDocument.Load(testLogFile); - XElement summary = doc.Root.Element(XName.Get("ResultSummary", doc.Root.Name.NamespaceName)); - Assert.Equal("Completed", summary.Attribute("outcome").Value); - XElement counters = summary.Element(XName.Get("Counters", doc.Root.Name.NamespaceName)); - Assert.Equal("3", counters.Attribute("total").Value); - Assert.Equal("3", counters.Attribute("passed").Value); + Directory.Delete(tempDir, true); } - finally - { - if (tempDir != null) - { - Directory.Delete(tempDir, true); - } - DockerHelper.DeleteContainer(testContainerName); - DockerHelper.DeleteImage(testTag); - DockerHelper.DeleteImage(appTag); - } + DockerHelper.DeleteContainer(testContainerName); + DockerHelper.DeleteImage(testTag); + DockerHelper.DeleteImage(appTag); } + } - private async Task VerifySampleAsync( - SampleImageData imageData, - SampleImageType sampleImageType, - Func verifyImageAsync) + private async Task VerifySampleAsync( + SampleImageData imageData, + SampleImageType sampleImageType, + Func verifyImageAsync) + { + string image = imageData.GetImage(sampleImageType, DockerHelper); + string imageType = Enum.GetName(typeof(SampleImageType), sampleImageType).ToLowerInvariant(); + try { - string image = imageData.GetImage(sampleImageType, DockerHelper); - string imageType = Enum.GetName(typeof(SampleImageType), sampleImageType).ToLowerInvariant(); - try + if (!imageData.IsPublished) { - if (!imageData.IsPublished) + string sampleName = sampleImageType switch { - string sampleName = sampleImageType switch - { - SampleImageType.Dotnetapp => "ConsoleApp", - SampleImageType.Aspnetapp => "AspNetCoreRazorApp", - _ => throw new ArgumentOutOfRangeException(nameof(sampleImageType)) - }; - string sampleFolder = Path.Combine(s_samplesPath, sampleName); - string dockerfilePath = $"{sampleFolder}/Dockerfile"; - if (!string.IsNullOrEmpty(imageData.DockerfileSuffix)) - { - dockerfilePath += $".{imageData.DockerfileSuffix}"; - } - - DockerHelper.Build(image, dockerfilePath, contextDir: sampleFolder, pull: Config.PullImages, platform: imageData.Platform); - } - - string containerName = imageData.GetIdentifier($"sample-{imageType}"); - await verifyImageAsync(image, containerName); - } - finally - { - if (!imageData.IsPublished) + SampleImageType.Dotnetapp => "ConsoleApp", + SampleImageType.Aspnetapp => "AspNetCoreRazorApp", + _ => throw new ArgumentOutOfRangeException(nameof(sampleImageType)) + }; + string sampleFolder = Path.Combine(SamplesPath, sampleName); + string dockerfilePath = $"{sampleFolder}/Dockerfile"; + if (!string.IsNullOrEmpty(imageData.DockerfileSuffix)) { - DockerHelper.DeleteImage(image); + dockerfilePath += $".{imageData.DockerfileSuffix}"; } + + DockerHelper.Build(image, dockerfilePath, contextDir: sampleFolder, pull: Config.PullImages, platform: imageData.Platform); } - } - private void ValidateEnvironmentVariables(SampleImageData imageData, string image, SampleImageType imageType) + string containerName = imageData.GetIdentifier($"sample-{imageType}"); + await verifyImageAsync(image, containerName); + } + finally { - List variables = new List(); - variables.AddRange(ProductImageTests.GetCommonEnvironmentVariables()); - - if (imageType == SampleImageType.Aspnetapp) + if (!imageData.IsPublished) { - variables.Add(EnvironmentVariableInfo.Require("ASPNETCORE_HTTP_PORTS", imageData.DefaultPort.ToString())); + DockerHelper.DeleteImage(image); } + } + } + + private void ValidateEnvironmentVariables(SampleImageData imageData, string image, SampleImageType imageType) + { + List variables = [.. ProductImageTests.GetCommonEnvironmentVariables()]; - EnvironmentVariableInfo.Validate( - variables, - image, - imageData, - DockerHelper); + if (imageType == SampleImageType.Aspnetapp) + { + variables.Add(EnvironmentVariableInfo.Require("ASPNETCORE_HTTP_PORTS", imageData.DefaultPort.ToString())); } + + EnvironmentVariableInfo.Validate( + variables, + image, + DockerHelper); } } diff --git a/tests/Microsoft.DotNet.Docker.Tests/SampleImageType.cs b/tests/Microsoft.DotNet.Docker.Tests/SampleImageType.cs index a2f4278ad1..52399cd905 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/SampleImageType.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/SampleImageType.cs @@ -1,12 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. -namespace Microsoft.DotNet.Docker.Tests +namespace Microsoft.DotNet.Docker.Tests; + +public enum SampleImageType { - public enum SampleImageType - { - Dotnetapp, - Aspnetapp - } + Dotnetapp, + Aspnetapp } diff --git a/tests/Microsoft.DotNet.Docker.Tests/SdkImageTests.cs b/tests/Microsoft.DotNet.Docker.Tests/SdkImageTests.cs index 177a9fa7f4..d5dd8ec13c 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/SdkImageTests.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/SdkImageTests.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. -// See the LICENSE file in the project root for more information. using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; +using System.Formats.Tar; using System.IO; +using System.IO.Compression; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; @@ -14,429 +14,405 @@ using System.Threading.Tasks; using Polly; using Polly.Retry; -using System.Formats.Tar; -using System.IO.Compression; using Xunit; using Xunit.Abstractions; #nullable enable -namespace Microsoft.DotNet.Docker.Tests +using Microsoft.DotNet.Docker.Tests.TestScenarios; + +namespace Microsoft.DotNet.Docker.Tests; + +[Trait("Category", "sdk")] +public class SdkImageTests(ITestOutputHelper outputHelper) : ProductImageTests(outputHelper) { - [Trait("Category", "sdk")] - public class SdkImageTests : ProductImageTests - { - private static readonly Dictionary> s_sdkContentsCache = - new Dictionary>(); + private static readonly Dictionary> SdkContentsCache = + []; - private static readonly RetryStrategyOptions s_sdkDownloadRetryStrategy = - new() - { - BackoffType = DelayBackoffType.Exponential, - MaxRetryAttempts = 4, - Delay = TimeSpan.FromSeconds(3), - }; + private static readonly RetryStrategyOptions SdkDownloadRetryStrategy = + new() + { + BackoffType = DelayBackoffType.Exponential, + MaxRetryAttempts = 4, + Delay = TimeSpan.FromSeconds(3), + }; - private static readonly ResiliencePipeline s_sdkDownloadPipeline = - new ResiliencePipelineBuilder() - .AddRetry(s_sdkDownloadRetryStrategy) - .Build(); + private static readonly ResiliencePipeline SdkDownloadPipeline = + new ResiliencePipelineBuilder() + .AddRetry(SdkDownloadRetryStrategy) + .Build(); - private static readonly HttpClient s_httpClient = CreateHttpClient(); + private static readonly HttpClient HttpClient = CreateHttpClient(); - public SdkImageTests(ITestOutputHelper outputHelper) - : base(outputHelper) - { - } + protected override DotNetImageRepo ImageRepo => DotNetImageRepo.SDK; + + public static IEnumerable GetImageData() + { + return TestData.GetImageData(DotNetImageRepo.SDK) + .Where(imageData => !imageData.IsDistroless) + .Select(imageData => new object[] { imageData }); + } - protected override DotNetImageRepo ImageRepo => DotNetImageRepo.SDK; + [LinuxImageTheory] + [MemberData(nameof(GetImageData))] + public async void VerifyBlazorWasmScenario(ProductImageData imageData) + { + bool useWasmTools = true; - public static IEnumerable GetImageData() + // `wasm-tools` workload does not work on ARM + if (imageData.IsArm) { - return TestData.GetImageData(DotNetImageRepo.SDK) - .Where(imageData => !imageData.IsDistroless) - .Select(imageData => new object[] { imageData }); + useWasmTools = false; } - [LinuxImageTheory] - [MemberData(nameof(GetImageData))] - public async void VerifyBlazorWasmScenario(ProductImageData imageData) + // `wasm-tools` is not supported on Alpine for .NET < 9 due to https://github.com/dotnet/sdk/issues/32327 + if (imageData.OS.Family == OSFamily.Alpine && imageData.Version.Major == 8) { - bool useWasmTools = true; + useWasmTools = false; + } - // `wasm-tools` workload does not work on ARM - if (imageData.IsArm) - { - useWasmTools = false; - } + // Emscripten SDK workload is the wrong version on linux-musl-x64 in .NET 10 Preview 6 + // Remove in nightly branch when https://github.com/dotnet/dotnet/issues/1487 is resolved + if (imageData.Version.Major == 10) + { + useWasmTools = false; + } - // `wasm-tools` is not supported on Alpine for .NET < 9 due to https://github.com/dotnet/sdk/issues/32327 - if (imageData.OS.Family == OSFamily.Alpine && imageData.Version.Major == 8) - { - useWasmTools = false; - } + BlazorWasmScenario testScenario = new(imageData, DockerHelper, OutputHelper, useWasmTools); + await testScenario.ExecuteAsync(); + } - // Emscripten SDK workload is the wrong version on linux-musl-x64 in .NET 10 Preview 6 - // Remove in nightly branch when https://github.com/dotnet/dotnet/issues/1487 is resolved - if (imageData.Version.Major == 10) - { - useWasmTools = false; - } + [LinuxImageTheory] + [MemberData(nameof(GetImageData))] + public void VerifyInsecureFiles(ProductImageData imageData) => VerifyCommonInsecureFiles(imageData); - BlazorWasmScenario testScenario = new(imageData, DockerHelper, OutputHelper, useWasmTools); - await testScenario.ExecuteAsync(); - } + [DotNetTheory] + [MemberData(nameof(GetImageData))] + public void VerifyNoSasToken(ProductImageData imageData) => VerifyCommonNoSasToken(imageData); - [LinuxImageTheory] - [MemberData(nameof(GetImageData))] - public void VerifyInsecureFiles(ProductImageData imageData) + [DotNetTheory] + [MemberData(nameof(GetImageData))] + public void VerifyEnvironmentVariables(ProductImageData imageData) + { + string imageName = imageData.GetImage(ImageRepo, DockerHelper); + string version = imageData.GetProductVersion(ImageRepo, ImageRepo, DockerHelper); + + List variables = + [ + EnvironmentVariableInfo.Require("DOTNET_GENERATE_ASPNET_CERTIFICATE", "false"), + EnvironmentVariableInfo.Require("DOTNET_USE_POLLING_FILE_WATCHER", "true"), + EnvironmentVariableInfo.Require("NUGET_XMLDOC_MODE", "skip"), + EnvironmentVariableInfo.Require("DOTNET_SDK_VERSION", version) with + { + IsProductVersion = true + }, + AspnetImageTests.GetAspnetVersionVariableInfo(ImageRepo, imageData, DockerHelper), + RuntimeImageTests.GetRuntimeVersionVariableInfo(ImageRepo, imageData, DockerHelper), + EnvironmentVariableInfo.Require("DOTNET_NOLOGO", "true"), + // DOTNET_ROLL_FORWARD must not be set globally, as it can silently change + // the runtime that unrelated workloads (e.g. dotnet test) execute on. + // See https://github.com/dotnet/dotnet-docker/issues/7255. + EnvironmentVariableInfo.Forbid("DOTNET_ROLL_FORWARD"), + ..GetCommonEnvironmentVariables(), + ]; + + if (imageData.SdkOS.Family == OSFamily.Alpine) { - base.VerifyCommonInsecureFiles(imageData); + variables.Add(EnvironmentVariableInfo.Require("DOTNET_SYSTEM_GLOBALIZATION_INVARIANT", "false")); } - [DotNetTheory] - [MemberData(nameof(GetImageData))] - public void VerifyNoSasToken(ProductImageData imageData) + EnvironmentVariableInfo.Validate(variables, imageName, DockerHelper); + } + + /// + /// Verifies that the dotnet folder contents of an SDK container match the contents in the official SDK archive file. + /// + [DotNetTheory] + [MemberData(nameof(GetImageData))] + public async Task VerifyDotnetFolderContents(ProductImageData imageData) + { + if (Config.IsInternal) { - base.VerifyCommonNoSasToken(imageData); + // Skip this test for internal builds, since this test does not + // yet authenticate to download the internal staged version of + // the .NET SDK. + return; } - [DotNetTheory] - [MemberData(nameof(GetImageData))] - public void VerifyEnvironmentVariables(ProductImageData imageData) + // Workaround for https://github.com/dotnet/sdk/issues/55238 + if (imageData.SdkOS.Family == OSFamily.Alpine && imageData.Version.Major == 11) { - string imageName = imageData.GetImage(ImageRepo, DockerHelper); - string version = imageData.GetProductVersion(ImageRepo, ImageRepo, DockerHelper); + return; + } - List variables = - [ - EnvironmentVariableInfo.Require("DOTNET_GENERATE_ASPNET_CERTIFICATE", "false"), - EnvironmentVariableInfo.Require("DOTNET_USE_POLLING_FILE_WATCHER", "true"), - EnvironmentVariableInfo.Require("NUGET_XMLDOC_MODE", "skip"), - EnvironmentVariableInfo.Require("DOTNET_SDK_VERSION", version) with - { - IsProductVersion = true - }, - AspnetImageTests.GetAspnetVersionVariableInfo(ImageRepo, imageData, DockerHelper), - RuntimeImageTests.GetRuntimeVersionVariableInfo(ImageRepo, imageData, DockerHelper), - EnvironmentVariableInfo.Require("DOTNET_NOLOGO", "true"), - // DOTNET_ROLL_FORWARD must not be set globally, as it can silently change - // the runtime that unrelated workloads (e.g. dotnet test) execute on. - // See https://github.com/dotnet/dotnet-docker/issues/7255. - EnvironmentVariableInfo.Forbid("DOTNET_ROLL_FORWARD"), - ..GetCommonEnvironmentVariables(), - ]; - - if (imageData.SdkOS.Family == OSFamily.Alpine) - { - variables.Add(EnvironmentVariableInfo.Require("DOTNET_SYSTEM_GLOBALIZATION_INVARIANT", "false")); - } + IEnumerable actualDotnetFiles = GetActualSdkContents(imageData); + IEnumerable expectedDotnetFiles = await GetExpectedSdkContentsAsync(imageData); - EnvironmentVariableInfo.Validate(variables, imageName, imageData, DockerHelper); - } + using TempFileContext actualFilesContext = FileHelper.UseTempFile("container-image"); + using TempFileContext expectedFilesContext = FileHelper.UseTempFile("sdk-archive"); - /// - /// Verifies that the dotnet folder contents of an SDK container match the contents in the official SDK archive file. - /// - [DotNetTheory] - [MemberData(nameof(GetImageData))] - public async Task VerifyDotnetFolderContents(ProductImageData imageData) - { - if (Config.IsInternal) - { - // Skip this test for internal builds, since this test does not - // yet authenticate to download the internal staged version of - // the .NET SDK. - return; - } + File.WriteAllLines(actualFilesContext.Path, actualDotnetFiles.Select(file => $"{file.Path} {file.Sha512}")); + File.WriteAllLines(expectedFilesContext.Path, expectedDotnetFiles.Select(file => $"{file.Path} {file.Sha512}")); - // Workaround for https://github.com/dotnet/sdk/issues/55238 - if (imageData.SdkOS.Family == OSFamily.Alpine && imageData.Version.Major == 11) - { - return; - } + bool filesMatch = FileHelper.CompareFiles(expectedFilesContext.Path, actualFilesContext.Path, OutputHelper); - IEnumerable actualDotnetFiles = GetActualSdkContents(imageData); - IEnumerable expectedDotnetFiles = await GetExpectedSdkContentsAsync(imageData); + Assert.True(filesMatch, "Differences found in the dotnet folder contents."); + } - using TempFileContext actualFilesContext = FileHelper.UseTempFile("container-image"); - using TempFileContext expectedFilesContext = FileHelper.UseTempFile("sdk-archive"); + [LinuxImageTheory] + [MemberData(nameof(GetImageData))] + public void VerifyInstalledPackages(ProductImageData imageData) => VerifyInstalledPackagesBase(imageData, ImageRepo); - File.WriteAllLines(actualFilesContext.Path, actualDotnetFiles.Select(file => $"{file.Path} {file.Sha512}")); - File.WriteAllLines(expectedFilesContext.Path, expectedDotnetFiles.Select(file => $"{file.Path} {file.Sha512}")); + [DotNetTheory] + [MemberData(nameof(GetImageData))] + public void VerifyDefaultUser(ProductImageData imageData) => VerifyCommonDefaultUser(imageData); - bool filesMatch = FileHelper.CompareFiles(expectedFilesContext.Path, actualFilesContext.Path, OutputHelper); + /// + /// Verifies that a git command can be executed without failure. + /// + [DotNetTheory] + [MemberData(nameof(GetImageData))] + public void VerifyGitInstallation(ProductImageData imageData) + { + DockerHelper.Run( + image: imageData.GetImage(DotNetImageRepo.SDK, DockerHelper), + name: imageData.GetIdentifier($"git"), + command: "git version"); + } - Assert.True(filesMatch, "Differences found in the dotnet folder contents."); - } + /// + /// Verifies that a tar command can be executed without failure. + /// + [DotNetTheory] + [MemberData(nameof(GetImageData))] + public void VerifyTarInstallation(ProductImageData imageData) + { + // tar should exist in the SDK for both Linux and Windows. The --version option works in either OS + DockerHelper.Run( + image: imageData.GetImage(DotNetImageRepo.SDK, DockerHelper), + name: imageData.GetIdentifier("tar"), + command: "tar --version" + ); + } - [LinuxImageTheory] - [MemberData(nameof(GetImageData))] - public void VerifyInstalledPackages(ProductImageData imageData) + /// + /// Verifies that dnx is on the PATH and that it is functional. + /// + [DotNetTheory] + [MemberData(nameof(GetImageData))] + public void VerifyDnxInstallation(ProductImageData imageData) + { + if (!imageData.SupportsDnx) { - VerifyInstalledPackagesBase(imageData, ImageRepo); + return; } - [DotNetTheory] - [MemberData(nameof(GetImageData))] - public void VerifyDefaultUser(ProductImageData imageData) - { - VerifyCommonDefaultUser(imageData); - } + string dnxCommand = DockerHelper.IsLinuxContainerModeEnabled + ? "dnx --help" + : "dnx.cmd --help"; + + DockerHelper.Run( + image: imageData.GetImage(DotNetImageRepo.SDK, DockerHelper), + name: imageData.GetIdentifier("dnx"), + command: dnxCommand + ); + } - /// - /// Verifies that a git command can be executed without failure. - /// - [DotNetTheory] - [MemberData(nameof(GetImageData))] - public void VerifyGitInstallation(ProductImageData imageData) + private IEnumerable GetActualSdkContents(ProductImageData imageData) + { + string dotnetPath; + string destinationPath; + string command; + + if (DockerHelper.IsLinuxContainerModeEnabled) { - DockerHelper.Run( - image: imageData.GetImage(DotNetImageRepo.SDK, DockerHelper), - name: imageData.GetIdentifier($"git"), - command: "git version"); - } + dotnetPath = "/usr/share/dotnet"; + destinationPath = "/sdk"; - /// - /// Verifies that a tar command can be executed without failure. - /// - [DotNetTheory] - [MemberData(nameof(GetImageData))] - public void VerifyTarInstallation(ProductImageData imageData) + // Alpine's BusyBox find doesn't support comma-separated types (-type f,l) + command = imageData.SdkOS.Family == OSFamily.Alpine + ? $"find {destinationPath} ( -type f -o -type l ) -exec sha512sum {{}} +" + : $"find {destinationPath} -type f,l -exec sha512sum {{}} +"; + } + else { - // tar should exist in the SDK for both Linux and Windows. The --version option works in either OS - DockerHelper.Run( - image: imageData.GetImage(DotNetImageRepo.SDK, DockerHelper), - name: imageData.GetIdentifier("tar"), - command: "tar --version" - ); + dotnetPath = "\"Program Files\\dotnet\""; + destinationPath = "C:\\sdk"; + string powerShellCommand = + $"Get-ChildItem -File -Force -Recurse '{destinationPath}' " + + "| Get-FileHash -Algorithm SHA512 " + + "| select @{name='Value'; expression={$_.Hash + ' ' +$_.Path}} " + + "| select -ExpandProperty Value"; + command = $"pwsh -Command \"{powerShellCommand}\""; } - /// - /// Verifies that dnx is on the PATH and that it is functional. - /// - [DotNetTheory] - [MemberData(nameof(GetImageData))] - public void VerifyDnxInstallation(ProductImageData imageData) - { - if (!imageData.SupportsDnx) - { - return; - } + string baseImage = imageData.GetImage(ImageRepo, DockerHelper); + string tag = imageData.GetIdentifier("SdkContents").ToLower(); - var dnxCommand = DockerHelper.IsLinuxContainerModeEnabled - ? "dnx --help" - : "dnx.cmd --help"; + DockerHelper.Build( + tag: tag, + dockerfile: Path.Combine(DockerHelper.TestArtifactsDir, "Dockerfile.copy"), + contextDir: DockerHelper.TestArtifactsDir, + platform: imageData.Platform, + buildArgs: + [ + $"copy_image={baseImage}", + $"base_image={baseImage}", + $"copy_origin={dotnetPath}", + $"copy_destination={destinationPath}" + ]); + + string containerFileList = DockerHelper.Run( + image: tag, + name: tag, + command: command, + silenceOutput: true); + + return containerFileList + .Split("\n") + .Select(line => + { + string[] parts = line.Split(" ").Select(part => part.Trim()).ToArray(); + return new SdkContentFileInfo(Path.GetRelativePath(destinationPath, parts[1]), parts[0]); + }) + .OrderBy(fileInfo => fileInfo.Path); + } - DockerHelper.Run( - image: imageData.GetImage(DotNetImageRepo.SDK, DockerHelper), - name: imageData.GetIdentifier("dnx"), - command: dnxCommand - ); - } + private async Task> GetExpectedSdkContentsAsync(ProductImageData imageData) + { + string sdkUrl = GetSdkUrl(imageData); + OutputHelper.WriteLine("Downloading SDK archive: " + sdkUrl); - private IEnumerable GetActualSdkContents(ProductImageData imageData) + if (!SdkContentsCache.TryGetValue(sdkUrl, out IEnumerable? files)) { - string dotnetPath; - string destinationPath; - string command; + using TempFolderContext tempFolder = FileHelper.UseTempFolder(); + string sdkFile = Path.Combine(tempFolder.Path, "sdk-archive"); + + await SdkDownloadPipeline.ExecuteAsync(async cancellationToken => await HttpClient.DownloadFileAsync(new Uri(sdkUrl), sdkFile)); + + using TempFolderContext extractFolder = FileHelper.UseTempFolder(); - if (DockerHelper.IsLinuxContainerModeEnabled) + if (Path.GetExtension(sdkUrl).Equals(".zip", StringComparison.OrdinalIgnoreCase)) { - dotnetPath = "/usr/share/dotnet"; - destinationPath = "/sdk"; - - // Alpine's BusyBox find doesn't support comma-separated types (-type f,l) - if (imageData.SdkOS.Family == OSFamily.Alpine) - { - command = $"find {destinationPath} ( -type f -o -type l ) -exec sha512sum {{}} +"; - } - else - { - command = $"find {destinationPath} -type f,l -exec sha512sum {{}} +"; - } + ZipFile.ExtractToDirectory(sdkFile, extractFolder.Path); } else { - dotnetPath = "\"Program Files\\dotnet\""; - destinationPath = "C:\\sdk"; - string powerShellCommand = - $"Get-ChildItem -File -Force -Recurse '{destinationPath}' " + - "| Get-FileHash -Algorithm SHA512 " + - "| select @{name='Value'; expression={$_.Hash + ' ' +$_.Path}} " + - "| select -ExpandProperty Value"; - command = $"pwsh -Command \"{powerShellCommand}\""; + using FileStream fileStream = File.OpenRead(sdkFile); + using GZipStream gzipStream = new GZipStream(fileStream, CompressionMode.Decompress); + TarFile.ExtractToDirectory(gzipStream, extractFolder.Path, overwriteFiles: false); } - string baseImage = imageData.GetImage(ImageRepo, DockerHelper); - string tag = imageData.GetIdentifier("SdkContents").ToLower(); - - DockerHelper.Build( - tag: tag, - dockerfile: Path.Combine(DockerHelper.TestArtifactsDir, "Dockerfile.copy"), - contextDir: DockerHelper.TestArtifactsDir, - platform: imageData.Platform, - buildArgs: - [ - $"copy_image={baseImage}", - $"base_image={baseImage}", - $"copy_origin={dotnetPath}", - $"copy_destination={destinationPath}" - ]); - - string containerFileList = DockerHelper.Run( - image: tag, - name: tag, - command: command, - silenceOutput: true); - - return containerFileList - .Split("\n") - .Select(line => + files = Directory.EnumerateFiles(extractFolder.Path, "*", SearchOption.AllDirectories) + .Select(file => { - string[] parts = line.Split(" ").Select(part => part.Trim()).ToArray(); - return new SdkContentFileInfo(Path.GetRelativePath(destinationPath, parts[1]), parts[0]); - }) - .OrderBy(fileInfo => fileInfo.Path); - } + string filePath = Path.GetFullPath(file); + string relativePath = Path.GetRelativePath(extractFolder.Path, filePath); - private async Task> GetExpectedSdkContentsAsync(ProductImageData imageData) - { - string sdkUrl = GetSdkUrl(imageData); - OutputHelper.WriteLine("Downloading SDK archive: " + sdkUrl); + byte[] fileData = File.ReadAllBytes(filePath); + byte[] sha512HashBytes = SHA512.HashData(fileData); + string sha512Hash = Convert.ToHexString(sha512HashBytes); - if (!s_sdkContentsCache.TryGetValue(sdkUrl, out IEnumerable? files)) - { - using TempFolderContext tempFolder = FileHelper.UseTempFolder(); - string sdkFile = Path.Combine(tempFolder.Path, "sdk-archive"); + return new SdkContentFileInfo(relativePath, sha512Hash); + }) + .OrderBy(file => file.Path) + .ToArray(); - await s_sdkDownloadPipeline.ExecuteAsync(async cancellationToken => - { - await s_httpClient.DownloadFileAsync(new Uri(sdkUrl), sdkFile); - }); + SdkContentsCache.Add(sdkUrl, files); + } - using TempFolderContext extractFolder = FileHelper.UseTempFolder(); + return files; + } - if (Path.GetExtension(sdkUrl).Equals(".zip", StringComparison.OrdinalIgnoreCase)) - { - ZipFile.ExtractToDirectory(sdkFile, extractFolder.Path); - } - else - { - using FileStream fileStream = File.OpenRead(sdkFile); - using var gzipStream = new GZipStream(fileStream, CompressionMode.Decompress); - TarFile.ExtractToDirectory(gzipStream, extractFolder.Path, overwriteFiles: false); - } - - files = Directory.EnumerateFiles(extractFolder.Path, "*", SearchOption.AllDirectories) - .Select(file => - { - string filePath = Path.GetFullPath(file); - string relativePath = Path.GetRelativePath(extractFolder.Path, filePath); - - byte[] fileData = File.ReadAllBytes(filePath); - byte[] sha512HashBytes = SHA512.HashData(fileData); - string sha512Hash = Convert.ToHexString(sha512HashBytes); - - return new SdkContentFileInfo(relativePath, sha512Hash); - }) - .OrderBy(file => file.Path) - .ToArray(); - - s_sdkContentsCache.Add(sdkUrl, files); - } + private static string GetSdkVersionFileLabel(string sdkBuildVersion, string dotnetVersion) + { + // This should be kept in sync with the template for computing the SDK version file: + // https://github.com/dotnet/dotnet-docker/blob/4f48d36a98187a6e350d54167ef5b568ccd3882f/eng/dockerfile-templates/sdk/Dockerfile.linux.install-sdk#L22-L31 - return files; - } + bool isStableBranding = !sdkBuildVersion.Contains('-') + || sdkBuildVersion.Contains("-servicing") + || sdkBuildVersion.Contains("-rtm"); - private static string GetSdkVersionFileLabel(string sdkBuildVersion, string dotnetVersion) + if (isStableBranding) { - // This should be kept in sync with the template for computing the SDK version file: - // https://github.com/dotnet/dotnet-docker/blob/4f48d36a98187a6e350d54167ef5b568ccd3882f/eng/dockerfile-templates/sdk/Dockerfile.linux.install-sdk#L22-L31 - - bool isStableBranding = !sdkBuildVersion.Contains('-') - || sdkBuildVersion.Contains("-servicing") - || sdkBuildVersion.Contains("-rtm"); + return Config.GetVariableValue($"sdk|{dotnetVersion}|product-version"); + } - if (isStableBranding) - { - return Config.GetVariableValue($"sdk|{dotnetVersion}|product-version"); - } + bool useFinalVersion = + Config.TryGetVariableValue($"sdk|{dotnetVersion}|use-final-version", out string finalFlag) + && finalFlag == "true"; + if (useFinalVersion) + { + return Config.GetVariableValue($"sdk|{dotnetVersion}|product-version") + ".final"; + } - bool useFinalVersion = - Config.TryGetVariableValue($"sdk|{dotnetVersion}|use-final-version", out string finalFlag) - && finalFlag == "true"; - if (useFinalVersion) - { - return Config.GetVariableValue($"sdk|{dotnetVersion}|product-version") + ".final"; - } + return sdkBuildVersion; + } - return sdkBuildVersion; + private string GetSdkUrl(ProductImageData imageData) + { + bool isInternal = Config.IsInternal; + string sdkBuildVersion = Config.GetBuildVersion(ImageRepo, imageData.VersionString); + string sdkFileVersionLabel = isInternal + ? imageData.GetProductVersion(ImageRepo, ImageRepo, DockerHelper) + : GetSdkVersionFileLabel(sdkBuildVersion, imageData.VersionString); + + string osType = DockerHelper.IsLinuxContainerModeEnabled ? "linux" : "win"; + if (imageData.SdkOS.Family == OSFamily.Alpine) + { + osType += "-musl"; } - private string GetSdkUrl(ProductImageData imageData) + string architecture = imageData.Arch switch { - bool isInternal = Config.IsInternal; - string sdkBuildVersion = Config.GetBuildVersion(ImageRepo, imageData.VersionString); - string sdkFileVersionLabel = isInternal - ? imageData.GetProductVersion(ImageRepo, ImageRepo, DockerHelper) - : GetSdkVersionFileLabel(sdkBuildVersion, imageData.VersionString); - - string osType = DockerHelper.IsLinuxContainerModeEnabled ? "linux" : "win"; - if (imageData.SdkOS.Family == OSFamily.Alpine) - { - osType += "-musl"; - } + Arch.Amd64 => "x64", + Arch.Arm => "arm", + Arch.Arm64 => "arm64", + _ => throw new InvalidOperationException($"Unexpected architecture type: '{imageData.Arch}'"), + }; - string architecture = imageData.Arch switch - { - Arch.Amd64 => "x64", - Arch.Arm => "arm", - Arch.Arm64 => "arm64", - _ => throw new InvalidOperationException($"Unexpected architecture type: '{imageData.Arch}'"), - }; + string fileType = DockerHelper.IsLinuxContainerModeEnabled ? "tar.gz" : "zip"; + string baseUrl = Config.GetBaseUrl(imageData.VersionString); + string url = $"{baseUrl}/Sdk/{sdkBuildVersion}/dotnet-sdk-{sdkFileVersionLabel}-{osType}-{architecture}.{fileType}"; - string fileType = DockerHelper.IsLinuxContainerModeEnabled ? "tar.gz" : "zip"; - string baseUrl = Config.GetBaseUrl(imageData.VersionString); - string url = $"{baseUrl}/Sdk/{sdkBuildVersion}/dotnet-sdk-{sdkFileVersionLabel}-{osType}-{architecture}.{fileType}"; + return url; + } - return url; - } + private record SdkContentFileInfo + { + public string Path { get; init; } + public string Sha512 { get; init; } - private record SdkContentFileInfo + public SdkContentFileInfo(string path, string sha512) { - public string Path { get; init; } - public string Sha512 { get; init; } - - public SdkContentFileInfo(string path, string sha512) - { - Path = NormalizePath(path); - Sha512 = sha512.ToLower(); - } - - private static string NormalizePath(string path) - { - return path - .Replace("\\", "/") - .Replace("/usr/share/dotnet", string.Empty, StringComparison.OrdinalIgnoreCase) - .Replace("c:/program files/dotnet", string.Empty, StringComparison.OrdinalIgnoreCase) - .TrimStart('/') - .ToLower(); - } + Path = NormalizePath(path); + Sha512 = sha512.ToLower(); } - private static HttpClient CreateHttpClient() + private static string NormalizePath(string path) { - var client = new HttpClient(); + return path + .Replace("\\", "/") + .Replace("/usr/share/dotnet", string.Empty, StringComparison.OrdinalIgnoreCase) + .Replace("c:/program files/dotnet", string.Empty, StringComparison.OrdinalIgnoreCase) + .TrimStart('/') + .ToLower(); + } + } - if (Config.IsInternal) - { - client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( - "Basic", - Convert.ToBase64String(Encoding.ASCII.GetBytes(string.Format("{0}:{1}", "", - Config.InternalAccessToken)))); - }; + private static HttpClient CreateHttpClient() + { + HttpClient client = new HttpClient(); - return client; + if (Config.IsInternal) + { + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( + "Basic", + Convert.ToBase64String(Encoding.ASCII.GetBytes(string.Format("{0}:{1}", "", + Config.InternalAccessToken)))); } + + ; + + return client; } } diff --git a/tests/Microsoft.DotNet.Docker.Tests/SkippableTheoryAttribute.cs b/tests/Microsoft.DotNet.Docker.Tests/SkippableTheoryAttribute.cs index c8507be87a..85efedc2ad 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/SkippableTheoryAttribute.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/SkippableTheoryAttribute.cs @@ -1,29 +1,27 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. using System.Linq; using System.Text.RegularExpressions; -namespace Microsoft.DotNet.Docker.Tests +namespace Microsoft.DotNet.Docker.Tests; + +public class SkippableTheoryAttribute : DotNetTheoryAttribute { - public class SkippableTheoryAttribute : DotNetTheoryAttribute + public SkippableTheoryAttribute(params string[] skippedOperatingSystems) { - public SkippableTheoryAttribute(params string[] skippedOperatingSystems) + if (Config.OsNames.Any() && !Config.OsNames.Any(osName => osName != "*")) { - if (Config.OsNames.Any() && !Config.OsNames.Any(osName => osName != "*")) + foreach (string osName in Config.OsNames) { - foreach (string osName in Config.OsNames) + string osPattern = + osName != null ? Config.GetFilterRegexPattern(osName) : null; + foreach (string skippedOperatingSystem in skippedOperatingSystems) { - string osPattern = - osName != null ? Config.GetFilterRegexPattern(osName) : null; - foreach (string skippedOperatingSystem in skippedOperatingSystems) + if (Regex.IsMatch(skippedOperatingSystem, osPattern, RegexOptions.IgnoreCase)) { - if (Regex.IsMatch(skippedOperatingSystem, osPattern, RegexOptions.IgnoreCase)) - { - Skip = $"{skippedOperatingSystem} is unsupported"; - break; - } + Skip = $"{skippedOperatingSystem} is unsupported"; + break; } } } diff --git a/tests/Microsoft.DotNet.Docker.Tests/StaticTagTests.cs b/tests/Microsoft.DotNet.Docker.Tests/StaticTagTests.cs index 2d52902c4b..60f6c7f501 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/StaticTagTests.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/StaticTagTests.cs @@ -1,682 +1,677 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; -using Microsoft.CodeAnalysis; -using Microsoft.DotNet.Docker.Shared; using FluentAssertions; using FluentAssertions.Execution; +using Microsoft.CodeAnalysis; +using Microsoft.DotNet.Docker.Shared; using Xunit; - using static Microsoft.DotNet.Docker.Tests.ManifestHelper; #nullable enable -namespace Microsoft.DotNet.Docker.Tests +namespace Microsoft.DotNet.Docker.Tests; + +[Trait("Category", "pre-build")] +public class StaticTagTests { - [Trait("Category", "pre-build")] - public class StaticTagTests + public enum VersionType { - public enum VersionType - { - Major = 1, - MajorMinor = 2, - MajorMinorPatch = 3 - } + Major = 1, + MajorMinor = 2, + MajorMinorPatch = 3 + } - private const string LatestTagValue = "latest"; - private const string SingleNumberRegex = @"\d+"; - private const string MajorVersionRegex = SingleNumberRegex; - private const string MajorMinorVersionRegex = @$"{SingleNumberRegex}\.{SingleNumberRegex}"; - private static readonly string[] ApplianceRepoIds = - [ - "monitor", - "monitor-base", - "aspire-dashboard", - "yarp" - ]; - - private enum TestType - { - // Tests validating the 'latest' tags. - // Tests begin with "LatestTag_" - Latest, - - // Tests validating platform-specific tags - // Tags have variations of versions, oses, and architectures. - // Does not include tags with only versions specified (). - // Tests begin with "PlatformTag_" - Platform, - - // Tests validating alpine floating tags - // Tags have variations of versions and architectures. - // Tests begin with "FloatingAlpineTag_" - FloatingAlpine, - - // Tests validating version tags - // Tags have variations of versions - // Does not include tags with specified oses or architectures (). - // Tests begin with "VersionTag_" - Version - } + private const string LatestTagValue = "latest"; + private const string SingleNumberRegex = @"\d+"; + private const string MajorVersionRegex = SingleNumberRegex; + private const string MajorMinorVersionRegex = @$"{SingleNumberRegex}\.{SingleNumberRegex}"; + private static readonly string[] ApplianceRepoIds = + [ + "monitor", + "monitor-base", + "aspire-dashboard", + "yarp" + ]; + + private enum TestType + { + // Tests validating the 'latest' tags. + // Tests begin with "LatestTag_" + Latest, + + // Tests validating platform-specific tags + // Tags have variations of versions, oses, and architectures. + // Does not include tags with only versions specified (). + // Tests begin with "PlatformTag_" + Platform, + + // Tests validating alpine floating tags + // Tags have variations of versions and architectures. + // Tests begin with "FloatingAlpineTag_" + FloatingAlpine, + + // Tests validating version tags + // Tags have variations of versions + // Does not include tags with specified oses or architectures (). + // Tests begin with "VersionTag_" + Version + } - [Theory] - [MemberData(nameof(GetTagTestObjects), TestType.Latest)] - public void LatestTag_OnePerRepo(Repo repo) - { - IEnumerable latestTagImages = repo.Images - .Where(image => ManifestHelper.GetResolvedSharedTags(image).Contains(LatestTagValue)); + [Theory] + [MemberData(nameof(GetTagTestObjects), TestType.Latest)] + public void LatestTag_OnePerRepo(Repo repo) + { + IEnumerable latestTagImages = repo.Images + .Where(image => GetResolvedSharedTags(image).Contains(LatestTagValue)); - latestTagImages.Should().ContainSingle("expected exactly one image to have the latest tag"); - } + latestTagImages.Should().ContainSingle("expected exactly one image to have the latest tag"); + } - [Theory] - [MemberData(nameof(GetTagTestObjects), TestType.Latest)] - public void LatestTag_IsNotPlatformSpecific(Repo repo) + [Theory] + [MemberData(nameof(GetTagTestObjects), TestType.Latest)] + public void LatestTag_IsNotPlatformSpecific(Repo repo) + { + foreach (Image image in repo.Images) { - foreach (Image image in repo.Images) - { - IEnumerable latestTagPlatforms = image.Platforms - .Where(platform => ManifestHelper.GetResolvedTags(platform).Contains(LatestTagValue)); + IEnumerable latestTagPlatforms = image.Platforms + .Where(platform => ManifestHelper.GetResolvedTags(platform).Contains(LatestTagValue)); - latestTagPlatforms.Should().BeEmpty("expected latest tag to be a shared tag, not a platform-specific tag"); - } + latestTagPlatforms.Should().BeEmpty("expected latest tag to be a shared tag, not a platform-specific tag"); } + } - [Theory] - [MemberData(nameof(GetTagTestObjects), TestType.Latest)] - public void LatestTag_OnCorrectMajorVersion(Repo repo) - { - IEnumerable<(Image Image, List SharedTags)> imageDatas = repo.Images - .Select(image => (Image: image, SharedTags: ManifestHelper.GetResolvedSharedTags(image))); + [Theory] + [MemberData(nameof(GetTagTestObjects), TestType.Latest)] + public void LatestTag_OnCorrectMajorVersion(Repo repo) + { + IEnumerable<(Image Image, List SharedTags)> imageDatas = repo.Images + .Select(image => (Image: image, SharedTags: GetResolvedSharedTags(image))); - (Image Image, List SharedTags) latestImageData = imageDatas - .Where(imageData => imageData.SharedTags.Contains(LatestTagValue)) - .First(); + (Image Image, List SharedTags) latestImageData = imageDatas + .First(imageData => imageData.SharedTags.Contains(LatestTagValue)); - int expectedMajorVersion = GetExpectedLatestMajorVersion(repo); + int expectedMajorVersion = GetExpectedLatestMajorVersion(repo); - latestImageData.SharedTags.Should().ContainMatch($"{expectedMajorVersion}.*", - because: $"latest tag should be on .NET {expectedMajorVersion} in repo {repo.Name}"); + latestImageData.SharedTags.Should().ContainMatch($"{expectedMajorVersion}.*", + because: $"latest tag should be on .NET {expectedMajorVersion} in repo {repo.Name}"); + } + + // - -- Non-windows only for non-Appliance repos, old schema only for Appliance repos + // - -- Non-windows only for non-Appliance repos, old schema only for Appliance repos + // - -- Old schema only for Appliance repos + // - - Non-Appliance repos, old schema only for Appliance repos + // - - Non-Appliance repos, old schema only for Appliance repos + // - -- New schema only for Appliance repos only + // - - New schema only for Appliance repos only + // - - New schema and non-monitor for Appliance repos only + [Theory] + [MemberData(nameof(GetTagTestObjects), TestType.Platform)] + public void PlatformTag_TagExists( + Repo repo, + VersionType versionType, + bool checkOs, + bool checkArchitecture, + Func skipDockerFileOn) + { + if (!checkOs && !checkArchitecture) + { + throw new ArgumentException("At least one of 'checkOs' or 'checkArchitecture' must be true. " + + "Please use the VersionTag tests for this scenario."); } - // - -- Non-windows only for non-Appliance repos, old schema only for Appliance repos - // - -- Non-windows only for non-Appliance repos, old schema only for Appliance repos - // - -- Old schema only for Appliance repos - // - - Non-Appliance repos, old schema only for Appliance repos - // - - Non-Appliance repos, old schema only for Appliance repos - // - -- New schema only for Appliance repos only - // - - New schema only for Appliance repos only - // - - New schema and non-monitor for Appliance repos only - [Theory] - [MemberData(nameof(GetTagTestObjects), TestType.Platform)] - public void PlatformTag_TagExists( - Repo repo, - VersionType versionType, - bool checkOs, - bool checkArchitecture, - Func skipDockerFileOn) + Dictionary> dockerfileTags = GetDockerfileTags(repo); + foreach ((DockerfileInfo dockerfileInfo, List tags) in dockerfileTags) { - if (!checkOs && !checkArchitecture) + if (skipDockerFileOn(dockerfileInfo)) { - throw new ArgumentException("At least one of 'checkOs' or 'checkArchitecture' must be true. " + - "Please use the VersionTag tests for this scenario."); + continue; } - Dictionary> dockerfileTags = ManifestHelper.GetDockerfileTags(repo); - foreach ((DockerfileInfo dockerfileInfo, List tags) in dockerfileTags) + DotNetVersion dockerfileVersion = dockerfileInfo.ProductVersion; + + bool hasPreviewInMajorVersionGroup = HasPreviewProductVersion(repo, dockerfileVersion.Major); + + // .NET Monitor 8 Azure Linux images do not have Azure Linux platform + // (e.g. *-amd64) tags but do have undocumented CBL-Mariner tags. The tag + // pattern test assumes that a dockerfile will produce platform tags for the + // underlying OS, which is true for most cases, but not for .NET Monitor 8 + // due to combination of undocumenting platform tags for appliance images + // (which only .NET Monitor had at the time) and the update from CBL-Mariner + // to Azure Linux. Rewrite the OS to match CBL-Mariner in this instance. + string os = dockerfileInfo.OsDir; + if (repo.Name.EndsWith("monitor") && + dockerfileInfo.VersionDir.StartsWith("8.") && + OS.AzureLinuxDistroless.Equals(os)) { - if (skipDockerFileOn(dockerfileInfo)) - { - continue; - } + os = OS.MarinerDistroless; + } - var dockerfileVersion = dockerfileInfo.ProductVersion; - - bool hasPreviewInMajorVersionGroup = HasPreviewProductVersion(repo, dockerfileVersion.Major); - - // .NET Monitor 8 Azure Linux images do not have Azure Linux platform - // (e.g. *-amd64) tags but do have undocumented CBL-Mariner tags. The tag - // pattern test assumes that a dockerfile will produce platform tags for the - // underlying OS, which is true for most cases, but not for .NET Monitor 8 - // due to combination of undocumenting platform tags for appliance images - // (which only .NET Monitor had at the time) and the update from CBL-Mariner - // to Azure Linux. Rewrite the OS to match CBL-Mariner in this instance. - string os = dockerfileInfo.OsDir; - if (repo.Name.EndsWith("monitor") && - dockerfileInfo.VersionDir.StartsWith("8.") && - OS.AzureLinuxDistroless.Equals(os)) - { - os = OS.MarinerDistroless; - } + Regex expectedPattern = GetTagRegex( + versionType, + dockerfileInfo.ProductVersion, + checkOs ? os : null, + checkArchitecture ? dockerfileInfo.ArchitectureDir : null); - Regex expectedPattern = GetTagRegex( - versionType, - dockerfileInfo.ProductVersion, - checkOs ? os : null, - checkArchitecture ? dockerfileInfo.ArchitectureDir : null); + IEnumerable matchingTags = tags.Where(tag => expectedPattern.IsMatch(tag)); - var matchingTags = tags.Where(tag => expectedPattern.IsMatch(tag)); + if (versionType == VersionType.Major && !IsLatestInMajorVersionGroup(repo, dockerfileInfo.ProductVersion, hasPreviewInMajorVersionGroup)) + { + // Special case for major version tags + // These tags should be on the latest Major.Minor GA version for the respective major version + matchingTags.Should().BeEmpty("expected tag to be on latest Major.Minor version for version " + + dockerfileVersion.Major + ", but found the tag on " + dockerfileInfo); + } + else + { + matchingTags + .Should() + .ContainSingle($"{dockerfileInfo} requires exactly one tag that matches the expected pattern {expectedPattern}"); + } + } + } - if (versionType == VersionType.Major && !IsLatestInMajorVersionGroup(repo, dockerfileInfo.ProductVersion, hasPreviewInMajorVersionGroup)) + // - -alpine- + // - -alpine + // - -alpine + // - -alpine- + // - -alpine- + // - -alpine Appliance repos only + [Theory] + [MemberData(nameof(GetTagTestObjects), TestType.FloatingAlpine)] + public void FloatingAlpineTag_OnLatestVersion(Repo repo, VersionType versionType, bool checkArchitecture) + { + IEnumerable>> alpineDockerfileTags = + GetDockerfileTags(repo) + .Where(p => p.Key.OsDir.Contains(OS.Alpine)); + + using (new AssertionScope()) + { + foreach ((DockerfileInfo dockerfileInfo, List tags) in alpineDockerfileTags) + { + string alpineFloatingTagVersion = GetAlpineFloatingTagVersion(dockerfileInfo); + Regex pattern = GetFloatingTagRegex(dockerfileInfo); + if (dockerfileInfo.OsDir == alpineFloatingTagVersion) { - // Special case for major version tags - // These tags should be on the latest Major.Minor GA version for the respective major version - matchingTags.Should().BeEmpty("expected tag to be on latest Major.Minor version for version " + - dockerfileVersion.Major + ", but found the tag on " + dockerfileInfo); + tags.Should().ContainSingle(tag => pattern.IsMatch(tag), + because: $"image {dockerfileInfo} should have an {OS.Alpine} floating tag"); } else { - matchingTags - .Should() - .ContainSingle($"{dockerfileInfo} requires exactly one tag that matches the expected pattern {expectedPattern}"); + tags.Should().NotContain(tag => pattern.IsMatch(tag), + because: $"image {dockerfileInfo} should not have an {OS.Alpine} floating tag"); } } } - // - -alpine- - // - -alpine - // - -alpine - // - -alpine- - // - -alpine- - // - -alpine Appliance repos only - [Theory] - [MemberData(nameof(GetTagTestObjects), TestType.FloatingAlpine)] - public void FloatingAlpineTag_OnLatestVersion(Repo repo, VersionType versionType, bool checkArchitecture) - { - IEnumerable>> alpineDockerfileTags = - GetDockerfileTags(repo) - .Where(p => p.Key.OsDir.Contains(OS.Alpine)); - - using (new AssertionScope()) - { - foreach ((DockerfileInfo dockerfileInfo, List tags) in alpineDockerfileTags) - { - string alpineFloatingTagVersion = GetAlpineFloatingTagVersion(dockerfileInfo); - Regex pattern = GetFloatingTagRegex(dockerfileInfo); - if (dockerfileInfo.OsDir == alpineFloatingTagVersion) - { - tags.Should().ContainSingle(tag => pattern.IsMatch(tag), - because: $"image {dockerfileInfo} should have an {OS.Alpine} floating tag"); - } - else - { - tags.Should().NotContain(tag => pattern.IsMatch(tag), - because: $"image {dockerfileInfo} should not have an {OS.Alpine} floating tag"); - } - } - } + string GetAlpineFloatingTagVersion(DockerfileInfo info) => + Config.GetVariableValue($"alpine|{info.VersionDir}|floating-tag-version"); - string GetAlpineFloatingTagVersion(DockerfileInfo info) => - Config.GetVariableValue($"alpine|{info.VersionDir}|floating-tag-version"); + Regex GetFloatingTagRegex(DockerfileInfo info) => + GetTagRegex( + versionType, + productVersion: info.ProductVersion, + os: OS.Alpine, + architecture: checkArchitecture ? info.ArchitectureDir : null); + } - Regex GetFloatingTagRegex(DockerfileInfo info) => - GetTagRegex( - versionType, - productVersion: info.ProductVersion, - os: OS.Alpine, - architecture: checkArchitecture ? info.ArchitectureDir : null); - } + // - + // - + // - Appliance repos only + [Theory] + [MemberData(nameof(GetTagTestObjects), TestType.Version)] + public void VersionTag_SameOsAndVersion(Repo repo, VersionType versionType) + { + // Group tags -> dockerfiles + Dictionary> tagsToDockerfiles = + GetDockerfileTags(repo) + .SelectMany(pair => + { + (DockerfileInfo? dockerfileInfo, List? allTags) = pair; + return allTags + .Where(tag => IsTagOfFormat( + tag: tag, + versionType: versionType, + productVersion: dockerfileInfo.ProductVersion, + os: null, + architecture: null)) + .Select(tag => (tag, pair.Key)); + }) + .GroupBy(pair => pair.tag, pair => pair.Key) + .ToDictionary(group => group.Key, group => group.ToList()); - // - - // - - // - Appliance repos only - [Theory] - [MemberData(nameof(GetTagTestObjects), TestType.Version)] - public void VersionTag_SameOsAndVersion(Repo repo, VersionType versionType) + foreach (KeyValuePair> tagToDockerfiles in tagsToDockerfiles) { - // Group tags -> dockerfiles - Dictionary> tagsToDockerfiles = - GetDockerfileTags(repo) - .SelectMany(pair => - { - var (dockerfileInfo, allTags) = pair; - return allTags - .Where(tag => IsTagOfFormat( - tag: tag, - versionType: versionType, - productVersion: dockerfileInfo.ProductVersion, - os: null, - architecture: null)) - .Select(tag => (tag, pair.Key)); - }) - .GroupBy(pair => pair.tag, pair => pair.Key) - .ToDictionary(group => group.Key, group => group.ToList()); - - foreach (KeyValuePair> tagToDockerfiles in tagsToDockerfiles) - { - string tag = tagToDockerfiles.Key; - List dockerfiles = tagToDockerfiles.Value; + string tag = tagToDockerfiles.Key; + List dockerfiles = tagToDockerfiles.Value; - var dockerfileProductVersions = dockerfiles - .Select(dockerfile => dockerfile.ProductVersion) - .Distinct() - .ToList(); + List dockerfileProductVersions = dockerfiles + .Select(dockerfile => dockerfile.ProductVersion) + .Distinct() + .ToList(); - dockerfileProductVersions.Should().ContainSingle( - "all dockerfiles for tag " + tag + " should have the same version"); + dockerfileProductVersions.Should().ContainSingle( + "all dockerfiles for tag " + tag + " should have the same version"); - if (versionType == VersionType.Major) - { - var dockerfileVersion = dockerfileProductVersions.First(); + if (versionType == VersionType.Major) + { + DotNetVersion dockerfileVersion = dockerfileProductVersions.First(); - // Special case for major version tags - // These tags should be on the most up-to-date Major.Minor version for the respective major version - IsLatestInMajorVersionGroup(repo, dockerfileVersion, tag.Contains("-preview")).Should().BeTrue( - "expected tag to be on the latest Major.Minor GA version for the major version " + - dockerfileVersion.Major + ", but found the tag on " + dockerfileVersion); - } + // Special case for major version tags + // These tags should be on the most up-to-date Major.Minor version for the respective major version + IsLatestInMajorVersionGroup(repo, dockerfileVersion, tag.Contains("-preview")).Should().BeTrue( + "expected tag to be on the latest Major.Minor GA version for the major version " + + dockerfileVersion.Major + ", but found the tag on " + dockerfileVersion); + } - List dockerfileOses = dockerfiles - .Select(dockerfile => dockerfile.OsDir) - .Distinct() - .ToList(); + List dockerfileOses = dockerfiles + .Select(dockerfile => dockerfile.OsDir) + .Distinct() + .ToList(); - dockerfileOses.Should().ContainSingle( - "all dockerfiles for tag " + tag + " should have the same os"); - } + dockerfileOses.Should().ContainSingle( + "all dockerfiles for tag " + tag + " should have the same os"); } + } - private static IEnumerable GetTagTestObjects(TestType testType) + private static IEnumerable GetTagTestObjects(TestType testType) + { + List testObjects = []; + foreach (Repo repo in GetManifest().Repos) { - List testObjects = new List(); - foreach (Repo repo in ManifestHelper.GetManifest().Repos) + switch (testType) { - switch (testType) - { - case TestType.Latest: - testObjects.Add(GetTagTestInput(testType, repo)); - break; + case TestType.Latest: + testObjects.Add(GetTagTestInput(testType, repo)); + break; - case TestType.Platform: - if (ApplianceRepoIds.Contains(repo.Id)) - { - // Only appliance repos have major version tags - // Only appliance repos have floating distro tags () - testObjects.AddRange(new[] - { - GetTagTestInput( - testType, - repo, - VersionType.MajorMinorPatch, - checkOs: true, - checkArchitecture: true, - skipDockerfileOn: IsApplianceVersionUsingNewSchema - ), // -- - - GetTagTestInput( - testType, - repo, - VersionType.MajorMinorPatch, - checkOs: true, - checkArchitecture: false, - skipDockerfileOn: IsApplianceVersionUsingNewSchema - ), // - - - GetTagTestInput( - testType, - repo, - VersionType.MajorMinorPatch, - checkOs: false, - checkArchitecture: true, - skipDockerfileOn: IsApplianceVersionUsingOldSchema - ), // - - - GetTagTestInput( - testType, - repo, - VersionType.MajorMinor, - checkOs: true, - checkArchitecture: true, - skipDockerfileOn: IsApplianceVersionUsingNewSchema - ), // -- - - GetTagTestInput( - testType, - repo, - VersionType.MajorMinor, - checkOs: true, - checkArchitecture: false, - skipDockerfileOn: IsApplianceVersionUsingNewSchema - ), // - - - GetTagTestInput( - testType, - repo, - VersionType.MajorMinor, - checkOs: false, - checkArchitecture: true, - skipDockerfileOn: IsApplianceVersionUsingOldSchema - ), // - - - GetTagTestInput( - testType, - repo, - VersionType.Major, - checkOs: true, - checkArchitecture: true, - skipDockerfileOn: IsApplianceVersionUsingNewSchema - ), // -- - - GetTagTestInput( - testType, - repo, - VersionType.Major, - checkOs: true, - checkArchitecture: false, - skipDockerfileOn: IsApplianceVersionUsingNewSchema - ) // - - }); - - if (!repo.Name.Contains("monitor")) - { - // Monitor repos don't have these tags - testObjects.Add(GetTagTestInput( - testType, - repo, - VersionType.Major, - checkOs: false, - checkArchitecture: true, - skipDockerfileOn: IsApplianceVersionUsingOldSchema - )); // - - } - } - else - { - testObjects.AddRange(new[] - { - GetTagTestInput( - testType, - repo, - VersionType.MajorMinorPatch, - checkOs: true, - checkArchitecture: true, - skipDockerfileOn: IsWindows - ), // -- - - GetTagTestInput( - testType, - repo, - VersionType.MajorMinorPatch, - checkOs: true, - checkArchitecture: false - ), // - - - GetTagTestInput( - testType, - repo, - VersionType.MajorMinor, - checkOs: true, - checkArchitecture: true, - skipDockerfileOn: IsWindows - ), // -- - - GetTagTestInput( - testType, - repo, - VersionType.MajorMinor, - checkOs: true, - checkArchitecture: false - ) // - - }); - } - break; + case TestType.Platform: + if (ApplianceRepoIds.Contains(repo.Id)) + { + // Only appliance repos have major version tags + // Only appliance repos have floating distro tags () + testObjects.AddRange( + [ + GetTagTestInput( + testType, + repo, + VersionType.MajorMinorPatch, + checkOs: true, + checkArchitecture: true, + skipDockerfileOn: IsApplianceVersionUsingNewSchema + ), // -- + + GetTagTestInput( + testType, + repo, + VersionType.MajorMinorPatch, + checkOs: true, + checkArchitecture: false, + skipDockerfileOn: IsApplianceVersionUsingNewSchema + ), // - + + GetTagTestInput( + testType, + repo, + VersionType.MajorMinorPatch, + checkOs: false, + checkArchitecture: true, + skipDockerfileOn: IsApplianceVersionUsingOldSchema + ), // - - case TestType.FloatingAlpine: - if (ApplianceRepoIds.Contains(repo.Id)) - { - // Only appliance repos have major version alpine floating tags - // Only appliance repos have major.minor.patch alpine floating tags - testObjects.AddRange(new[] - { - GetTagTestInput( - testType, - repo, - VersionType.MajorMinorPatch, - checkArchitecture: true - ), // -alpine- - - GetTagTestInput( - testType, - repo, - VersionType.MajorMinorPatch, - checkArchitecture: false - ), // -alpine - - GetTagTestInput( - testType, - repo, - VersionType.Major, - checkArchitecture: true - ), // -alpine- - - GetTagTestInput( - testType, - repo, - VersionType.Major, - checkArchitecture: false - ) // -alpine - }); - } - testObjects.AddRange(new[] - { GetTagTestInput( testType, repo, VersionType.MajorMinor, - checkArchitecture: true - ), // -alpine- + checkOs: true, + checkArchitecture: true, + skipDockerfileOn: IsApplianceVersionUsingNewSchema + ), // -- GetTagTestInput( testType, repo, VersionType.MajorMinor, - checkArchitecture: false - ) // -alpine - }); - break; + checkOs: true, + checkArchitecture: false, + skipDockerfileOn: IsApplianceVersionUsingNewSchema + ), // - + + GetTagTestInput( + testType, + repo, + VersionType.MajorMinor, + checkOs: false, + checkArchitecture: true, + skipDockerfileOn: IsApplianceVersionUsingOldSchema + ), // - + + GetTagTestInput( + testType, + repo, + VersionType.Major, + checkOs: true, + checkArchitecture: true, + skipDockerfileOn: IsApplianceVersionUsingNewSchema + ), // -- - case TestType.Version: - if (ApplianceRepoIds.Contains(repo.Id)) + GetTagTestInput( + testType, + repo, + VersionType.Major, + checkOs: true, + checkArchitecture: false, + skipDockerfileOn: IsApplianceVersionUsingNewSchema + ) // - + ]); + + if (!repo.Name.Contains("monitor")) { - // Only appliance repos have major version tags + // Monitor repos don't have these tags testObjects.Add(GetTagTestInput( testType, repo, - VersionType.Major - )); // + VersionType.Major, + checkOs: false, + checkArchitecture: true, + skipDockerfileOn: IsApplianceVersionUsingOldSchema + )); // - } - testObjects.AddRange(new[] - { + } + else + { + testObjects.AddRange( + [ GetTagTestInput( testType, repo, - VersionType.MajorMinor - ), // + VersionType.MajorMinorPatch, + checkOs: true, + checkArchitecture: true, + skipDockerfileOn: IsWindows + ), // -- GetTagTestInput( testType, repo, - VersionType.MajorMinorPatch - ), // - }); - break; - - default: - throw new ArgumentException("Invalid tag type", nameof(testType)); - } - } - return testObjects.ToArray(); - } + VersionType.MajorMinorPatch, + checkOs: true, + checkArchitecture: false + ), // - - private static object[] GetTagTestInput( - TestType testType, - Repo repo, - VersionType? versionType = null, - bool? checkOs = false, - bool? checkArchitecture = false, - Func? skipDockerfileOn = null) - { - switch (testType) - { - case TestType.Latest: - return [ repo ]; + GetTagTestInput( + testType, + repo, + VersionType.MajorMinor, + checkOs: true, + checkArchitecture: true, + skipDockerfileOn: IsWindows + ), // -- - case TestType.Platform: - if (versionType == null || checkOs == null || checkArchitecture == null) - { - throw new ArgumentException($"'{nameof(versionType)}', '{nameof(checkOs)}' and '{nameof(checkArchitecture)}' must be specified", nameof(testType)); + GetTagTestInput( + testType, + repo, + VersionType.MajorMinor, + checkOs: true, + checkArchitecture: false + ) // - + ]); } - return [ repo, versionType, checkOs, checkArchitecture, skipDockerfileOn ?? (_ => false) ]; + + break; case TestType.FloatingAlpine: - if (versionType == null || checkArchitecture == null) + if (ApplianceRepoIds.Contains(repo.Id)) { - throw new ArgumentException($"'{nameof(versionType)}' and '{nameof(checkArchitecture)}' must be specified", nameof(testType)); + // Only appliance repos have major version alpine floating tags + // Only appliance repos have major.minor.patch alpine floating tags + testObjects.AddRange( + [ + GetTagTestInput( + testType, + repo, + VersionType.MajorMinorPatch, + checkArchitecture: true + ), // -alpine- + + GetTagTestInput( + testType, + repo, + VersionType.MajorMinorPatch, + checkArchitecture: false + ), // -alpine + + GetTagTestInput( + testType, + repo, + VersionType.Major, + checkArchitecture: true + ), // -alpine- + + GetTagTestInput( + testType, + repo, + VersionType.Major, + checkArchitecture: false + ) // -alpine + ]); } - return [ repo, versionType, checkArchitecture ]; + + testObjects.AddRange( + [ + GetTagTestInput( + testType, + repo, + VersionType.MajorMinor, + checkArchitecture: true + ), // -alpine- + + GetTagTestInput( + testType, + repo, + VersionType.MajorMinor, + checkArchitecture: false + ) // -alpine + ]); + break; case TestType.Version: - if (versionType == null) + if (ApplianceRepoIds.Contains(repo.Id)) { - throw new ArgumentException($"'{nameof(versionType)}' must be specified", nameof(testType)); + // Only appliance repos have major version tags + testObjects.Add(GetTagTestInput( + testType, + repo, + VersionType.Major + )); // } - return [ repo, versionType ]; + + testObjects.AddRange( + [ + GetTagTestInput( + testType, + repo, + VersionType.MajorMinor + ), // + + GetTagTestInput( + testType, + repo, + VersionType.MajorMinorPatch + ), // + ]); + break; default: throw new ArgumentException("Invalid tag type", nameof(testType)); } } - private static bool IsWindows(DockerfileInfo dockerfileInfo) => - dockerfileInfo.OsDir.Contains("windowsservercore") || dockerfileInfo.OsDir.Contains("nanoserver"); - - // Certain versions of appliance repos use a new tag schema. - // This new schema excludes the OS from all tags. - // The aspire-dashboard repo uses this schema for all versions. - // The monitor and monitor-base repos use this schema for versions 9 and above. - private static bool IsApplianceVersionUsingOldSchema(DockerfileInfo dockerfileInfo) => - dockerfileInfo.RepoDir.Contains("monitor") && dockerfileInfo.ProductVersion.Major <= 8; - - // - private static bool IsApplianceVersionUsingNewSchema(DockerfileInfo dockerfileInfo) => - !IsApplianceVersionUsingOldSchema(dockerfileInfo); - - /// - /// Determines if the is the latest in its major version group. - /// - /// - /// By default, this will only consider GA versions within the major version group (unless there are no GA versions). - /// The parameter can be set to include preview versions in the check. - /// - private static bool IsLatestInMajorVersionGroup(Repo repo, DotNetVersion productVersion, bool includePreviewVersions) + return testObjects.ToArray(); + } + + private static object[] GetTagTestInput( + TestType testType, + Repo repo, + VersionType? versionType = null, + bool? checkOs = false, + bool? checkArchitecture = false, + Func? skipDockerfileOn = null) + { + switch (testType) { - IEnumerable productVersions = GetResolvedProductVersions(repo).Select(DotNetVersion.Parse); - Assert.NotEmpty(productVersions); + case TestType.Latest: + return [repo]; - List majorMinorVersions = productVersions - .GroupBy(version => version.Major) - .Select(group => + case TestType.Platform: + if (versionType == null || checkOs == null || checkArchitecture == null) { - if (includePreviewVersions) - { - return group; - } - else - { - // Use the latest GA major version. - // If there are no GA versions, use the latest preview version. - var gaVersions = group.Where(version => version.IsGA); - return gaVersions.Any() ? gaVersions : group; - } - }) - .Select(group => group - .OrderByDescending(version => version) - .First()) - .ToList(); + throw new ArgumentException($"'{nameof(versionType)}', '{nameof(checkOs)}' and '{nameof(checkArchitecture)}' must be specified", nameof(testType)); + } - return majorMinorVersions.Contains(productVersion); - } + return [repo, versionType, checkOs, checkArchitecture, skipDockerfileOn ?? (_ => false)]; - /// - /// Determines if a major product version has a preview version. - /// - private static bool HasPreviewProductVersion(Repo repo, int majorVersion) - { - var productVersions = GetResolvedProductVersions(repo).Select(DotNetVersion.Parse); + case TestType.FloatingAlpine: + if (versionType == null || checkArchitecture == null) + { + throw new ArgumentException($"'{nameof(versionType)}' and '{nameof(checkArchitecture)}' must be specified", nameof(testType)); + } - IGrouping? matchingMajorVersionGroup = productVersions - .GroupBy(version => version.Major) - .SingleOrDefault(group => group.Key == majorVersion); + return [repo, versionType, checkArchitecture]; - if (matchingMajorVersionGroup is null) - { - return false; - } + case TestType.Version: + if (versionType == null) + { + throw new ArgumentException($"'{nameof(versionType)}' must be specified", nameof(testType)); + } - return matchingMajorVersionGroup.Any(version => !version.IsGA); - } + return [repo, versionType]; - private static bool IsTagOfFormat( - string tag, - VersionType versionType, - DotNetVersion? productVersion, - string? os = null, - string? architecture = null) - { - Regex pattern = GetTagRegex(versionType, productVersion, os, architecture); - return pattern.IsMatch(tag); + default: + throw new ArgumentException("Invalid tag type", nameof(testType)); } + } - private static Regex GetTagRegex(VersionType versionType, DotNetVersion? productVersion, string? os, string? architecture) - { - string tagRegex = versionType switch + private static bool IsWindows(DockerfileInfo dockerfileInfo) => + dockerfileInfo.OsDir.Contains("windowsservercore") || dockerfileInfo.OsDir.Contains("nanoserver"); + + // Certain versions of appliance repos use a new tag schema. + // This new schema excludes the OS from all tags. + // The aspire-dashboard repo uses this schema for all versions. + // The monitor and monitor-base repos use this schema for versions 9 and above. + private static bool IsApplianceVersionUsingOldSchema(DockerfileInfo dockerfileInfo) => + dockerfileInfo.RepoDir.Contains("monitor") && dockerfileInfo.ProductVersion.Major <= 8; + + // + private static bool IsApplianceVersionUsingNewSchema(DockerfileInfo dockerfileInfo) => + !IsApplianceVersionUsingOldSchema(dockerfileInfo); + + /// + /// Determines if the is the latest in its major version group. + /// + /// + /// By default, this will only consider GA versions within the major version group (unless there are no GA versions). + /// The parameter can be set to include preview versions in the check. + /// + private static bool IsLatestInMajorVersionGroup(Repo repo, DotNetVersion productVersion, bool includePreviewVersions) + { + IEnumerable productVersions = GetResolvedProductVersions(repo).Select(DotNetVersion.Parse); + Assert.NotEmpty(productVersions); + + List majorMinorVersions = productVersions + .GroupBy(version => version.Major) + .Select(group => { - VersionType.Major => - productVersion == null ? MajorVersionRegex : @$"{productVersion.Major}(-preview)?", - VersionType.MajorMinor => - @$"{productVersion?.ToString(2) ?? MajorMinorVersionRegex}(-preview)?", - VersionType.MajorMinorPatch => - @$"{productVersion?.ToString(3) ?? @"\d+\.\d+\.\d+"}(?:-(alpha|beta|preview|rc)\.{SingleNumberRegex})?", - _ => throw new ArgumentException("Invalid version type", nameof(versionType)), - }; - - return new Regex($"^{tagRegex}" + (os != null ? $"-{os}" : string.Empty) + (architecture != null ? $"-{architecture}" : string.Empty) + "$"); - } + if (includePreviewVersions) + { + return group; + } + else + { + // Use the latest GA major version. + // If there are no GA versions, use the latest preview version. + IEnumerable gaVersions = group.Where(version => version.IsGA); + return gaVersions.Any() ? gaVersions : group; + } + }) + .Select(group => group + .OrderByDescending(version => version) + .First()) + .ToList(); - private static Version GetAlpineVersion(DockerfileInfo dockerfileInfo) + return majorMinorVersions.Contains(productVersion); + } + + /// + /// Determines if a major product version has a preview version. + /// + private static bool HasPreviewProductVersion(Repo repo, int majorVersion) + { + IEnumerable productVersions = GetResolvedProductVersions(repo).Select(DotNetVersion.Parse); + + IGrouping? matchingMajorVersionGroup = productVersions + .GroupBy(version => version.Major) + .SingleOrDefault(group => group.Key == majorVersion); + + if (matchingMajorVersionGroup is null) { - string parsedOs = dockerfileInfo.OsDir.Replace(OS.Alpine, string.Empty); - parsedOs.Should().NotBeNullOrWhiteSpace( - because: $"{dockerfileInfo} should have a specific Alpine version for osVersion"); - return Version.Parse(parsedOs); + return false; } - private static int GetExpectedLatestMajorVersion(Repo repo) + return matchingMajorVersionGroup.Any(version => !version.IsGA); + } + + private static bool IsTagOfFormat( + string tag, + VersionType versionType, + DotNetVersion? productVersion, + string? os = null, + string? architecture = null) + { + Regex pattern = GetTagRegex(versionType, productVersion, os, architecture); + return pattern.IsMatch(tag); + } + + private static Regex GetTagRegex(VersionType versionType, DotNetVersion? productVersion, string? os, string? architecture) + { + string tagRegex = versionType switch { - var productVersions = GetResolvedProductVersions(repo).Select(DotNetVersion.Parse); + VersionType.Major => + productVersion == null ? MajorVersionRegex : @$"{productVersion.Major}(-preview)?", + VersionType.MajorMinor => + @$"{productVersion?.ToString(2) ?? MajorMinorVersionRegex}(-preview)?", + VersionType.MajorMinorPatch => + @$"{productVersion?.ToString(3) ?? @"\d+\.\d+\.\d+"}(?:-(alpha|beta|preview|rc)\.{SingleNumberRegex})?", + _ => throw new ArgumentException("Invalid version type", nameof(versionType)), + }; + + return new Regex($"^{tagRegex}" + (os != null ? $"-{os}" : string.Empty) + (architecture != null ? $"-{architecture}" : string.Empty) + "$"); + } - Assert.NotEmpty(productVersions); + private static int GetExpectedLatestMajorVersion(Repo repo) + { + IEnumerable productVersions = GetResolvedProductVersions(repo).Select(DotNetVersion.Parse); - if (productVersions.Count() == 1) - { - // Use the first product version if there is only one - return productVersions.First().Major; - } + Assert.NotEmpty(productVersions); - // In non-nightly branches, preview versions should not have the latest tag - if (!Config.IsNightlyRepo) - { - productVersions = productVersions - .Where(version => !version.Release.Contains("preview") && !version.Release.Contains("rc")); - } + if (productVersions.Count() == 1) + { + // Use the first product version if there is only one + return productVersions.First().Major; + } - return productVersions.Select(version => version.Major).Max(); + // In non-nightly branches, preview versions should not have the latest tag + if (!Config.IsNightlyRepo) + { + productVersions = productVersions + .Where(version => !version.Release.Contains("preview") && !version.Release.Contains("rc")); } + + return productVersions.Max(version => version.Major); } } diff --git a/tests/Microsoft.DotNet.Docker.Tests/SyftHelper.cs b/tests/Microsoft.DotNet.Docker.Tests/SyftHelper.cs index b346ff5fd7..52f85cb9a3 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/SyftHelper.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/SyftHelper.cs @@ -1,6 +1,5 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. #nullable enable @@ -18,7 +17,7 @@ public sealed class SyftHelper(DockerHelper dockerHelper, ITestOutputHelper outp { private const string OutputFileName = "syft-output.json"; - private static readonly Lazy s_syftImageTag = new(() => + private static readonly Lazy SyftImageTag = new(() => $"{Config.GetVariableValue("syft|repo")}:{Config.GetVariableValue("syft|tag")}" ); @@ -31,11 +30,11 @@ public JsonNode Scan( IEnumerable? excludePaths = null ) { - using var tempDir = FileHelper.UseTempFolder(); - var tempDirPath = tempDir.Path; + using TempFolderContext tempDir = FileHelper.UseTempFolder(); + string tempDirPath = tempDir.Path; - var syftImage = _dockerHelper.PullDockerHubImage(s_syftImageTag.Value); - var imageToInspect = imageData.GetImage(imageRepo, _dockerHelper); + string syftImage = _dockerHelper.PullDockerHubImage(SyftImageTag.Value); + string imageToInspect = imageData.GetImage(imageRepo, _dockerHelper); // Ignore the dotnet folder, or else syft will report all the packages in the .NET Runtime. // We only care about the packages from the linux distro for these tests. @@ -44,8 +43,8 @@ public JsonNode Scan( // Create a Dockerfile tailored to this specific scan (allows us to inline the image name // in exec-form RUN) - var dockerfilePath = Path.Combine(tempDirPath, "Dockerfile"); - var dockerfileContents = CreateDockerfileContents(syftImage, imageToInspect, excludePaths); + string dockerfilePath = Path.Combine(tempDirPath, "Dockerfile"); + string dockerfileContents = CreateDockerfileContents(syftImage, imageToInspect, excludePaths); File.WriteAllText(dockerfilePath, dockerfileContents); _outputHelper.WriteLine( @@ -84,10 +83,10 @@ public JsonNode Scan( /// The Syft image tag to use for scanning /// The image that will be scanned /// - /// Paths to exclude from the scan, relative to the root of + /// Paths to exclude from the scan, relative to the root of /// /// - /// The contents of a Dockerfile that, when built, will scan using + /// The contents of a Dockerfile that, when built, will scan using /// Syft and output the results in a scratch image layer. When used in combination with the /// docker build --output argument, the results will be written to a file named `syft.json`. /// @@ -117,30 +116,32 @@ IEnumerable excludePaths ]; excludePaths ??= []; - IEnumerable excludeArgs = excludePaths.SelectMany(path => { + IEnumerable excludeArgs = excludePaths.SelectMany(path => + { // Syft expects relative paths. The paths are relative to the scan directory, which in // this case is /rootfs/. if (!path.StartsWith("./")) { path = "./" + path.TrimStart('/'); } + return new[] { "--exclude", path }; }); syftCommand = [.. syftCommand, .. excludeArgs]; - var syftCommandString = string.Join(", ", syftCommand.Select(a => $"\"{a}\"")); + string syftCommandString = string.Join(", ", syftCommand.Select(a => $"\"{a}\"")); // Since syft is running outside its container, it can't access its copy of CA certificates, // meaning it can't make a secure HTTPS connection to its update server, so we need to // disable its update check. Also, we don't need it checking for updates during tests anyways. - const string DisableSyftUpdateCheck = "ENV SYFT_CHECK_FOR_APP_UPDATE=0"; + const string disableSyftUpdateCheck = "ENV SYFT_CHECK_FOR_APP_UPDATE=0"; return $""" FROM {syftImage} AS syft FROM {imageToScan} AS scan-image FROM syft AS run-scan - {DisableSyftUpdateCheck} + {disableSyftUpdateCheck} USER root RUN --mount=from=scan-image,source=/,target=/rootfs \ [{syftCommandString}] diff --git a/tests/Microsoft.DotNet.Docker.Tests/TestAppArtifacts/Directory.Build.props b/tests/Microsoft.DotNet.Docker.Tests/TestAppArtifacts/Directory.Build.props new file mode 100644 index 0000000000..935eada6ac --- /dev/null +++ b/tests/Microsoft.DotNet.Docker.Tests/TestAppArtifacts/Directory.Build.props @@ -0,0 +1,4 @@ + + + diff --git a/tests/Microsoft.DotNet.Docker.Tests/TestData.cs b/tests/Microsoft.DotNet.Docker.Tests/TestData.cs index 5408c61ca5..b8acaf2eb2 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/TestData.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/TestData.cs @@ -1,6 +1,5 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. using System; using System.Collections.Generic; @@ -8,629 +7,628 @@ using System.Text.RegularExpressions; using static Microsoft.DotNet.Docker.Tests.ImageVersion; -namespace Microsoft.DotNet.Docker.Tests +namespace Microsoft.DotNet.Docker.Tests; + +public static class TestData { - public static class TestData + private static readonly ProductImageData[] LinuxTestData = + [ + new ProductImageData { Version = V8_0, OS = OS.BookwormSlim, Arch = Arch.Amd64 }, + new ProductImageData { Version = V8_0, OS = OS.Jammy, Arch = Arch.Amd64 }, + new ProductImageData { Version = V8_0, OS = OS.JammyChiseled, Arch = Arch.Amd64, SdkOS = OS.Jammy }, + new ProductImageData { Version = V8_0, OS = OS.JammyChiseled, Arch = Arch.Amd64, SdkOS = OS.Jammy }, + new ProductImageData { Version = V8_0, OS = OS.JammyChiseled, Arch = Arch.Amd64, SdkOS = OS.Jammy, + ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V8_0, OS = OS.JammyChiseled, Arch = Arch.Amd64, SdkOS = OS.Jammy, + ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V8_0, OS = OS.JammyChiseled, Arch = Arch.Amd64, SdkOS = OS.Jammy, + ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V8_0, OS = OS.Noble, Arch = Arch.Amd64 }, + new ProductImageData { Version = V8_0, OS = OS.NobleChiseled, Arch = Arch.Amd64, SdkOS = OS.Noble }, + new ProductImageData { Version = V8_0, OS = OS.NobleChiseled, Arch = Arch.Amd64, SdkOS = OS.Noble }, + new ProductImageData { Version = V8_0, OS = OS.NobleChiseled, Arch = Arch.Amd64, SdkOS = OS.Noble, + ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V8_0, OS = OS.NobleChiseled, Arch = Arch.Amd64, SdkOS = OS.Noble, + ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V8_0, OS = OS.NobleChiseled, Arch = Arch.Amd64, SdkOS = OS.Noble, + ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V8_0, OS = OS.Alpine324, Arch = Arch.Amd64 }, + new ProductImageData { Version = V8_0, OS = OS.Alpine324, Arch = Arch.Amd64, SdkOS = OS.Alpine324, + ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V8_0, OS = OS.Alpine324, Arch = Arch.Amd64, SdkOS = OS.Alpine324, + ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V8_0, OS = OS.Alpine324, Arch = Arch.Amd64, SdkOS = OS.Alpine324, + ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V8_0, OS = OS.AzureLinux30, Arch = Arch.Amd64 }, + new ProductImageData { Version = V8_0, OS = OS.AzureLinux30Distroless, Arch = Arch.Amd64, SdkOS = OS.AzureLinux30 }, + new ProductImageData { Version = V8_0, OS = OS.AzureLinux30Distroless, Arch = Arch.Amd64, SdkOS = OS.AzureLinux30, + ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V8_0, OS = OS.AzureLinux30Distroless, Arch = Arch.Amd64, SdkOS = OS.AzureLinux30, + ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V8_0, OS = OS.AzureLinux30Distroless, Arch = Arch.Amd64, SdkOS = OS.AzureLinux30, + ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, + + new ProductImageData { Version = V8_0, OS = OS.AzureLinux30, Arch = Arch.Arm64 }, + new ProductImageData { Version = V8_0, OS = OS.AzureLinux30Distroless, Arch = Arch.Arm64, SdkOS = OS.AzureLinux30 }, + new ProductImageData { Version = V8_0, OS = OS.AzureLinux30Distroless, Arch = Arch.Arm64, SdkOS = OS.AzureLinux30, + ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V8_0, OS = OS.AzureLinux30Distroless, Arch = Arch.Arm64, SdkOS = OS.AzureLinux30, + ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V8_0, OS = OS.AzureLinux30Distroless, Arch = Arch.Arm64, SdkOS = OS.AzureLinux30, + ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V8_0, OS = OS.BookwormSlim, Arch = Arch.Arm64 }, + new ProductImageData { Version = V8_0, OS = OS.Jammy, Arch = Arch.Arm64 }, + new ProductImageData { Version = V8_0, OS = OS.JammyChiseled, Arch = Arch.Arm64, SdkOS = OS.Jammy }, + new ProductImageData { Version = V8_0, OS = OS.JammyChiseled, Arch = Arch.Arm64, SdkOS = OS.Jammy, + ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V8_0, OS = OS.JammyChiseled, Arch = Arch.Arm64, SdkOS = OS.Jammy, + ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V8_0, OS = OS.JammyChiseled, Arch = Arch.Arm64, SdkOS = OS.Jammy, + ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V8_0, OS = OS.Noble, Arch = Arch.Arm64 }, + new ProductImageData { Version = V8_0, OS = OS.NobleChiseled, Arch = Arch.Arm64, SdkOS = OS.Noble }, + new ProductImageData { Version = V8_0, OS = OS.NobleChiseled, Arch = Arch.Arm64, SdkOS = OS.Noble, + ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V8_0, OS = OS.NobleChiseled, Arch = Arch.Arm64, SdkOS = OS.Noble, + ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V8_0, OS = OS.NobleChiseled, Arch = Arch.Arm64, SdkOS = OS.Noble, + ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V8_0, OS = OS.Alpine324, Arch = Arch.Arm64 }, + new ProductImageData { Version = V8_0, OS = OS.Alpine324, Arch = Arch.Arm64, SdkOS = OS.Alpine324, + ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V8_0, OS = OS.Alpine324, Arch = Arch.Arm64, SdkOS = OS.Alpine324, + ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V8_0, OS = OS.Alpine324, Arch = Arch.Arm64, SdkOS = OS.Alpine324, + ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, + + new ProductImageData { Version = V8_0, OS = OS.BookwormSlim, Arch = Arch.Arm }, + new ProductImageData { Version = V8_0, OS = OS.Jammy, Arch = Arch.Arm }, + new ProductImageData { Version = V8_0, OS = OS.JammyChiseled, Arch = Arch.Arm, SdkOS = OS.Jammy }, + new ProductImageData { Version = V8_0, OS = OS.JammyChiseled, Arch = Arch.Arm, SdkOS = OS.Jammy, + ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V8_0, OS = OS.JammyChiseled, Arch = Arch.Arm, SdkOS = OS.Jammy, + ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V8_0, OS = OS.JammyChiseled, Arch = Arch.Arm, SdkOS = OS.Jammy, + ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V8_0, OS = OS.Alpine324, Arch = Arch.Arm }, + new ProductImageData { Version = V8_0, OS = OS.Alpine324, Arch = Arch.Arm, SdkOS = OS.Alpine324, + ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V8_0, OS = OS.Alpine324, Arch = Arch.Arm, SdkOS = OS.Alpine324, + ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V8_0, OS = OS.Alpine324, Arch = Arch.Arm, SdkOS = OS.Alpine324, + ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, + + new ProductImageData { Version = V9_0, OS = OS.BookwormSlim, Arch = Arch.Amd64 }, + new ProductImageData { Version = V9_0, OS = OS.Noble, Arch = Arch.Amd64 }, + new ProductImageData { Version = V9_0, OS = OS.NobleChiseled, Arch = Arch.Amd64, SdkOS = OS.Noble }, + new ProductImageData { Version = V9_0, OS = OS.NobleChiseled, Arch = Arch.Amd64, SdkOS = OS.Noble }, + new ProductImageData { Version = V9_0, OS = OS.NobleChiseled, Arch = Arch.Amd64, SdkOS = OS.Noble, + ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V9_0, OS = OS.NobleChiseled, Arch = Arch.Amd64, SdkOS = OS.Noble, + ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V9_0, OS = OS.NobleChiseled, Arch = Arch.Amd64, SdkOS = OS.Noble, + ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V9_0, OS = OS.Alpine324, Arch = Arch.Amd64 }, + new ProductImageData { Version = V9_0, OS = OS.Alpine324, Arch = Arch.Amd64, SdkOS = OS.Alpine324, + ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V9_0, OS = OS.Alpine324, Arch = Arch.Amd64, SdkOS = OS.Alpine324, + ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V9_0, OS = OS.Alpine324, Arch = Arch.Amd64, SdkOS = OS.Alpine324, + ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V9_0, OS = OS.AzureLinux30, Arch = Arch.Amd64 }, + new ProductImageData { Version = V9_0, OS = OS.AzureLinux30Distroless, Arch = Arch.Amd64, SdkOS = OS.AzureLinux30 }, + new ProductImageData { Version = V9_0, OS = OS.AzureLinux30Distroless, Arch = Arch.Amd64, SdkOS = OS.AzureLinux30, + ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V9_0, OS = OS.AzureLinux30Distroless, Arch = Arch.Amd64, SdkOS = OS.AzureLinux30, + ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V9_0, OS = OS.AzureLinux30Distroless, Arch = Arch.Amd64, SdkOS = OS.AzureLinux30, + ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, + + new ProductImageData { Version = V9_0, OS = OS.AzureLinux30, Arch = Arch.Arm64 }, + new ProductImageData { Version = V9_0, OS = OS.AzureLinux30Distroless, Arch = Arch.Arm64, SdkOS = OS.AzureLinux30 }, + new ProductImageData { Version = V9_0, OS = OS.AzureLinux30Distroless, Arch = Arch.Arm64, SdkOS = OS.AzureLinux30, + ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V9_0, OS = OS.AzureLinux30Distroless, Arch = Arch.Arm64, SdkOS = OS.AzureLinux30, + ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V9_0, OS = OS.AzureLinux30Distroless, Arch = Arch.Arm64, SdkOS = OS.AzureLinux30, + ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V9_0, OS = OS.BookwormSlim, Arch = Arch.Arm64 }, + new ProductImageData { Version = V9_0, OS = OS.Noble, Arch = Arch.Arm64 }, + new ProductImageData { Version = V9_0, OS = OS.NobleChiseled, Arch = Arch.Arm64, SdkOS = OS.Noble }, + new ProductImageData { Version = V9_0, OS = OS.NobleChiseled, Arch = Arch.Arm64, SdkOS = OS.Noble, + ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V9_0, OS = OS.NobleChiseled, Arch = Arch.Arm64, SdkOS = OS.Noble, + ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V9_0, OS = OS.NobleChiseled, Arch = Arch.Arm64, SdkOS = OS.Noble, + ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V9_0, OS = OS.Alpine324, Arch = Arch.Arm64 }, + new ProductImageData { Version = V9_0, OS = OS.Alpine324, Arch = Arch.Arm64, SdkOS = OS.Alpine324, + ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V9_0, OS = OS.Alpine324, Arch = Arch.Arm64, SdkOS = OS.Alpine324, + ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V9_0, OS = OS.Alpine324, Arch = Arch.Arm64, SdkOS = OS.Alpine324, + ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, + + new ProductImageData { Version = V9_0, OS = OS.BookwormSlim, Arch = Arch.Arm }, + new ProductImageData { Version = V9_0, OS = OS.Alpine324, Arch = Arch.Arm }, + new ProductImageData { Version = V9_0, OS = OS.Alpine324, Arch = Arch.Arm, SdkOS = OS.Alpine324, + ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V9_0, OS = OS.Alpine324, Arch = Arch.Arm, SdkOS = OS.Alpine324, + ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V9_0, OS = OS.Alpine324, Arch = Arch.Arm, SdkOS = OS.Alpine324, + ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V9_0, OS = OS.Noble, Arch = Arch.Arm }, + new ProductImageData { Version = V9_0, OS = OS.NobleChiseled, Arch = Arch.Arm, SdkOS = OS.Noble }, + new ProductImageData { Version = V9_0, OS = OS.NobleChiseled, Arch = Arch.Arm, SdkOS = OS.Noble, + ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V9_0, OS = OS.NobleChiseled, Arch = Arch.Arm, SdkOS = OS.Noble, + ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V9_0, OS = OS.NobleChiseled, Arch = Arch.Arm, SdkOS = OS.Noble, + ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, + + new ProductImageData { Version = V10_0, OS = OS.Noble, Arch = Arch.Amd64 }, + new ProductImageData { Version = V10_0, OS = OS.NobleChiseled, Arch = Arch.Amd64, SdkOS = OS.Noble }, + new ProductImageData { Version = V10_0, OS = OS.NobleChiseled, Arch = Arch.Amd64, SdkOS = OS.Noble, + ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V10_0, OS = OS.NobleChiseled, Arch = Arch.Amd64, SdkOS = OS.Noble, + ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V10_0, OS = OS.NobleChiseled, Arch = Arch.Amd64, SdkOS = OS.Noble, + ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V10_0, OS = OS.Alpine324, Arch = Arch.Amd64 }, + new ProductImageData { Version = V10_0, OS = OS.Alpine324, Arch = Arch.Amd64, SdkOS = OS.Alpine324, + ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V10_0, OS = OS.Alpine324, Arch = Arch.Amd64, SdkOS = OS.Alpine324, + ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V10_0, OS = OS.Alpine324, Arch = Arch.Amd64, SdkOS = OS.Alpine324, + ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V10_0, OS = OS.AzureLinux30, Arch = Arch.Amd64 }, + new ProductImageData { Version = V10_0, OS = OS.AzureLinux40, Arch = Arch.Amd64 }, + new ProductImageData { Version = V10_0, OS = OS.AzureLinux30Distroless, Arch = Arch.Amd64, SdkOS = OS.AzureLinux30 }, + new ProductImageData { Version = V10_0, OS = OS.AzureLinux40Distroless, Arch = Arch.Amd64, SdkOS = OS.AzureLinux40 }, + new ProductImageData { Version = V10_0, OS = OS.AzureLinux30Distroless, Arch = Arch.Amd64, SdkOS = OS.AzureLinux30, + ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V10_0, OS = OS.AzureLinux40Distroless, Arch = Arch.Amd64, SdkOS = OS.AzureLinux40, + ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V10_0, OS = OS.AzureLinux30Distroless, Arch = Arch.Amd64, SdkOS = OS.AzureLinux30, + ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V10_0, OS = OS.AzureLinux40Distroless, Arch = Arch.Amd64, SdkOS = OS.AzureLinux40, + ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V10_0, OS = OS.AzureLinux30Distroless, Arch = Arch.Amd64, SdkOS = OS.AzureLinux30, + ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V10_0, OS = OS.AzureLinux40Distroless, Arch = Arch.Amd64, SdkOS = OS.AzureLinux40, + ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, + + new() { Version = V10_0, Arch = Arch.Amd64, SdkImageVariant = DotNetImageVariant.AOT, SupportedImageRepos = DotNetImageRepo.Runtime_Deps, OS = OS.Alpine324 }, + new() { Version = V10_0, Arch = Arch.Amd64, SdkImageVariant = DotNetImageVariant.AOT, SupportedImageRepos = DotNetImageRepo.Runtime_Deps, OS = OS.AzureLinux30Distroless, SdkOS = OS.AzureLinux30 }, + new() { Version = V10_0, Arch = Arch.Amd64, SdkImageVariant = DotNetImageVariant.AOT, SupportedImageRepos = DotNetImageRepo.Runtime_Deps, OS = OS.AzureLinux40Distroless, SdkOS = OS.AzureLinux40 }, + new() { Version = V10_0, Arch = Arch.Amd64, SdkImageVariant = DotNetImageVariant.AOT, SupportedImageRepos = DotNetImageRepo.Runtime_Deps, OS = OS.NobleChiseled, SdkOS = OS.Noble }, + new ProductImageData { Version = V10_0, OS = OS.Resolute, Arch = Arch.Amd64 }, + new ProductImageData { Version = V10_0, OS = OS.ResoluteChiseled, Arch = Arch.Amd64, SdkOS = OS.Resolute }, + new ProductImageData { Version = V10_0, OS = OS.ResoluteChiseled, Arch = Arch.Amd64, SdkOS = OS.Resolute, + ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V10_0, OS = OS.ResoluteChiseled, Arch = Arch.Amd64, SdkOS = OS.Resolute, + ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V10_0, OS = OS.ResoluteChiseled, Arch = Arch.Amd64, SdkOS = OS.Resolute, + ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, + new() { Version = V10_0, Arch = Arch.Amd64, SdkImageVariant = DotNetImageVariant.AOT, SupportedImageRepos = DotNetImageRepo.Runtime_Deps, OS = OS.ResoluteChiseled, SdkOS = OS.Resolute }, + + new ProductImageData { Version = V10_0, OS = OS.AzureLinux30, Arch = Arch.Arm64 }, + new ProductImageData { Version = V10_0, OS = OS.AzureLinux40, Arch = Arch.Arm64 }, + new ProductImageData { Version = V10_0, OS = OS.AzureLinux30Distroless, Arch = Arch.Arm64, SdkOS = OS.AzureLinux30 }, + new ProductImageData { Version = V10_0, OS = OS.AzureLinux40Distroless, Arch = Arch.Arm64, SdkOS = OS.AzureLinux40 }, + new ProductImageData { Version = V10_0, OS = OS.AzureLinux30Distroless, Arch = Arch.Arm64, SdkOS = OS.AzureLinux30, + ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V10_0, OS = OS.AzureLinux40Distroless, Arch = Arch.Arm64, SdkOS = OS.AzureLinux40, + ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V10_0, OS = OS.AzureLinux30Distroless, Arch = Arch.Arm64, SdkOS = OS.AzureLinux30, + ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V10_0, OS = OS.AzureLinux40Distroless, Arch = Arch.Arm64, SdkOS = OS.AzureLinux40, + ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V10_0, OS = OS.AzureLinux30Distroless, Arch = Arch.Arm64, SdkOS = OS.AzureLinux30, + ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V10_0, OS = OS.AzureLinux40Distroless, Arch = Arch.Arm64, SdkOS = OS.AzureLinux40, + ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V10_0, OS = OS.Noble, Arch = Arch.Arm64 }, + new ProductImageData { Version = V10_0, OS = OS.NobleChiseled, Arch = Arch.Arm64, SdkOS = OS.Noble }, + new ProductImageData { Version = V10_0, OS = OS.NobleChiseled, Arch = Arch.Arm64, SdkOS = OS.Noble, + ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V10_0, OS = OS.NobleChiseled, Arch = Arch.Arm64, SdkOS = OS.Noble, + ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V10_0, OS = OS.NobleChiseled, Arch = Arch.Arm64, SdkOS = OS.Noble, + ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V10_0, OS = OS.Alpine324, Arch = Arch.Arm64 }, + new ProductImageData { Version = V10_0, OS = OS.Alpine324, Arch = Arch.Arm64, SdkOS = OS.Alpine324, + ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V10_0, OS = OS.Alpine324, Arch = Arch.Arm64, SdkOS = OS.Alpine324, + ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V10_0, OS = OS.Alpine324, Arch = Arch.Arm64, SdkOS = OS.Alpine324, + ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, + + new() { Version = V10_0, Arch = Arch.Arm64, SdkImageVariant = DotNetImageVariant.AOT, SupportedImageRepos = DotNetImageRepo.Runtime_Deps, OS = OS.Alpine324 }, + new() { Version = V10_0, Arch = Arch.Arm64, SdkImageVariant = DotNetImageVariant.AOT, SupportedImageRepos = DotNetImageRepo.Runtime_Deps, OS = OS.AzureLinux30Distroless, SdkOS = OS.AzureLinux30 }, + new() { Version = V10_0, Arch = Arch.Arm64, SdkImageVariant = DotNetImageVariant.AOT, SupportedImageRepos = DotNetImageRepo.Runtime_Deps, OS = OS.AzureLinux40Distroless, SdkOS = OS.AzureLinux40 }, + new() { Version = V10_0, Arch = Arch.Arm64, SdkImageVariant = DotNetImageVariant.AOT, SupportedImageRepos = DotNetImageRepo.Runtime_Deps, OS = OS.NobleChiseled, SdkOS = OS.Noble }, + new ProductImageData { Version = V10_0, OS = OS.Resolute, Arch = Arch.Arm64 }, + new ProductImageData { Version = V10_0, OS = OS.ResoluteChiseled, Arch = Arch.Arm64, SdkOS = OS.Resolute }, + new ProductImageData { Version = V10_0, OS = OS.ResoluteChiseled, Arch = Arch.Arm64, SdkOS = OS.Resolute, + ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V10_0, OS = OS.ResoluteChiseled, Arch = Arch.Arm64, SdkOS = OS.Resolute, + ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V10_0, OS = OS.ResoluteChiseled, Arch = Arch.Arm64, SdkOS = OS.Resolute, + ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, + new() { Version = V10_0, Arch = Arch.Arm64, SdkImageVariant = DotNetImageVariant.AOT, SupportedImageRepos = DotNetImageRepo.Runtime_Deps, OS = OS.ResoluteChiseled, SdkOS = OS.Resolute }, + + new ProductImageData { Version = V10_0, OS = OS.Alpine324, Arch = Arch.Arm }, + new ProductImageData { Version = V10_0, OS = OS.Alpine324, Arch = Arch.Arm, SdkOS = OS.Alpine324, + ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V10_0, OS = OS.Alpine324, Arch = Arch.Arm, SdkOS = OS.Alpine324, + ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V10_0, OS = OS.Alpine324, Arch = Arch.Arm, SdkOS = OS.Alpine324, + ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V10_0, OS = OS.Noble, Arch = Arch.Arm }, + new ProductImageData { Version = V10_0, OS = OS.NobleChiseled, Arch = Arch.Arm, SdkOS = OS.Noble }, + new ProductImageData { Version = V10_0, OS = OS.NobleChiseled, Arch = Arch.Arm, SdkOS = OS.Noble, + ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V10_0, OS = OS.NobleChiseled, Arch = Arch.Arm, SdkOS = OS.Noble, + ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V10_0, OS = OS.NobleChiseled, Arch = Arch.Arm, SdkOS = OS.Noble, + ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V10_0, OS = OS.Resolute, Arch = Arch.Arm }, + new ProductImageData { Version = V10_0, OS = OS.ResoluteChiseled, Arch = Arch.Arm, SdkOS = OS.Resolute }, + new ProductImageData { Version = V10_0, OS = OS.ResoluteChiseled, Arch = Arch.Arm, SdkOS = OS.Resolute, + ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V10_0, OS = OS.ResoluteChiseled, Arch = Arch.Arm, SdkOS = OS.Resolute, + ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V10_0, OS = OS.ResoluteChiseled, Arch = Arch.Arm, SdkOS = OS.Resolute, + ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, + + // .NET 11 Linux Amd64 + new ProductImageData { Version = V11_0, OS = OS.Resolute, Arch = Arch.Amd64 }, + new ProductImageData { Version = V11_0, OS = OS.ResoluteChiseled, Arch = Arch.Amd64, SdkOS = OS.Resolute }, + new ProductImageData { Version = V11_0, OS = OS.ResoluteChiseled, Arch = Arch.Amd64, SdkOS = OS.Resolute, + ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V11_0, OS = OS.ResoluteChiseled, Arch = Arch.Amd64, SdkOS = OS.Resolute, + ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V11_0, OS = OS.ResoluteChiseled, Arch = Arch.Amd64, SdkOS = OS.Resolute, + ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V11_0, OS = OS.Alpine324, Arch = Arch.Amd64 }, + new ProductImageData { Version = V11_0, OS = OS.Alpine324, Arch = Arch.Amd64, SdkOS = OS.Alpine324, + ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V11_0, OS = OS.Alpine324, Arch = Arch.Amd64, SdkOS = OS.Alpine324, + ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V11_0, OS = OS.Alpine324, Arch = Arch.Amd64, SdkOS = OS.Alpine324, + ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V11_0, OS = OS.AzureLinux30, Arch = Arch.Amd64 }, + new ProductImageData { Version = V11_0, OS = OS.AzureLinux40, Arch = Arch.Amd64 }, + new ProductImageData { Version = V11_0, OS = OS.AzureLinux30Distroless, Arch = Arch.Amd64, SdkOS = OS.AzureLinux30 }, + new ProductImageData { Version = V11_0, OS = OS.AzureLinux40Distroless, Arch = Arch.Amd64, SdkOS = OS.AzureLinux40 }, + new ProductImageData { Version = V11_0, OS = OS.AzureLinux30Distroless, Arch = Arch.Amd64, SdkOS = OS.AzureLinux30, + ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V11_0, OS = OS.AzureLinux40Distroless, Arch = Arch.Amd64, SdkOS = OS.AzureLinux40, + ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V11_0, OS = OS.AzureLinux30Distroless, Arch = Arch.Amd64, SdkOS = OS.AzureLinux30, + ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V11_0, OS = OS.AzureLinux40Distroless, Arch = Arch.Amd64, SdkOS = OS.AzureLinux40, + ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V11_0, OS = OS.AzureLinux30Distroless, Arch = Arch.Amd64, SdkOS = OS.AzureLinux30, + ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V11_0, OS = OS.AzureLinux40Distroless, Arch = Arch.Amd64, SdkOS = OS.AzureLinux40, + ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, + + new() { Version = V11_0, Arch = Arch.Amd64, SdkImageVariant = DotNetImageVariant.AOT, SupportedImageRepos = DotNetImageRepo.Runtime_Deps, OS = OS.Alpine324 }, + new() { Version = V11_0, Arch = Arch.Amd64, SdkImageVariant = DotNetImageVariant.AOT, SupportedImageRepos = DotNetImageRepo.Runtime_Deps, OS = OS.AzureLinux30Distroless, SdkOS = OS.AzureLinux30 }, + new() { Version = V11_0, Arch = Arch.Amd64, SdkImageVariant = DotNetImageVariant.AOT, SupportedImageRepos = DotNetImageRepo.Runtime_Deps, OS = OS.AzureLinux40Distroless, SdkOS = OS.AzureLinux40 }, + new() { Version = V11_0, Arch = Arch.Amd64, SdkImageVariant = DotNetImageVariant.AOT, SupportedImageRepos = DotNetImageRepo.Runtime_Deps, OS = OS.ResoluteChiseled, SdkOS = OS.Resolute }, + + // .NET 11 Linux Arm64 + new ProductImageData { Version = V11_0, OS = OS.AzureLinux30, Arch = Arch.Arm64 }, + new ProductImageData { Version = V11_0, OS = OS.AzureLinux40, Arch = Arch.Arm64 }, + new ProductImageData { Version = V11_0, OS = OS.AzureLinux30Distroless, Arch = Arch.Arm64, SdkOS = OS.AzureLinux30 }, + new ProductImageData { Version = V11_0, OS = OS.AzureLinux40Distroless, Arch = Arch.Arm64, SdkOS = OS.AzureLinux40 }, + new ProductImageData { Version = V11_0, OS = OS.AzureLinux30Distroless, Arch = Arch.Arm64, SdkOS = OS.AzureLinux30, + ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V11_0, OS = OS.AzureLinux40Distroless, Arch = Arch.Arm64, SdkOS = OS.AzureLinux40, + ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V11_0, OS = OS.AzureLinux30Distroless, Arch = Arch.Arm64, SdkOS = OS.AzureLinux30, + ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V11_0, OS = OS.AzureLinux40Distroless, Arch = Arch.Arm64, SdkOS = OS.AzureLinux40, + ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V11_0, OS = OS.AzureLinux30Distroless, Arch = Arch.Arm64, SdkOS = OS.AzureLinux30, + ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V11_0, OS = OS.AzureLinux40Distroless, Arch = Arch.Arm64, SdkOS = OS.AzureLinux40, + ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V11_0, OS = OS.Resolute, Arch = Arch.Arm64 }, + new ProductImageData { Version = V11_0, OS = OS.ResoluteChiseled, Arch = Arch.Arm64, SdkOS = OS.Resolute }, + new ProductImageData { Version = V11_0, OS = OS.ResoluteChiseled, Arch = Arch.Arm64, SdkOS = OS.Resolute, + ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V11_0, OS = OS.ResoluteChiseled, Arch = Arch.Arm64, SdkOS = OS.Resolute, + ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V11_0, OS = OS.ResoluteChiseled, Arch = Arch.Arm64, SdkOS = OS.Resolute, + ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V11_0, OS = OS.Alpine324, Arch = Arch.Arm64 }, + new ProductImageData { Version = V11_0, OS = OS.Alpine324, Arch = Arch.Arm64, SdkOS = OS.Alpine324, + ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V11_0, OS = OS.Alpine324, Arch = Arch.Arm64, SdkOS = OS.Alpine324, + ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V11_0, OS = OS.Alpine324, Arch = Arch.Arm64, SdkOS = OS.Alpine324, + ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, + + new() { Version = V11_0, Arch = Arch.Arm64, SdkImageVariant = DotNetImageVariant.AOT, SupportedImageRepos = DotNetImageRepo.Runtime_Deps, OS = OS.Alpine324 }, + new() { Version = V11_0, Arch = Arch.Arm64, SdkImageVariant = DotNetImageVariant.AOT, SupportedImageRepos = DotNetImageRepo.Runtime_Deps, OS = OS.AzureLinux30Distroless, SdkOS = OS.AzureLinux30 }, + new() { Version = V11_0, Arch = Arch.Arm64, SdkImageVariant = DotNetImageVariant.AOT, SupportedImageRepos = DotNetImageRepo.Runtime_Deps, OS = OS.AzureLinux40Distroless, SdkOS = OS.AzureLinux40 }, + new() { Version = V11_0, Arch = Arch.Arm64, SdkImageVariant = DotNetImageVariant.AOT, SupportedImageRepos = DotNetImageRepo.Runtime_Deps, OS = OS.ResoluteChiseled, SdkOS = OS.Resolute }, + + // .NET 11 Linux Arm32 + new ProductImageData { Version = V11_0, OS = OS.Alpine324, Arch = Arch.Arm }, + new ProductImageData { Version = V11_0, OS = OS.Alpine324, Arch = Arch.Arm, SdkOS = OS.Alpine324, + ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V11_0, OS = OS.Alpine324, Arch = Arch.Arm, SdkOS = OS.Alpine324, + ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V11_0, OS = OS.Alpine324, Arch = Arch.Arm, SdkOS = OS.Alpine324, + ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V11_0, OS = OS.Resolute, Arch = Arch.Arm }, + new ProductImageData { Version = V11_0, OS = OS.ResoluteChiseled, Arch = Arch.Arm, SdkOS = OS.Resolute }, + new ProductImageData { Version = V11_0, OS = OS.ResoluteChiseled, Arch = Arch.Arm, SdkOS = OS.Resolute, + ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V11_0, OS = OS.ResoluteChiseled, Arch = Arch.Arm, SdkOS = OS.Resolute, + ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, + new ProductImageData { Version = V11_0, OS = OS.ResoluteChiseled, Arch = Arch.Arm, SdkOS = OS.Resolute, + ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, + ]; + + private static readonly ProductImageData[] WindowsTestData = + [ + new ProductImageData { Version = V8_0, OS = OS.NanoServer1809, Arch = Arch.Amd64 }, + new ProductImageData { Version = V8_0, OS = OS.NanoServerLtsc2022, Arch = Arch.Amd64 }, + new ProductImageData { Version = V8_0, OS = OS.NanoServerLtsc2025, Arch = Arch.Amd64 }, + new ProductImageData { Version = V8_0, OS = OS.ServerCoreLtsc2019, Arch = Arch.Amd64 }, + new ProductImageData { Version = V8_0, OS = OS.ServerCoreLtsc2022, Arch = Arch.Amd64 }, + new ProductImageData { Version = V8_0, OS = OS.ServerCoreLtsc2025, Arch = Arch.Amd64 }, + + new ProductImageData { Version = V9_0, OS = OS.NanoServer1809, Arch = Arch.Amd64 }, + new ProductImageData { Version = V9_0, OS = OS.NanoServerLtsc2022, Arch = Arch.Amd64 }, + new ProductImageData { Version = V9_0, OS = OS.NanoServerLtsc2025, Arch = Arch.Amd64 }, + new ProductImageData { Version = V9_0, OS = OS.ServerCoreLtsc2019, Arch = Arch.Amd64 }, + new ProductImageData { Version = V9_0, OS = OS.ServerCoreLtsc2022, Arch = Arch.Amd64 }, + new ProductImageData { Version = V9_0, OS = OS.ServerCoreLtsc2025, Arch = Arch.Amd64 }, + + new ProductImageData { Version = V10_0, OS = OS.NanoServerLtsc2022, Arch = Arch.Amd64 }, + new ProductImageData { Version = V10_0, OS = OS.NanoServerLtsc2025, Arch = Arch.Amd64 }, + new ProductImageData { Version = V10_0, OS = OS.ServerCoreLtsc2022, Arch = Arch.Amd64 }, + new ProductImageData { Version = V10_0, OS = OS.ServerCoreLtsc2025, Arch = Arch.Amd64 }, + + new ProductImageData { Version = V11_0, OS = OS.NanoServerLtsc2025, Arch = Arch.Amd64 }, + new ProductImageData { Version = V11_0, OS = OS.ServerCoreLtsc2025, Arch = Arch.Amd64 }, + ]; + + private static readonly SampleImageData[] LinuxSampleTestData = + [ + new SampleImageData { OS = OS.AlpineFloating, Arch = Arch.Amd64, DockerfileSuffix = "alpine", IsPublished = true }, + new SampleImageData { OS = OS.AlpineFloating, Arch = Arch.Arm, DockerfileSuffix = "alpine", IsPublished = true }, + new SampleImageData { OS = OS.AlpineFloating, Arch = Arch.Arm64, DockerfileSuffix = "alpine", IsPublished = true }, + new SampleImageData { OS = OS.ResoluteChiseled, Arch = Arch.Arm, DockerfileSuffix = "chiseled", IsPublished = true }, + new SampleImageData { OS = OS.ResoluteChiseled, Arch = Arch.Arm64, DockerfileSuffix = "chiseled", IsPublished = true }, + new SampleImageData { OS = OS.ResoluteChiseled, Arch = Arch.Amd64, DockerfileSuffix = "chiseled", IsPublished = true }, + + new SampleImageData { OS = OS.Resolute, Arch = Arch.Amd64 }, + new SampleImageData { OS = OS.Resolute, Arch = Arch.Arm }, + new SampleImageData { OS = OS.Resolute, Arch = Arch.Arm64 }, + new SampleImageData { OS = OS.AlpineFloating, Arch = Arch.Arm64, DockerfileSuffix = "alpine" }, + new SampleImageData { OS = OS.AlpineFloating, Arch = Arch.Amd64, DockerfileSuffix = "alpine" }, + new SampleImageData { OS = OS.AlpineFloating, Arch = Arch.Arm64, DockerfileSuffix = "alpine-icu" }, + new SampleImageData { OS = OS.AlpineFloating, Arch = Arch.Amd64, DockerfileSuffix = "alpine-icu" }, + new SampleImageData { OS = OS.Resolute, Arch = Arch.Arm, DockerfileSuffix = "ubuntu" }, + new SampleImageData { OS = OS.Resolute, Arch = Arch.Arm64, DockerfileSuffix = "ubuntu" }, + new SampleImageData { OS = OS.Resolute, Arch = Arch.Amd64, DockerfileSuffix = "ubuntu" }, + new SampleImageData { OS = OS.ResoluteChiseled, Arch = Arch.Arm, DockerfileSuffix = "chiseled" }, + new SampleImageData { OS = OS.ResoluteChiseled, Arch = Arch.Arm64, DockerfileSuffix = "chiseled" }, + new SampleImageData { OS = OS.ResoluteChiseled, Arch = Arch.Amd64, DockerfileSuffix = "chiseled" }, + ]; + + private static readonly SampleImageData[] WindowsSampleTestData = + [ + new SampleImageData { OS = OS.NanoServerLtsc2025, Arch = Arch.Amd64, IsPublished = true }, + + new SampleImageData { OS = OS.NanoServerLtsc2025, Arch = Arch.Amd64, DockerfileSuffix = "nanoserver" }, + + // Use Nano Server as the OS even though the Dockerfiles are for Windows Server Core. This is because the OS value + // needs to match the filter set by the build/test job. We only produce builds jobs based on what's in the manifest + // and the manifest only defines Nano Server-based Dockerfiles. So we just need to piggyback on the Nano Server + // jobs in order to test the Windows Server Core samples. + new SampleImageData { OS = OS.NanoServerLtsc2025, Arch = Arch.Amd64, DockerfileSuffix = "windowsservercore" }, + new SampleImageData { OS = OS.NanoServerLtsc2025, Arch = Arch.Amd64, DockerfileSuffix = "windowsservercore-iis" }, + ]; + + private static readonly ProductImageData[] LinuxMonitorTestData = + [ + new ProductImageData { Version = V8_1, VersionFamily = V8_0, OS = OS.JammyChiseled, OSTag = OS.UbuntuChiseled, Arch = Arch.Amd64, SupportedImageRepos = DotNetImageRepo.Monitor | DotNetImageRepo.Monitor_Base }, + new ProductImageData { Version = V8_1, VersionFamily = V8_0, OS = OS.JammyChiseled, OSTag = OS.UbuntuChiseled, Arch = Arch.Arm64, SupportedImageRepos = DotNetImageRepo.Monitor | DotNetImageRepo.Monitor_Base }, + // OSTag does not correspond to OS because platform tags for Azure Linux were not added to the images + // Use CBL-Mariner distroless for OSTag since those platform tags exist and won't require tests to understand the difference in tagging. + new ProductImageData { Version = V8_1, VersionFamily = V8_0, OS = OS.AzureLinux30Distroless, OSDir = OS.AzureLinuxDistroless, OSTag = OS.MarinerDistroless, Arch = Arch.Amd64, SupportedImageRepos = DotNetImageRepo.Monitor | DotNetImageRepo.Monitor_Base }, + new ProductImageData { Version = V8_1, VersionFamily = V8_0, OS = OS.AzureLinux30Distroless, OSDir = OS.AzureLinuxDistroless, OSTag = OS.MarinerDistroless, Arch = Arch.Arm64, SupportedImageRepos = DotNetImageRepo.Monitor | DotNetImageRepo.Monitor_Base }, + new ProductImageData { Version = V9_0, VersionFamily = V9_0, OS = OS.AzureLinux30Distroless, OSDir = OS.AzureLinuxDistroless, OSTag = "", Arch = Arch.Amd64, SupportedImageRepos = DotNetImageRepo.Monitor | DotNetImageRepo.Monitor_Base }, + new ProductImageData { Version = V9_0, VersionFamily = V9_0, OS = OS.AzureLinux30Distroless, OSDir = OS.AzureLinuxDistroless, OSTag = "", Arch = Arch.Arm64, SupportedImageRepos = DotNetImageRepo.Monitor | DotNetImageRepo.Monitor_Base }, + new ProductImageData { Version = V10_0, VersionFamily = V10_0, OS = OS.AzureLinux30Distroless, OSDir = OS.AzureLinuxDistroless, OSTag = "", Arch = Arch.Amd64, SupportedImageRepos = DotNetImageRepo.Monitor | DotNetImageRepo.Monitor_Base }, + new ProductImageData { Version = V10_0, VersionFamily = V10_0, OS = OS.AzureLinux30Distroless, OSDir = OS.AzureLinuxDistroless, OSTag = "", Arch = Arch.Arm64, SupportedImageRepos = DotNetImageRepo.Monitor | DotNetImageRepo.Monitor_Base }, + ]; + + private static readonly ProductImageData[] WindowsMonitorTestData = + [ + ]; + + private static readonly ProductImageData[] AspireDashboardTestData = + [ + new() { + Version = V17_0, + VersionFamily = V11_0, + OS = OS.AzureLinux30Distroless, + OSTag = "", + OSDir = OS.AzureLinuxDistroless, + Arch = Arch.Amd64, + SupportedImageRepos = DotNetImageRepo.Aspire_Dashboard, + }, + new() { + Version = V17_0, + VersionFamily = V11_0, + OS = OS.AzureLinux30Distroless, + OSTag = "", + OSDir = OS.AzureLinuxDistroless, + Arch = Arch.Arm64, + SupportedImageRepos = DotNetImageRepo.Aspire_Dashboard + }, + ]; + + private static readonly ProductImageData[] YarpTestData = + [ + new() { + Version = new ImageVersion(new Version(2,3), isPreview: true), + VersionFamily = V9_0, + OS = OS.AzureLinux30Distroless, + OSTag = "", + OSDir = OS.AzureLinuxDistroless, + Arch = Arch.Amd64, + SupportedImageRepos = DotNetImageRepo.Yarp, + }, + new() { + Version = new ImageVersion(new Version(2,3), isPreview: true), + VersionFamily = V9_0, + OS = OS.AzureLinux30Distroless, + OSTag = "", + OSDir = OS.AzureLinuxDistroless, + Arch = Arch.Arm64, + SupportedImageRepos = DotNetImageRepo.Yarp + }, + ]; + + public static IEnumerable AllImageData => + [ + ..LinuxTestData, + ..WindowsTestData, + ..AspireDashboardTestData, + ..LinuxMonitorTestData, + ..WindowsMonitorTestData, + ..YarpTestData, + ]; + + public static IEnumerable GetImageData( + DotNetImageRepo imageRepo, + DotNetImageVariant variant = DotNetImageVariant.None) { - private static readonly ProductImageData[] s_linuxTestData = - { - new ProductImageData { Version = V8_0, OS = OS.BookwormSlim, Arch = Arch.Amd64 }, - new ProductImageData { Version = V8_0, OS = OS.Jammy, Arch = Arch.Amd64 }, - new ProductImageData { Version = V8_0, OS = OS.JammyChiseled, Arch = Arch.Amd64, SdkOS = OS.Jammy }, - new ProductImageData { Version = V8_0, OS = OS.JammyChiseled, Arch = Arch.Amd64, SdkOS = OS.Jammy }, - new ProductImageData { Version = V8_0, OS = OS.JammyChiseled, Arch = Arch.Amd64, SdkOS = OS.Jammy, - ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V8_0, OS = OS.JammyChiseled, Arch = Arch.Amd64, SdkOS = OS.Jammy, - ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V8_0, OS = OS.JammyChiseled, Arch = Arch.Amd64, SdkOS = OS.Jammy, - ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V8_0, OS = OS.Noble, Arch = Arch.Amd64 }, - new ProductImageData { Version = V8_0, OS = OS.NobleChiseled, Arch = Arch.Amd64, SdkOS = OS.Noble }, - new ProductImageData { Version = V8_0, OS = OS.NobleChiseled, Arch = Arch.Amd64, SdkOS = OS.Noble }, - new ProductImageData { Version = V8_0, OS = OS.NobleChiseled, Arch = Arch.Amd64, SdkOS = OS.Noble, - ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V8_0, OS = OS.NobleChiseled, Arch = Arch.Amd64, SdkOS = OS.Noble, - ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V8_0, OS = OS.NobleChiseled, Arch = Arch.Amd64, SdkOS = OS.Noble, - ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V8_0, OS = OS.Alpine324, Arch = Arch.Amd64 }, - new ProductImageData { Version = V8_0, OS = OS.Alpine324, Arch = Arch.Amd64, SdkOS = OS.Alpine324, - ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V8_0, OS = OS.Alpine324, Arch = Arch.Amd64, SdkOS = OS.Alpine324, - ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V8_0, OS = OS.Alpine324, Arch = Arch.Amd64, SdkOS = OS.Alpine324, - ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V8_0, OS = OS.AzureLinux30, Arch = Arch.Amd64 }, - new ProductImageData { Version = V8_0, OS = OS.AzureLinux30Distroless, Arch = Arch.Amd64, SdkOS = OS.AzureLinux30 }, - new ProductImageData { Version = V8_0, OS = OS.AzureLinux30Distroless, Arch = Arch.Amd64, SdkOS = OS.AzureLinux30, - ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V8_0, OS = OS.AzureLinux30Distroless, Arch = Arch.Amd64, SdkOS = OS.AzureLinux30, - ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V8_0, OS = OS.AzureLinux30Distroless, Arch = Arch.Amd64, SdkOS = OS.AzureLinux30, - ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, - - new ProductImageData { Version = V8_0, OS = OS.AzureLinux30, Arch = Arch.Arm64 }, - new ProductImageData { Version = V8_0, OS = OS.AzureLinux30Distroless, Arch = Arch.Arm64, SdkOS = OS.AzureLinux30 }, - new ProductImageData { Version = V8_0, OS = OS.AzureLinux30Distroless, Arch = Arch.Arm64, SdkOS = OS.AzureLinux30, - ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V8_0, OS = OS.AzureLinux30Distroless, Arch = Arch.Arm64, SdkOS = OS.AzureLinux30, - ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V8_0, OS = OS.AzureLinux30Distroless, Arch = Arch.Arm64, SdkOS = OS.AzureLinux30, - ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V8_0, OS = OS.BookwormSlim, Arch = Arch.Arm64 }, - new ProductImageData { Version = V8_0, OS = OS.Jammy, Arch = Arch.Arm64 }, - new ProductImageData { Version = V8_0, OS = OS.JammyChiseled, Arch = Arch.Arm64, SdkOS = OS.Jammy }, - new ProductImageData { Version = V8_0, OS = OS.JammyChiseled, Arch = Arch.Arm64, SdkOS = OS.Jammy, - ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V8_0, OS = OS.JammyChiseled, Arch = Arch.Arm64, SdkOS = OS.Jammy, - ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V8_0, OS = OS.JammyChiseled, Arch = Arch.Arm64, SdkOS = OS.Jammy, - ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V8_0, OS = OS.Noble, Arch = Arch.Arm64 }, - new ProductImageData { Version = V8_0, OS = OS.NobleChiseled, Arch = Arch.Arm64, SdkOS = OS.Noble }, - new ProductImageData { Version = V8_0, OS = OS.NobleChiseled, Arch = Arch.Arm64, SdkOS = OS.Noble, - ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V8_0, OS = OS.NobleChiseled, Arch = Arch.Arm64, SdkOS = OS.Noble, - ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V8_0, OS = OS.NobleChiseled, Arch = Arch.Arm64, SdkOS = OS.Noble, - ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V8_0, OS = OS.Alpine324, Arch = Arch.Arm64 }, - new ProductImageData { Version = V8_0, OS = OS.Alpine324, Arch = Arch.Arm64, SdkOS = OS.Alpine324, - ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V8_0, OS = OS.Alpine324, Arch = Arch.Arm64, SdkOS = OS.Alpine324, - ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V8_0, OS = OS.Alpine324, Arch = Arch.Arm64, SdkOS = OS.Alpine324, - ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, - - new ProductImageData { Version = V8_0, OS = OS.BookwormSlim, Arch = Arch.Arm }, - new ProductImageData { Version = V8_0, OS = OS.Jammy, Arch = Arch.Arm }, - new ProductImageData { Version = V8_0, OS = OS.JammyChiseled, Arch = Arch.Arm, SdkOS = OS.Jammy }, - new ProductImageData { Version = V8_0, OS = OS.JammyChiseled, Arch = Arch.Arm, SdkOS = OS.Jammy, - ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V8_0, OS = OS.JammyChiseled, Arch = Arch.Arm, SdkOS = OS.Jammy, - ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V8_0, OS = OS.JammyChiseled, Arch = Arch.Arm, SdkOS = OS.Jammy, - ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V8_0, OS = OS.Alpine324, Arch = Arch.Arm }, - new ProductImageData { Version = V8_0, OS = OS.Alpine324, Arch = Arch.Arm, SdkOS = OS.Alpine324, - ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V8_0, OS = OS.Alpine324, Arch = Arch.Arm, SdkOS = OS.Alpine324, - ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V8_0, OS = OS.Alpine324, Arch = Arch.Arm, SdkOS = OS.Alpine324, - ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, - - new ProductImageData { Version = V9_0, OS = OS.BookwormSlim, Arch = Arch.Amd64 }, - new ProductImageData { Version = V9_0, OS = OS.Noble, Arch = Arch.Amd64 }, - new ProductImageData { Version = V9_0, OS = OS.NobleChiseled, Arch = Arch.Amd64, SdkOS = OS.Noble }, - new ProductImageData { Version = V9_0, OS = OS.NobleChiseled, Arch = Arch.Amd64, SdkOS = OS.Noble }, - new ProductImageData { Version = V9_0, OS = OS.NobleChiseled, Arch = Arch.Amd64, SdkOS = OS.Noble, - ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V9_0, OS = OS.NobleChiseled, Arch = Arch.Amd64, SdkOS = OS.Noble, - ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V9_0, OS = OS.NobleChiseled, Arch = Arch.Amd64, SdkOS = OS.Noble, - ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V9_0, OS = OS.Alpine324, Arch = Arch.Amd64 }, - new ProductImageData { Version = V9_0, OS = OS.Alpine324, Arch = Arch.Amd64, SdkOS = OS.Alpine324, - ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V9_0, OS = OS.Alpine324, Arch = Arch.Amd64, SdkOS = OS.Alpine324, - ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V9_0, OS = OS.Alpine324, Arch = Arch.Amd64, SdkOS = OS.Alpine324, - ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V9_0, OS = OS.AzureLinux30, Arch = Arch.Amd64 }, - new ProductImageData { Version = V9_0, OS = OS.AzureLinux30Distroless, Arch = Arch.Amd64, SdkOS = OS.AzureLinux30 }, - new ProductImageData { Version = V9_0, OS = OS.AzureLinux30Distroless, Arch = Arch.Amd64, SdkOS = OS.AzureLinux30, - ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V9_0, OS = OS.AzureLinux30Distroless, Arch = Arch.Amd64, SdkOS = OS.AzureLinux30, - ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V9_0, OS = OS.AzureLinux30Distroless, Arch = Arch.Amd64, SdkOS = OS.AzureLinux30, - ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, - - new ProductImageData { Version = V9_0, OS = OS.AzureLinux30, Arch = Arch.Arm64 }, - new ProductImageData { Version = V9_0, OS = OS.AzureLinux30Distroless, Arch = Arch.Arm64, SdkOS = OS.AzureLinux30 }, - new ProductImageData { Version = V9_0, OS = OS.AzureLinux30Distroless, Arch = Arch.Arm64, SdkOS = OS.AzureLinux30, - ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V9_0, OS = OS.AzureLinux30Distroless, Arch = Arch.Arm64, SdkOS = OS.AzureLinux30, - ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V9_0, OS = OS.AzureLinux30Distroless, Arch = Arch.Arm64, SdkOS = OS.AzureLinux30, - ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V9_0, OS = OS.BookwormSlim, Arch = Arch.Arm64 }, - new ProductImageData { Version = V9_0, OS = OS.Noble, Arch = Arch.Arm64 }, - new ProductImageData { Version = V9_0, OS = OS.NobleChiseled, Arch = Arch.Arm64, SdkOS = OS.Noble }, - new ProductImageData { Version = V9_0, OS = OS.NobleChiseled, Arch = Arch.Arm64, SdkOS = OS.Noble, - ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V9_0, OS = OS.NobleChiseled, Arch = Arch.Arm64, SdkOS = OS.Noble, - ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V9_0, OS = OS.NobleChiseled, Arch = Arch.Arm64, SdkOS = OS.Noble, - ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V9_0, OS = OS.Alpine324, Arch = Arch.Arm64 }, - new ProductImageData { Version = V9_0, OS = OS.Alpine324, Arch = Arch.Arm64, SdkOS = OS.Alpine324, - ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V9_0, OS = OS.Alpine324, Arch = Arch.Arm64, SdkOS = OS.Alpine324, - ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V9_0, OS = OS.Alpine324, Arch = Arch.Arm64, SdkOS = OS.Alpine324, - ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, - - new ProductImageData { Version = V9_0, OS = OS.BookwormSlim, Arch = Arch.Arm }, - new ProductImageData { Version = V9_0, OS = OS.Alpine324, Arch = Arch.Arm }, - new ProductImageData { Version = V9_0, OS = OS.Alpine324, Arch = Arch.Arm, SdkOS = OS.Alpine324, - ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V9_0, OS = OS.Alpine324, Arch = Arch.Arm, SdkOS = OS.Alpine324, - ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V9_0, OS = OS.Alpine324, Arch = Arch.Arm, SdkOS = OS.Alpine324, - ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V9_0, OS = OS.Noble, Arch = Arch.Arm }, - new ProductImageData { Version = V9_0, OS = OS.NobleChiseled, Arch = Arch.Arm, SdkOS = OS.Noble }, - new ProductImageData { Version = V9_0, OS = OS.NobleChiseled, Arch = Arch.Arm, SdkOS = OS.Noble, - ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V9_0, OS = OS.NobleChiseled, Arch = Arch.Arm, SdkOS = OS.Noble, - ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V9_0, OS = OS.NobleChiseled, Arch = Arch.Arm, SdkOS = OS.Noble, - ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, - - new ProductImageData { Version = V10_0, OS = OS.Noble, Arch = Arch.Amd64 }, - new ProductImageData { Version = V10_0, OS = OS.NobleChiseled, Arch = Arch.Amd64, SdkOS = OS.Noble }, - new ProductImageData { Version = V10_0, OS = OS.NobleChiseled, Arch = Arch.Amd64, SdkOS = OS.Noble, - ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V10_0, OS = OS.NobleChiseled, Arch = Arch.Amd64, SdkOS = OS.Noble, - ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V10_0, OS = OS.NobleChiseled, Arch = Arch.Amd64, SdkOS = OS.Noble, - ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V10_0, OS = OS.Alpine324, Arch = Arch.Amd64 }, - new ProductImageData { Version = V10_0, OS = OS.Alpine324, Arch = Arch.Amd64, SdkOS = OS.Alpine324, - ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V10_0, OS = OS.Alpine324, Arch = Arch.Amd64, SdkOS = OS.Alpine324, - ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V10_0, OS = OS.Alpine324, Arch = Arch.Amd64, SdkOS = OS.Alpine324, - ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V10_0, OS = OS.AzureLinux30, Arch = Arch.Amd64 }, - new ProductImageData { Version = V10_0, OS = OS.AzureLinux40, Arch = Arch.Amd64 }, - new ProductImageData { Version = V10_0, OS = OS.AzureLinux30Distroless, Arch = Arch.Amd64, SdkOS = OS.AzureLinux30 }, - new ProductImageData { Version = V10_0, OS = OS.AzureLinux40Distroless, Arch = Arch.Amd64, SdkOS = OS.AzureLinux40 }, - new ProductImageData { Version = V10_0, OS = OS.AzureLinux30Distroless, Arch = Arch.Amd64, SdkOS = OS.AzureLinux30, - ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V10_0, OS = OS.AzureLinux40Distroless, Arch = Arch.Amd64, SdkOS = OS.AzureLinux40, - ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V10_0, OS = OS.AzureLinux30Distroless, Arch = Arch.Amd64, SdkOS = OS.AzureLinux30, - ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V10_0, OS = OS.AzureLinux40Distroless, Arch = Arch.Amd64, SdkOS = OS.AzureLinux40, - ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V10_0, OS = OS.AzureLinux30Distroless, Arch = Arch.Amd64, SdkOS = OS.AzureLinux30, - ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V10_0, OS = OS.AzureLinux40Distroless, Arch = Arch.Amd64, SdkOS = OS.AzureLinux40, - ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, - - new() { Version = V10_0, Arch = Arch.Amd64, SdkImageVariant = DotNetImageVariant.AOT, SupportedImageRepos = DotNetImageRepo.Runtime_Deps, OS = OS.Alpine324 }, - new() { Version = V10_0, Arch = Arch.Amd64, SdkImageVariant = DotNetImageVariant.AOT, SupportedImageRepos = DotNetImageRepo.Runtime_Deps, OS = OS.AzureLinux30Distroless, SdkOS = OS.AzureLinux30 }, - new() { Version = V10_0, Arch = Arch.Amd64, SdkImageVariant = DotNetImageVariant.AOT, SupportedImageRepos = DotNetImageRepo.Runtime_Deps, OS = OS.AzureLinux40Distroless, SdkOS = OS.AzureLinux40 }, - new() { Version = V10_0, Arch = Arch.Amd64, SdkImageVariant = DotNetImageVariant.AOT, SupportedImageRepos = DotNetImageRepo.Runtime_Deps, OS = OS.NobleChiseled, SdkOS = OS.Noble }, - new ProductImageData { Version = V10_0, OS = OS.Resolute, Arch = Arch.Amd64 }, - new ProductImageData { Version = V10_0, OS = OS.ResoluteChiseled, Arch = Arch.Amd64, SdkOS = OS.Resolute }, - new ProductImageData { Version = V10_0, OS = OS.ResoluteChiseled, Arch = Arch.Amd64, SdkOS = OS.Resolute, - ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V10_0, OS = OS.ResoluteChiseled, Arch = Arch.Amd64, SdkOS = OS.Resolute, - ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V10_0, OS = OS.ResoluteChiseled, Arch = Arch.Amd64, SdkOS = OS.Resolute, - ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, - new() { Version = V10_0, Arch = Arch.Amd64, SdkImageVariant = DotNetImageVariant.AOT, SupportedImageRepos = DotNetImageRepo.Runtime_Deps, OS = OS.ResoluteChiseled, SdkOS = OS.Resolute }, - - new ProductImageData { Version = V10_0, OS = OS.AzureLinux30, Arch = Arch.Arm64 }, - new ProductImageData { Version = V10_0, OS = OS.AzureLinux40, Arch = Arch.Arm64 }, - new ProductImageData { Version = V10_0, OS = OS.AzureLinux30Distroless, Arch = Arch.Arm64, SdkOS = OS.AzureLinux30 }, - new ProductImageData { Version = V10_0, OS = OS.AzureLinux40Distroless, Arch = Arch.Arm64, SdkOS = OS.AzureLinux40 }, - new ProductImageData { Version = V10_0, OS = OS.AzureLinux30Distroless, Arch = Arch.Arm64, SdkOS = OS.AzureLinux30, - ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V10_0, OS = OS.AzureLinux40Distroless, Arch = Arch.Arm64, SdkOS = OS.AzureLinux40, - ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V10_0, OS = OS.AzureLinux30Distroless, Arch = Arch.Arm64, SdkOS = OS.AzureLinux30, - ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V10_0, OS = OS.AzureLinux40Distroless, Arch = Arch.Arm64, SdkOS = OS.AzureLinux40, - ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V10_0, OS = OS.AzureLinux30Distroless, Arch = Arch.Arm64, SdkOS = OS.AzureLinux30, - ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V10_0, OS = OS.AzureLinux40Distroless, Arch = Arch.Arm64, SdkOS = OS.AzureLinux40, - ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V10_0, OS = OS.Noble, Arch = Arch.Arm64 }, - new ProductImageData { Version = V10_0, OS = OS.NobleChiseled, Arch = Arch.Arm64, SdkOS = OS.Noble }, - new ProductImageData { Version = V10_0, OS = OS.NobleChiseled, Arch = Arch.Arm64, SdkOS = OS.Noble, - ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V10_0, OS = OS.NobleChiseled, Arch = Arch.Arm64, SdkOS = OS.Noble, - ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V10_0, OS = OS.NobleChiseled, Arch = Arch.Arm64, SdkOS = OS.Noble, - ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V10_0, OS = OS.Alpine324, Arch = Arch.Arm64 }, - new ProductImageData { Version = V10_0, OS = OS.Alpine324, Arch = Arch.Arm64, SdkOS = OS.Alpine324, - ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V10_0, OS = OS.Alpine324, Arch = Arch.Arm64, SdkOS = OS.Alpine324, - ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V10_0, OS = OS.Alpine324, Arch = Arch.Arm64, SdkOS = OS.Alpine324, - ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, - - new() { Version = V10_0, Arch = Arch.Arm64, SdkImageVariant = DotNetImageVariant.AOT, SupportedImageRepos = DotNetImageRepo.Runtime_Deps, OS = OS.Alpine324 }, - new() { Version = V10_0, Arch = Arch.Arm64, SdkImageVariant = DotNetImageVariant.AOT, SupportedImageRepos = DotNetImageRepo.Runtime_Deps, OS = OS.AzureLinux30Distroless, SdkOS = OS.AzureLinux30 }, - new() { Version = V10_0, Arch = Arch.Arm64, SdkImageVariant = DotNetImageVariant.AOT, SupportedImageRepos = DotNetImageRepo.Runtime_Deps, OS = OS.AzureLinux40Distroless, SdkOS = OS.AzureLinux40 }, - new() { Version = V10_0, Arch = Arch.Arm64, SdkImageVariant = DotNetImageVariant.AOT, SupportedImageRepos = DotNetImageRepo.Runtime_Deps, OS = OS.NobleChiseled, SdkOS = OS.Noble }, - new ProductImageData { Version = V10_0, OS = OS.Resolute, Arch = Arch.Arm64 }, - new ProductImageData { Version = V10_0, OS = OS.ResoluteChiseled, Arch = Arch.Arm64, SdkOS = OS.Resolute }, - new ProductImageData { Version = V10_0, OS = OS.ResoluteChiseled, Arch = Arch.Arm64, SdkOS = OS.Resolute, - ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V10_0, OS = OS.ResoluteChiseled, Arch = Arch.Arm64, SdkOS = OS.Resolute, - ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V10_0, OS = OS.ResoluteChiseled, Arch = Arch.Arm64, SdkOS = OS.Resolute, - ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, - new() { Version = V10_0, Arch = Arch.Arm64, SdkImageVariant = DotNetImageVariant.AOT, SupportedImageRepos = DotNetImageRepo.Runtime_Deps, OS = OS.ResoluteChiseled, SdkOS = OS.Resolute }, - - new ProductImageData { Version = V10_0, OS = OS.Alpine324, Arch = Arch.Arm }, - new ProductImageData { Version = V10_0, OS = OS.Alpine324, Arch = Arch.Arm, SdkOS = OS.Alpine324, - ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V10_0, OS = OS.Alpine324, Arch = Arch.Arm, SdkOS = OS.Alpine324, - ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V10_0, OS = OS.Alpine324, Arch = Arch.Arm, SdkOS = OS.Alpine324, - ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V10_0, OS = OS.Noble, Arch = Arch.Arm }, - new ProductImageData { Version = V10_0, OS = OS.NobleChiseled, Arch = Arch.Arm, SdkOS = OS.Noble }, - new ProductImageData { Version = V10_0, OS = OS.NobleChiseled, Arch = Arch.Arm, SdkOS = OS.Noble, - ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V10_0, OS = OS.NobleChiseled, Arch = Arch.Arm, SdkOS = OS.Noble, - ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V10_0, OS = OS.NobleChiseled, Arch = Arch.Arm, SdkOS = OS.Noble, - ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V10_0, OS = OS.Resolute, Arch = Arch.Arm }, - new ProductImageData { Version = V10_0, OS = OS.ResoluteChiseled, Arch = Arch.Arm, SdkOS = OS.Resolute }, - new ProductImageData { Version = V10_0, OS = OS.ResoluteChiseled, Arch = Arch.Arm, SdkOS = OS.Resolute, - ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V10_0, OS = OS.ResoluteChiseled, Arch = Arch.Arm, SdkOS = OS.Resolute, - ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V10_0, OS = OS.ResoluteChiseled, Arch = Arch.Arm, SdkOS = OS.Resolute, - ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, - - // .NET 11 Linux Amd64 - new ProductImageData { Version = V11_0, OS = OS.Resolute, Arch = Arch.Amd64 }, - new ProductImageData { Version = V11_0, OS = OS.ResoluteChiseled, Arch = Arch.Amd64, SdkOS = OS.Resolute }, - new ProductImageData { Version = V11_0, OS = OS.ResoluteChiseled, Arch = Arch.Amd64, SdkOS = OS.Resolute, - ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V11_0, OS = OS.ResoluteChiseled, Arch = Arch.Amd64, SdkOS = OS.Resolute, - ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V11_0, OS = OS.ResoluteChiseled, Arch = Arch.Amd64, SdkOS = OS.Resolute, - ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V11_0, OS = OS.Alpine324, Arch = Arch.Amd64 }, - new ProductImageData { Version = V11_0, OS = OS.Alpine324, Arch = Arch.Amd64, SdkOS = OS.Alpine324, - ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V11_0, OS = OS.Alpine324, Arch = Arch.Amd64, SdkOS = OS.Alpine324, - ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V11_0, OS = OS.Alpine324, Arch = Arch.Amd64, SdkOS = OS.Alpine324, - ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V11_0, OS = OS.AzureLinux30, Arch = Arch.Amd64 }, - new ProductImageData { Version = V11_0, OS = OS.AzureLinux40, Arch = Arch.Amd64 }, - new ProductImageData { Version = V11_0, OS = OS.AzureLinux30Distroless, Arch = Arch.Amd64, SdkOS = OS.AzureLinux30 }, - new ProductImageData { Version = V11_0, OS = OS.AzureLinux40Distroless, Arch = Arch.Amd64, SdkOS = OS.AzureLinux40 }, - new ProductImageData { Version = V11_0, OS = OS.AzureLinux30Distroless, Arch = Arch.Amd64, SdkOS = OS.AzureLinux30, - ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V11_0, OS = OS.AzureLinux40Distroless, Arch = Arch.Amd64, SdkOS = OS.AzureLinux40, - ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V11_0, OS = OS.AzureLinux30Distroless, Arch = Arch.Amd64, SdkOS = OS.AzureLinux30, - ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V11_0, OS = OS.AzureLinux40Distroless, Arch = Arch.Amd64, SdkOS = OS.AzureLinux40, - ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V11_0, OS = OS.AzureLinux30Distroless, Arch = Arch.Amd64, SdkOS = OS.AzureLinux30, - ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V11_0, OS = OS.AzureLinux40Distroless, Arch = Arch.Amd64, SdkOS = OS.AzureLinux40, - ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, - - new() { Version = V11_0, Arch = Arch.Amd64, SdkImageVariant = DotNetImageVariant.AOT, SupportedImageRepos = DotNetImageRepo.Runtime_Deps, OS = OS.Alpine324 }, - new() { Version = V11_0, Arch = Arch.Amd64, SdkImageVariant = DotNetImageVariant.AOT, SupportedImageRepos = DotNetImageRepo.Runtime_Deps, OS = OS.AzureLinux30Distroless, SdkOS = OS.AzureLinux30 }, - new() { Version = V11_0, Arch = Arch.Amd64, SdkImageVariant = DotNetImageVariant.AOT, SupportedImageRepos = DotNetImageRepo.Runtime_Deps, OS = OS.AzureLinux40Distroless, SdkOS = OS.AzureLinux40 }, - new() { Version = V11_0, Arch = Arch.Amd64, SdkImageVariant = DotNetImageVariant.AOT, SupportedImageRepos = DotNetImageRepo.Runtime_Deps, OS = OS.ResoluteChiseled, SdkOS = OS.Resolute }, - - // .NET 11 Linux Arm64 - new ProductImageData { Version = V11_0, OS = OS.AzureLinux30, Arch = Arch.Arm64 }, - new ProductImageData { Version = V11_0, OS = OS.AzureLinux40, Arch = Arch.Arm64 }, - new ProductImageData { Version = V11_0, OS = OS.AzureLinux30Distroless, Arch = Arch.Arm64, SdkOS = OS.AzureLinux30 }, - new ProductImageData { Version = V11_0, OS = OS.AzureLinux40Distroless, Arch = Arch.Arm64, SdkOS = OS.AzureLinux40 }, - new ProductImageData { Version = V11_0, OS = OS.AzureLinux30Distroless, Arch = Arch.Arm64, SdkOS = OS.AzureLinux30, - ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V11_0, OS = OS.AzureLinux40Distroless, Arch = Arch.Arm64, SdkOS = OS.AzureLinux40, - ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V11_0, OS = OS.AzureLinux30Distroless, Arch = Arch.Arm64, SdkOS = OS.AzureLinux30, - ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V11_0, OS = OS.AzureLinux40Distroless, Arch = Arch.Arm64, SdkOS = OS.AzureLinux40, - ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V11_0, OS = OS.AzureLinux30Distroless, Arch = Arch.Arm64, SdkOS = OS.AzureLinux30, - ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V11_0, OS = OS.AzureLinux40Distroless, Arch = Arch.Arm64, SdkOS = OS.AzureLinux40, - ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V11_0, OS = OS.Resolute, Arch = Arch.Arm64 }, - new ProductImageData { Version = V11_0, OS = OS.ResoluteChiseled, Arch = Arch.Arm64, SdkOS = OS.Resolute }, - new ProductImageData { Version = V11_0, OS = OS.ResoluteChiseled, Arch = Arch.Arm64, SdkOS = OS.Resolute, - ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V11_0, OS = OS.ResoluteChiseled, Arch = Arch.Arm64, SdkOS = OS.Resolute, - ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V11_0, OS = OS.ResoluteChiseled, Arch = Arch.Arm64, SdkOS = OS.Resolute, - ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V11_0, OS = OS.Alpine324, Arch = Arch.Arm64 }, - new ProductImageData { Version = V11_0, OS = OS.Alpine324, Arch = Arch.Arm64, SdkOS = OS.Alpine324, - ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V11_0, OS = OS.Alpine324, Arch = Arch.Arm64, SdkOS = OS.Alpine324, - ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V11_0, OS = OS.Alpine324, Arch = Arch.Arm64, SdkOS = OS.Alpine324, - ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, - - new() { Version = V11_0, Arch = Arch.Arm64, SdkImageVariant = DotNetImageVariant.AOT, SupportedImageRepos = DotNetImageRepo.Runtime_Deps, OS = OS.Alpine324 }, - new() { Version = V11_0, Arch = Arch.Arm64, SdkImageVariant = DotNetImageVariant.AOT, SupportedImageRepos = DotNetImageRepo.Runtime_Deps, OS = OS.AzureLinux30Distroless, SdkOS = OS.AzureLinux30 }, - new() { Version = V11_0, Arch = Arch.Arm64, SdkImageVariant = DotNetImageVariant.AOT, SupportedImageRepos = DotNetImageRepo.Runtime_Deps, OS = OS.AzureLinux40Distroless, SdkOS = OS.AzureLinux40 }, - new() { Version = V11_0, Arch = Arch.Arm64, SdkImageVariant = DotNetImageVariant.AOT, SupportedImageRepos = DotNetImageRepo.Runtime_Deps, OS = OS.ResoluteChiseled, SdkOS = OS.Resolute }, - - // .NET 11 Linux Arm32 - new ProductImageData { Version = V11_0, OS = OS.Alpine324, Arch = Arch.Arm }, - new ProductImageData { Version = V11_0, OS = OS.Alpine324, Arch = Arch.Arm, SdkOS = OS.Alpine324, - ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V11_0, OS = OS.Alpine324, Arch = Arch.Arm, SdkOS = OS.Alpine324, - ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V11_0, OS = OS.Alpine324, Arch = Arch.Arm, SdkOS = OS.Alpine324, - ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V11_0, OS = OS.Resolute, Arch = Arch.Arm }, - new ProductImageData { Version = V11_0, OS = OS.ResoluteChiseled, Arch = Arch.Arm, SdkOS = OS.Resolute }, - new ProductImageData { Version = V11_0, OS = OS.ResoluteChiseled, Arch = Arch.Arm, SdkOS = OS.Resolute, - ImageVariant = DotNetImageVariant.Composite, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V11_0, OS = OS.ResoluteChiseled, Arch = Arch.Arm, SdkOS = OS.Resolute, - ImageVariant = DotNetImageVariant.Composite | DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Aspnet }, - new ProductImageData { Version = V11_0, OS = OS.ResoluteChiseled, Arch = Arch.Arm, SdkOS = OS.Resolute, - ImageVariant = DotNetImageVariant.Extra, SupportedImageRepos = DotNetImageRepo.Runtime_Deps | DotNetImageRepo.Runtime | DotNetImageRepo.Aspnet }, - }; - - private static readonly ProductImageData[] s_windowsTestData = - { - new ProductImageData { Version = V8_0, OS = OS.NanoServer1809, Arch = Arch.Amd64 }, - new ProductImageData { Version = V8_0, OS = OS.NanoServerLtsc2022, Arch = Arch.Amd64 }, - new ProductImageData { Version = V8_0, OS = OS.NanoServerLtsc2025, Arch = Arch.Amd64 }, - new ProductImageData { Version = V8_0, OS = OS.ServerCoreLtsc2019, Arch = Arch.Amd64 }, - new ProductImageData { Version = V8_0, OS = OS.ServerCoreLtsc2022, Arch = Arch.Amd64 }, - new ProductImageData { Version = V8_0, OS = OS.ServerCoreLtsc2025, Arch = Arch.Amd64 }, - - new ProductImageData { Version = V9_0, OS = OS.NanoServer1809, Arch = Arch.Amd64 }, - new ProductImageData { Version = V9_0, OS = OS.NanoServerLtsc2022, Arch = Arch.Amd64 }, - new ProductImageData { Version = V9_0, OS = OS.NanoServerLtsc2025, Arch = Arch.Amd64 }, - new ProductImageData { Version = V9_0, OS = OS.ServerCoreLtsc2019, Arch = Arch.Amd64 }, - new ProductImageData { Version = V9_0, OS = OS.ServerCoreLtsc2022, Arch = Arch.Amd64 }, - new ProductImageData { Version = V9_0, OS = OS.ServerCoreLtsc2025, Arch = Arch.Amd64 }, - - new ProductImageData { Version = V10_0, OS = OS.NanoServerLtsc2022, Arch = Arch.Amd64 }, - new ProductImageData { Version = V10_0, OS = OS.NanoServerLtsc2025, Arch = Arch.Amd64 }, - new ProductImageData { Version = V10_0, OS = OS.ServerCoreLtsc2022, Arch = Arch.Amd64 }, - new ProductImageData { Version = V10_0, OS = OS.ServerCoreLtsc2025, Arch = Arch.Amd64 }, - - new ProductImageData { Version = V11_0, OS = OS.NanoServerLtsc2025, Arch = Arch.Amd64 }, - new ProductImageData { Version = V11_0, OS = OS.ServerCoreLtsc2025, Arch = Arch.Amd64 }, - }; - - private static readonly SampleImageData[] s_linuxSampleTestData = - { - new SampleImageData { OS = OS.AlpineFloating, Arch = Arch.Amd64, DockerfileSuffix = "alpine", IsPublished = true }, - new SampleImageData { OS = OS.AlpineFloating, Arch = Arch.Arm, DockerfileSuffix = "alpine", IsPublished = true }, - new SampleImageData { OS = OS.AlpineFloating, Arch = Arch.Arm64, DockerfileSuffix = "alpine", IsPublished = true }, - new SampleImageData { OS = OS.ResoluteChiseled, Arch = Arch.Arm, DockerfileSuffix = "chiseled", IsPublished = true }, - new SampleImageData { OS = OS.ResoluteChiseled, Arch = Arch.Arm64, DockerfileSuffix = "chiseled", IsPublished = true }, - new SampleImageData { OS = OS.ResoluteChiseled, Arch = Arch.Amd64, DockerfileSuffix = "chiseled", IsPublished = true }, - - new SampleImageData { OS = OS.Resolute, Arch = Arch.Amd64 }, - new SampleImageData { OS = OS.Resolute, Arch = Arch.Arm }, - new SampleImageData { OS = OS.Resolute, Arch = Arch.Arm64 }, - new SampleImageData { OS = OS.AlpineFloating, Arch = Arch.Arm64, DockerfileSuffix = "alpine" }, - new SampleImageData { OS = OS.AlpineFloating, Arch = Arch.Amd64, DockerfileSuffix = "alpine" }, - new SampleImageData { OS = OS.AlpineFloating, Arch = Arch.Arm64, DockerfileSuffix = "alpine-icu" }, - new SampleImageData { OS = OS.AlpineFloating, Arch = Arch.Amd64, DockerfileSuffix = "alpine-icu" }, - new SampleImageData { OS = OS.Resolute, Arch = Arch.Arm, DockerfileSuffix = "ubuntu" }, - new SampleImageData { OS = OS.Resolute, Arch = Arch.Arm64, DockerfileSuffix = "ubuntu" }, - new SampleImageData { OS = OS.Resolute, Arch = Arch.Amd64, DockerfileSuffix = "ubuntu" }, - new SampleImageData { OS = OS.ResoluteChiseled, Arch = Arch.Arm, DockerfileSuffix = "chiseled" }, - new SampleImageData { OS = OS.ResoluteChiseled, Arch = Arch.Arm64, DockerfileSuffix = "chiseled" }, - new SampleImageData { OS = OS.ResoluteChiseled, Arch = Arch.Amd64, DockerfileSuffix = "chiseled" }, - }; - - private static readonly SampleImageData[] s_windowsSampleTestData = - { - new SampleImageData { OS = OS.NanoServerLtsc2025, Arch = Arch.Amd64, IsPublished = true }, - - new SampleImageData { OS = OS.NanoServerLtsc2025, Arch = Arch.Amd64, DockerfileSuffix = "nanoserver" }, - - // Use Nano Server as the OS even though the Dockerfiles are for Windows Server Core. This is because the OS value - // needs to match the filter set by the build/test job. We only produce builds jobs based on what's in the manifest - // and the manifest only defines Nano Server-based Dockerfiles. So we just need to piggyback on the Nano Server - // jobs in order to test the Windows Server Core samples. - new SampleImageData { OS = OS.NanoServerLtsc2025, Arch = Arch.Amd64, DockerfileSuffix = "windowsservercore" }, - new SampleImageData { OS = OS.NanoServerLtsc2025, Arch = Arch.Amd64, DockerfileSuffix = "windowsservercore-iis" }, - }; - - private static readonly ProductImageData[] s_linuxMonitorTestData = - [ - new ProductImageData { Version = V8_1, VersionFamily = V8_0, OS = OS.JammyChiseled, OSTag = OS.UbuntuChiseled, Arch = Arch.Amd64, SupportedImageRepos = DotNetImageRepo.Monitor | DotNetImageRepo.Monitor_Base }, - new ProductImageData { Version = V8_1, VersionFamily = V8_0, OS = OS.JammyChiseled, OSTag = OS.UbuntuChiseled, Arch = Arch.Arm64, SupportedImageRepos = DotNetImageRepo.Monitor | DotNetImageRepo.Monitor_Base }, - // OSTag does not correspond to OS because platform tags for Azure Linux were not added to the images - // Use CBL-Mariner distroless for OSTag since those platform tags exist and won't require tests to understand the difference in tagging. - new ProductImageData { Version = V8_1, VersionFamily = V8_0, OS = OS.AzureLinux30Distroless, OSDir = OS.AzureLinuxDistroless, OSTag = OS.MarinerDistroless, Arch = Arch.Amd64, SupportedImageRepos = DotNetImageRepo.Monitor | DotNetImageRepo.Monitor_Base }, - new ProductImageData { Version = V8_1, VersionFamily = V8_0, OS = OS.AzureLinux30Distroless, OSDir = OS.AzureLinuxDistroless, OSTag = OS.MarinerDistroless, Arch = Arch.Arm64, SupportedImageRepos = DotNetImageRepo.Monitor | DotNetImageRepo.Monitor_Base }, - new ProductImageData { Version = V9_0, VersionFamily = V9_0, OS = OS.AzureLinux30Distroless, OSDir = OS.AzureLinuxDistroless, OSTag = "", Arch = Arch.Amd64, SupportedImageRepos = DotNetImageRepo.Monitor | DotNetImageRepo.Monitor_Base }, - new ProductImageData { Version = V9_0, VersionFamily = V9_0, OS = OS.AzureLinux30Distroless, OSDir = OS.AzureLinuxDistroless, OSTag = "", Arch = Arch.Arm64, SupportedImageRepos = DotNetImageRepo.Monitor | DotNetImageRepo.Monitor_Base }, - new ProductImageData { Version = V10_0, VersionFamily = V10_0, OS = OS.AzureLinux30Distroless, OSDir = OS.AzureLinuxDistroless, OSTag = "", Arch = Arch.Amd64, SupportedImageRepos = DotNetImageRepo.Monitor | DotNetImageRepo.Monitor_Base }, - new ProductImageData { Version = V10_0, VersionFamily = V10_0, OS = OS.AzureLinux30Distroless, OSDir = OS.AzureLinuxDistroless, OSTag = "", Arch = Arch.Arm64, SupportedImageRepos = DotNetImageRepo.Monitor | DotNetImageRepo.Monitor_Base }, - ]; - - private static readonly ProductImageData[] s_windowsMonitorTestData = - { - }; + return (DockerHelper.IsLinuxContainerModeEnabled ? LinuxTestData : WindowsTestData) + .FilterImagesBySupportedRepo(imageRepo) + .FilterImagesByVariant(variant) + .FilterImagesByPath(imageRepo) + .FilterImagesByArch() + .FilterImagesByOs() + .Cast(); + } - private static readonly ProductImageData[] s_AspireDashboardTestData = - { - new() { - Version = V17_0, - VersionFamily = V11_0, - OS = OS.AzureLinux30Distroless, - OSTag = "", - OSDir = OS.AzureLinuxDistroless, - Arch = Arch.Amd64, - SupportedImageRepos = DotNetImageRepo.Aspire_Dashboard, - }, - new() { - Version = V17_0, - VersionFamily = V11_0, - OS = OS.AzureLinux30Distroless, - OSTag = "", - OSDir = OS.AzureLinuxDistroless, - Arch = Arch.Arm64, - SupportedImageRepos = DotNetImageRepo.Aspire_Dashboard - }, - }; - - private static readonly ProductImageData[] s_YarpTestData = - [ - new() { - Version = new ImageVersion(new Version(2,3), isPreview: true), - VersionFamily = V9_0, - OS = OS.AzureLinux30Distroless, - OSTag = "", - OSDir = OS.AzureLinuxDistroless, - Arch = Arch.Amd64, - SupportedImageRepos = DotNetImageRepo.Yarp, - }, - new() { - Version = new ImageVersion(new Version(2,3), isPreview: true), - VersionFamily = V9_0, - OS = OS.AzureLinux30Distroless, - OSTag = "", - OSDir = OS.AzureLinuxDistroless, - Arch = Arch.Arm64, - SupportedImageRepos = DotNetImageRepo.Yarp - }, - ]; - - public static IEnumerable AllImageData => - [ - ..s_linuxTestData, - ..s_windowsTestData, - ..s_AspireDashboardTestData, - ..s_linuxMonitorTestData, - ..s_windowsMonitorTestData, - ..s_YarpTestData, - ]; - - public static IEnumerable GetImageData( - DotNetImageRepo imageRepo, - DotNetImageVariant variant = DotNetImageVariant.None) - { - return (DockerHelper.IsLinuxContainerModeEnabled ? s_linuxTestData : s_windowsTestData) - .FilterImagesBySupportedRepo(imageRepo) - .FilterImagesByVariant(variant) - .FilterImagesByPath(imageRepo) - .FilterImagesByArch() - .FilterImagesByOs() - .Cast(); - } + public static IEnumerable GetAllSampleImageData() => + DockerHelper.IsLinuxContainerModeEnabled ? LinuxSampleTestData : WindowsSampleTestData; - public static IEnumerable GetAllSampleImageData() => - DockerHelper.IsLinuxContainerModeEnabled ? s_linuxSampleTestData : s_windowsSampleTestData; + public static IEnumerable GetSampleImageData() + { + return GetAllSampleImageData() + .FilterImagesByArch() + .FilterImagesByOs() + .Cast(); + } - public static IEnumerable GetSampleImageData() + public static IEnumerable GetAspireDashboardImageData() + { + if (!DockerHelper.IsLinuxContainerModeEnabled) { - return GetAllSampleImageData() - .FilterImagesByArch() - .FilterImagesByOs() - .Cast(); + return []; } - public static IEnumerable GetAspireDashboardImageData() - { - if (!DockerHelper.IsLinuxContainerModeEnabled) - { - return []; - } - - return s_AspireDashboardTestData - .FilterImagesByPath(DotNetImageRepo.Aspire_Dashboard) - .FilterImagesByArch() - .FilterImagesByOs() - .Cast(); - } + return AspireDashboardTestData + .FilterImagesByPath(DotNetImageRepo.Aspire_Dashboard) + .FilterImagesByArch() + .FilterImagesByOs() + .Cast(); + } - public static IEnumerable GetMonitorImageData() - { - return (DockerHelper.IsLinuxContainerModeEnabled ? s_linuxMonitorTestData : s_windowsMonitorTestData) - .FilterImagesByPath(DotNetImageRepo.Monitor) - .FilterImagesByArch() - .FilterImagesByOs() - .Cast(); - } + public static IEnumerable GetMonitorImageData() + { + return (DockerHelper.IsLinuxContainerModeEnabled ? LinuxMonitorTestData : WindowsMonitorTestData) + .FilterImagesByPath(DotNetImageRepo.Monitor) + .FilterImagesByArch() + .FilterImagesByOs() + .Cast(); + } - public static IEnumerable GetYarpImageData() + public static IEnumerable GetYarpImageData() + { + if (!DockerHelper.IsLinuxContainerModeEnabled) { - if (!DockerHelper.IsLinuxContainerModeEnabled) - { - return []; - } - - return s_YarpTestData - .FilterImagesByPath(DotNetImageRepo.Yarp) - .FilterImagesByArch() - .FilterImagesByOs() - .Cast(); + return []; } - public static IEnumerable FilterImagesByArch(this IEnumerable imageData) - { - string archFilterPattern = GetFilterRegexPattern("IMAGE_ARCH"); - return imageData - .Where(imageData => archFilterPattern == null - || Regex.IsMatch(Enum.GetName(typeof(Arch), imageData.Arch), archFilterPattern, RegexOptions.IgnoreCase)); - } + return YarpTestData + .FilterImagesByPath(DotNetImageRepo.Yarp) + .FilterImagesByArch() + .FilterImagesByOs() + .Cast(); + } - public static IEnumerable FilterImagesByOs(this IEnumerable imageData) - { - IEnumerable osFilterPatterns = Config.OsNames - .Select(Config.GetFilterRegexPattern); + public static IEnumerable FilterImagesByArch(this IEnumerable imageData) + { + string archFilterPattern = GetFilterRegexPattern("IMAGE_ARCH"); + return imageData + .Where(imageData => archFilterPattern == null + || Regex.IsMatch(Enum.GetName(typeof(Arch), imageData.Arch), archFilterPattern, RegexOptions.IgnoreCase)); + } - return imageData - .Where(imageData => !osFilterPatterns.Any() - || osFilterPatterns.Any(osFilterPattern => Regex.IsMatch(imageData.OS, osFilterPattern, RegexOptions.IgnoreCase))); - } + public static IEnumerable FilterImagesByOs(this IEnumerable imageData) + { + IEnumerable osFilterPatterns = Config.OsNames + .Select(Config.GetFilterRegexPattern); - public static IEnumerable FilterImagesByPath(this IEnumerable imageData, DotNetImageRepo imageRepo) - { - IEnumerable pathPatterns = Config.Paths - .Select(Config.GetFilterRegexPattern); - - IEnumerable filteredImageData = imageData - .Where(imageData => - { - return !pathPatterns.Any() || pathPatterns.Any(pathPattern => - { - string dockerfilePath = imageData.GetDockerfilePath(imageRepo); - return Regex.IsMatch(dockerfilePath, pathPattern, RegexOptions.IgnoreCase); - }); - }); - - return filteredImageData; - } + return imageData + .Where(imageData => !osFilterPatterns.Any() + || osFilterPatterns.Any(osFilterPattern => Regex.IsMatch(imageData.OS, osFilterPattern, RegexOptions.IgnoreCase))); + } - public static IEnumerable FilterImagesBySupportedRepo( - this IEnumerable imageData, - DotNetImageRepo imageRepo) - { - IEnumerable images = imageData.Where(imageData => - imageData.ImageRepoIsSupported(imageRepo)); - return images; - } + public static IEnumerable FilterImagesByPath(this IEnumerable imageData, DotNetImageRepo imageRepo) + { + IEnumerable pathPatterns = Config.Paths + .Select(Config.GetFilterRegexPattern); - public static IEnumerable FilterImagesByVariant( - this IEnumerable imageData, - DotNetImageVariant variant) - { - IEnumerable images = imageData.Where(imageData => - imageData.ImageVariant.HasFlag(variant)); - return images; - } + IEnumerable filteredImageData = imageData + .Where(imageData => + { + return !pathPatterns.Any() || pathPatterns.Any(pathPattern => + { + string dockerfilePath = imageData.GetDockerfilePath(imageRepo); + return Regex.IsMatch(dockerfilePath, pathPattern, RegexOptions.IgnoreCase); + }); + }); + + return filteredImageData; + } - private static string GetFilterRegexPattern(string filterEnvName) - { - string filter = Environment.GetEnvironmentVariable(filterEnvName); - return Config.GetFilterRegexPattern(filter); - } + public static IEnumerable FilterImagesBySupportedRepo( + this IEnumerable imageData, + DotNetImageRepo imageRepo) + { + IEnumerable images = imageData.Where(imageData => + imageData.ImageRepoIsSupported(imageRepo)); + return images; + } + + public static IEnumerable FilterImagesByVariant( + this IEnumerable imageData, + DotNetImageVariant variant) + { + IEnumerable images = imageData.Where(imageData => + imageData.ImageVariant.HasFlag(variant)); + return images; + } + + private static string GetFilterRegexPattern(string filterEnvName) + { + string filter = Environment.GetEnvironmentVariable(filterEnvName); + return Config.GetFilterRegexPattern(filter); } } diff --git a/tests/Microsoft.DotNet.Docker.Tests/TestDataTests.cs b/tests/Microsoft.DotNet.Docker.Tests/TestDataTests.cs index d118cd6282..ea484cc0b7 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/TestDataTests.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/TestDataTests.cs @@ -1,6 +1,5 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. using System; using System.Collections.Generic; @@ -17,8 +16,6 @@ public class TestDataTests(ITestOutputHelper outputHelper) { private readonly DockerHelper _dockerHelper = new(outputHelper); - private readonly ITestOutputHelper _outputHelper = outputHelper; - private readonly Manifest _manifest = ManifestHelper.GetManifest(); public static readonly IEnumerable ImageRepos = @@ -31,7 +28,7 @@ public class TestDataTests(ITestOutputHelper outputHelper) [MemberData(nameof(ImageRepos))] public void VerifyTestData(DotNetImageRepo imageRepo) { - var testData = GetTestData(imageRepo); + IEnumerable testData = GetTestData(imageRepo); string repoName = ImageData.GetRepoName(ProductImageData.GetRepoName(imageRepo)); Repo? manifestRepo = _manifest.Repos.FirstOrDefault(r => r.Name == repoName); diff --git a/tests/Microsoft.DotNet.Docker.Tests/TestScenarios/AspireDashboardBasicScenario.cs b/tests/Microsoft.DotNet.Docker.Tests/TestScenarios/AspireDashboardBasicScenario.cs index d6842a1687..d3cc76faf5 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/TestScenarios/AspireDashboardBasicScenario.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/TestScenarios/AspireDashboardBasicScenario.cs @@ -1,9 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + #nullable enable using System.Threading.Tasks; using Xunit.Abstractions; -namespace Microsoft.DotNet.Docker.Tests; +namespace Microsoft.DotNet.Docker.Tests.TestScenarios; public class AspireDashboardBasicScenario : ITestScenario { diff --git a/tests/Microsoft.DotNet.Docker.Tests/TestScenarios/BlazorWasmScenario.cs b/tests/Microsoft.DotNet.Docker.Tests/TestScenarios/BlazorWasmScenario.cs index 091b270b11..35626c8b42 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/TestScenarios/BlazorWasmScenario.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/TestScenarios/BlazorWasmScenario.cs @@ -1,8 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + #nullable enable using Xunit.Abstractions; -namespace Microsoft.DotNet.Docker.Tests; +namespace Microsoft.DotNet.Docker.Tests.TestScenarios; public class BlazorWasmScenario( ProductImageData imageData, diff --git a/tests/Microsoft.DotNet.Docker.Tests/TestScenarios/ConsoleAppScenario.cs b/tests/Microsoft.DotNet.Docker.Tests/TestScenarios/ConsoleAppScenario.cs index c22f8ebdd2..53d501f9c3 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/TestScenarios/ConsoleAppScenario.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/TestScenarios/ConsoleAppScenario.cs @@ -1,11 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + #nullable enable using System.Threading.Tasks; using Xunit.Abstractions; -namespace Microsoft.DotNet.Docker.Tests; +namespace Microsoft.DotNet.Docker.Tests.TestScenarios; -public abstract class ConsoleAppScenario : ProjectTemplateTestScenario +public abstract class ConsoleAppScenario(ProductImageData imageData, DockerHelper dockerHelper, ITestOutputHelper outputHelper) : ProjectTemplateTestScenario(imageData, dockerHelper, outputHelper) { protected override string SampleName { get; } = "console"; @@ -13,11 +16,6 @@ public abstract class ConsoleAppScenario : ProjectTemplateTestScenario protected override DotNetImageRepo SdkImageRepo { get; } = DotNetImageRepo.SDK; - public ConsoleAppScenario(ProductImageData imageData, DockerHelper dockerHelper, ITestOutputHelper outputHelper) - : base(imageData, dockerHelper, outputHelper) - { - } - protected override Task RunAsync(string image, string? user, string? command = null) { string containerName = ImageData.GetIdentifier("app-run"); diff --git a/tests/Microsoft.DotNet.Docker.Tests/TestScenarios/GlobalizationScenario.cs b/tests/Microsoft.DotNet.Docker.Tests/TestScenarios/GlobalizationScenario.cs index 8d05de2867..66664551c4 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/TestScenarios/GlobalizationScenario.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/TestScenarios/GlobalizationScenario.cs @@ -1,6 +1,5 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. using System; using System.IO; @@ -10,7 +9,10 @@ namespace Microsoft.DotNet.Docker.Tests.TestScenarios; #nullable enable -public sealed class GlobalizationScenario : ITestScenario, IDisposable +public sealed class GlobalizationScenario( + ProductImageData imageData, + DotNetImageRepo repo, + DockerHelper dockerHelper) : ITestScenario, IDisposable { private const string Dockerfile = """ ARG sdk_image @@ -29,19 +31,9 @@ ENTRYPOINT ["/app/App"] """; private readonly TempFolderContext _tempFolderContext = FileHelper.UseTempFolder(); - private readonly ProductImageData _imageData; - private readonly DotNetImageRepo _repo; - private readonly DockerHelper _dockerHelper; - - public GlobalizationScenario( - ProductImageData imageData, - DotNetImageRepo repo, - DockerHelper dockerHelper) - { - _imageData = imageData; - _repo = repo; - _dockerHelper = dockerHelper; - } + private readonly ProductImageData _imageData = imageData; + private readonly DotNetImageRepo _repo = repo; + private readonly DockerHelper _dockerHelper = dockerHelper; public async Task ExecuteAsync() { diff --git a/tests/Microsoft.DotNet.Docker.Tests/TestScenarios/ITestScenario.cs b/tests/Microsoft.DotNet.Docker.Tests/TestScenarios/ITestScenario.cs index 38c1689415..52806e346b 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/TestScenarios/ITestScenario.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/TestScenarios/ITestScenario.cs @@ -1,12 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. #nullable enable using System.Threading.Tasks; -namespace Microsoft.DotNet.Docker.Tests; +namespace Microsoft.DotNet.Docker.Tests.TestScenarios; public interface ITestScenario { diff --git a/tests/Microsoft.DotNet.Docker.Tests/TestScenarios/NlsScenario.cs b/tests/Microsoft.DotNet.Docker.Tests/TestScenarios/NlsScenario.cs index a67f3f82a4..9d8d315ca0 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/TestScenarios/NlsScenario.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/TestScenarios/NlsScenario.cs @@ -1,6 +1,5 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. using System; using System.IO; @@ -10,7 +9,10 @@ namespace Microsoft.DotNet.Docker.Tests.TestScenarios; #nullable enable -public sealed class NlsScenario : ITestScenario, IDisposable +public sealed class NlsScenario( + ProductImageData imageData, + DotNetImageRepo repo, + DockerHelper dockerHelper) : ITestScenario, IDisposable { private const string DockerfileName = "NLSTest.Dockerfile"; @@ -18,21 +20,11 @@ public sealed class NlsScenario : ITestScenario, IDisposable private readonly TempFolderContext _tempFolderContext = FileHelper.UseTempFolder(); - private readonly ProductImageData _imageData; + private readonly ProductImageData _imageData = imageData; - private readonly DotNetImageRepo _repo; + private readonly DotNetImageRepo _repo = repo; - private readonly DockerHelper _dockerHelper; - - public NlsScenario( - ProductImageData imageData, - DotNetImageRepo repo, - DockerHelper dockerHelper) - { - _imageData = imageData; - _repo = repo; - _dockerHelper = dockerHelper; - } + private readonly DockerHelper _dockerHelper = dockerHelper; // ICU is not supported on Nano Server private bool IsIcuSupported => _imageData.OS.Family == OSFamily.WindowsServerCore; @@ -58,7 +50,7 @@ public Task ExecuteAsync() if (IsIcuSupported) { - buildArgs = [..buildArgs, $"icu_expected={IsIcuSupported}"]; + buildArgs = [.. buildArgs, $"icu_expected={IsIcuSupported}"]; } string tag = nameof(NlsScenario).ToLowerInvariant(); diff --git a/tests/Microsoft.DotNet.Docker.Tests/TestScenarios/ProjectTemplateTestScenario.cs b/tests/Microsoft.DotNet.Docker.Tests/TestScenarios/ProjectTemplateTestScenario.cs index cc271f9e71..4e840f837c 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/TestScenarios/ProjectTemplateTestScenario.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/TestScenarios/ProjectTemplateTestScenario.cs @@ -1,6 +1,5 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. #nullable enable @@ -10,17 +9,20 @@ using System.Threading.Tasks; using Xunit.Abstractions; -namespace Microsoft.DotNet.Docker.Tests; +namespace Microsoft.DotNet.Docker.Tests.TestScenarios; -public abstract class ProjectTemplateTestScenario : ITestScenario +public abstract class ProjectTemplateTestScenario( + ProductImageData imageData, + DockerHelper dockerHelper, + ITestOutputHelper outputHelper) : ITestScenario { protected static string OSDockerfileSuffix { get; } = DockerHelper.IsLinuxContainerModeEnabled ? "linux" : "windows"; protected static string? AdminUser { get; } = DockerHelper.IsLinuxContainerModeEnabled ? "root" : null; protected static string? NonRootUser { get; } = DockerHelper.IsLinuxContainerModeEnabled ? "app" : "ContainerUser"; - protected DockerHelper DockerHelper { get; } - protected ProductImageData ImageData { get; } - protected ITestOutputHelper OutputHelper { get; } + protected DockerHelper DockerHelper { get; } = dockerHelper; + protected ProductImageData ImageData { get; } = imageData; + protected ITestOutputHelper OutputHelper { get; } = outputHelper; protected virtual bool NonRootUserSupported => DockerHelper.IsLinuxContainerModeEnabled; @@ -33,20 +35,10 @@ public abstract class ProjectTemplateTestScenario : ITestScenario protected abstract DotNetImageRepo RuntimeImageRepo { get; } protected abstract DotNetImageRepo SdkImageRepo { get; } - public ProjectTemplateTestScenario( - ProductImageData imageData, - DockerHelper dockerHelper, - ITestOutputHelper outputHelper) - { - DockerHelper = dockerHelper; - ImageData = imageData; - OutputHelper = outputHelper; - } - private string Build(TestSolution testSolution, string stageTarget, string[]? customBuildArgs) { - const string DockerfileName = "Dockerfile"; - string dockerfilePath = Path.Combine(DockerHelper.TestArtifactsDir, DockerfileName); + const string dockerfileName = "Dockerfile"; + string dockerfilePath = Path.Combine(DockerHelper.TestArtifactsDir, dockerfileName); File.WriteAllText(dockerfilePath, Dockerfile.Content); string tag = ImageData.GetIdentifier(stageTarget); @@ -62,15 +54,15 @@ private string Build(TestSolution testSolution, string stageTarget, string[]? cu { // Docker needs all FROM images to be defined even if we won't use them for testing Composite images ProductImageData runtimeDepsImageData = ImageData.ImageVariant.HasFlag(DotNetImageVariant.Composite) - ? new ProductImageData() - { - Version = ImageData.Version, - OS = ImageData.OS, - Arch = ImageData.Arch, - SdkOS = ImageData.SdkOS, - ImageVariant = DotNetImageVariant.None, - SupportedImageRepos = DotNetImageRepo.Runtime_Deps, - } + ? new ProductImageData() + { + Version = ImageData.Version, + OS = ImageData.OS, + Arch = ImageData.Arch, + SdkOS = ImageData.SdkOS, + ImageVariant = DotNetImageVariant.None, + SupportedImageRepos = DotNetImageRepo.Runtime_Deps, + } : ImageData; buildArgs.Add($"runtime_deps_image={runtimeDepsImageData.GetImage(DotNetImageRepo.Runtime_Deps, DockerHelper)}"); @@ -81,12 +73,12 @@ private string Build(TestSolution testSolution, string stageTarget, string[]? cu buildArgs.AddRange(customBuildArgs); } - const string InternalAccessTokenVar = "InternalAccessToken"; + const string internalAccessTokenVar = "InternalAccessToken"; if (!string.IsNullOrEmpty(Config.InternalAccessToken)) { - buildArgs.Add(InternalAccessTokenVar); - Environment.SetEnvironmentVariable(InternalAccessTokenVar, Config.InternalAccessToken); + buildArgs.Add(internalAccessTokenVar); + Environment.SetEnvironmentVariable(internalAccessTokenVar, Config.InternalAccessToken); } try @@ -103,7 +95,7 @@ private string Build(TestSolution testSolution, string stageTarget, string[]? cu { if (!string.IsNullOrEmpty(Config.InternalAccessToken)) { - Environment.SetEnvironmentVariable(InternalAccessTokenVar, null); + Environment.SetEnvironmentVariable(internalAccessTokenVar, null); } } @@ -139,7 +131,7 @@ public async Task ExecuteAsync() // publish command with a different RID than the default would end up restoring images. This is not // what we'd want and plus it would fail in that case if it was targeting a private NuGet feed because // the password isn't necessarily provided in that stage. - string[] customBuildArgs = [ ..CustomDockerBuildArgs, $"rid={ImageData.Rid}" ]; + string[] customBuildArgs = [.. CustomDockerBuildArgs, $"rid={ImageData.Rid}"]; // Build and run app on SDK image string buildTag = Build(testSolution, TestDockerfile.BuildStageName, customBuildArgs); diff --git a/tests/Microsoft.DotNet.Docker.Tests/TestScenarios/ReverseProxyBasicScenario.cs b/tests/Microsoft.DotNet.Docker.Tests/TestScenarios/ReverseProxyBasicScenario.cs index 65d676e858..aa4c438c96 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/TestScenarios/ReverseProxyBasicScenario.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/TestScenarios/ReverseProxyBasicScenario.cs @@ -1,3 +1,6 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + #nullable enable using System; @@ -5,17 +8,14 @@ using System.IO; using System.Net; using System.Net.Http; -using System.Runtime.ExceptionServices; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading.Tasks; -using FluentAssertions.Execution; using Shouldly; using Xunit; using Xunit.Abstractions; -using Xunit.Sdk; -namespace Microsoft.DotNet.Docker.Tests; +namespace Microsoft.DotNet.Docker.Tests.TestScenarios; public class YarpBasicScenario : ITestScenario { @@ -183,9 +183,9 @@ private class TelemetryResults { public int SpanIdCount { get; set; } public int LogMessageCount { get; set; } - public List MetricNames { get; init; } = new(); - public List ResourceNames { get; init; } = new(); - public List TraceIds { get; init; } = new(); + public List MetricNames { get; init; } = []; + public List ResourceNames { get; init; } = []; + public List TraceIds { get; init; } = []; [JsonIgnore] public int MetricNameCount => MetricNames.Count; [JsonIgnore] diff --git a/tests/Microsoft.DotNet.Docker.Tests/TestScenarios/TestDockerfile.cs b/tests/Microsoft.DotNet.Docker.Tests/TestScenarios/TestDockerfile.cs index 2e9a35ac59..1866d5cccb 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/TestScenarios/TestDockerfile.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/TestScenarios/TestDockerfile.cs @@ -1,6 +1,5 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. #nullable enable @@ -9,7 +8,7 @@ using System.Linq; using System.Text; -namespace Microsoft.DotNet.Docker.Tests; +namespace Microsoft.DotNet.Docker.Tests.TestScenarios; public class TestDockerfile(IEnumerable args, IEnumerable stages) { @@ -27,7 +26,7 @@ private static string GetContent(IEnumerable args, IEnumerable s IEnumerable stageContents = stages.Select(stageContent => string.Join(Environment.NewLine, stageContent)); - string[] contents = [ argsContent, ..stageContents ]; + string[] contents = [argsContent, .. stageContents]; string dockerfileContent = string.Join(Environment.NewLine + Environment.NewLine, contents); return dockerfileContent; @@ -42,11 +41,11 @@ public static class TestDockerfileBuilder COPY NuGet.config . """; - private static DockerOS s_os = DockerHelper.IsLinuxContainerModeEnabled + private static readonly DockerOS Os = DockerHelper.IsLinuxContainerModeEnabled ? DockerOS.Linux : DockerOS.Windows; - private static string[] s_commonArgs = [ + private static readonly string[] CommonArgs = [ "sdk_image", "runtime_image", "runtime_deps_image", @@ -105,8 +104,8 @@ ENTRYPOINT ["./app"] }; return new TestDockerfile( - args: s_commonArgs, - stages: [ GetDefaultBuildStage(), ..publishAndAppStages ]); + args: CommonArgs, + stages: [GetDefaultBuildStage(), .. publishAndAppStages]); } public static TestDockerfile GetTestProjectDockerfile() @@ -124,8 +123,8 @@ COPY tests/ . """; return new TestDockerfile( - args: s_commonArgs, - stages: [ GetDefaultBuildStage(), testStage ]); + args: CommonArgs, + stages: [GetDefaultBuildStage(), testStage]); } public static TestDockerfile GetBlazorWasmDockerfile(bool useWasmTools) @@ -187,7 +186,7 @@ ARG rid // App stage can remain empty in order to test publish functionality. string appStage = $"""FROM $runtime_deps_image AS {TestDockerfile.AppStageName}"""; - return new TestDockerfile(s_commonArgs, stages: [ buildStage, publishStage, appStage ]); + return new TestDockerfile(CommonArgs, stages: [buildStage, publishStage, appStage]); } private static string GetDefaultBuildStage() @@ -210,15 +209,17 @@ RUN dotnet build --no-restore return buildStageBuilder.ToString(); } - private static string FormatArg(string arg) => s_os == DockerOS.Windows ? $"%{arg}%" : $"${arg}"; + private static string FormatArg(string arg) => Os == DockerOS.Windows ? $"%{arg}%" : $"${arg}"; } -public enum DockerOS { +public enum DockerOS +{ Linux, Windows, } -public enum PublishConfig { +public enum PublishConfig +{ Aot, FxDependent, SelfContained, diff --git a/tests/Microsoft.DotNet.Docker.Tests/TestScenarios/WebScenario.cs b/tests/Microsoft.DotNet.Docker.Tests/TestScenarios/WebScenario.cs index 1f2f35c07a..0a2a0d4989 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/TestScenarios/WebScenario.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/TestScenarios/WebScenario.cs @@ -1,3 +1,6 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + #nullable enable using System; @@ -8,7 +11,7 @@ using Polly.Retry; using Xunit.Abstractions; -namespace Microsoft.DotNet.Docker.Tests; +namespace Microsoft.DotNet.Docker.Tests.TestScenarios; public abstract class WebScenario(ProductImageData imageData, DockerHelper dockerHelper, ITestOutputHelper outputHelper) : ConsoleAppScenario(imageData, dockerHelper, outputHelper) @@ -67,8 +70,8 @@ public static async Task GetHttpResponseFromContainerAsync( string? pathAndQuery = null, AuthenticationHeaderValue? authorizationHeader = null) { - const int RetryAttempts = 4; - const int RetryDelaySeconds = 3; + const int retryAttempts = 4; + const int retryDelaySeconds = 3; if (!string.IsNullOrEmpty(pathAndQuery) && !pathAndQuery.StartsWith('/')) { @@ -82,17 +85,17 @@ public static async Task GetHttpResponseFromContainerAsync( ResiliencePipeline pipeline = new ResiliencePipelineBuilder() .AddRetry(new RetryStrategyOptions() - { - BackoffType = DelayBackoffType.Exponential, - MaxRetryAttempts = RetryAttempts, - Delay = TimeSpan.FromSeconds(RetryDelaySeconds), - - // If the container is still starting up, it will refuse connections until it's ready. - // If it crashed, stop retrying immediately. - ShouldHandle = new PredicateBuilder() + { + BackoffType = DelayBackoffType.Exponential, + MaxRetryAttempts = retryAttempts, + Delay = TimeSpan.FromSeconds(retryDelaySeconds), + + // If the container is still starting up, it will refuse connections until it's ready. + // If it crashed, stop retrying immediately. + ShouldHandle = new PredicateBuilder() .Handle(exception => DockerHelper.ContainerIsRunning(containerName)), - }) + }) .Build(); using HttpClient client = new(); @@ -132,7 +135,7 @@ public static async Task VerifyHttpResponseFromContainerAsync( ITestOutputHelper outputHelper, int containerPort, string? pathAndQuery = null, - Action? validateCallback = null) + Action? _ = null) { (await GetHttpResponseFromContainerAsync( containerName, diff --git a/tests/Microsoft.DotNet.Docker.Tests/TestSolution.cs b/tests/Microsoft.DotNet.Docker.Tests/TestSolution.cs index 0125aa300a..6b3f893f97 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/TestSolution.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/TestSolution.cs @@ -1,3 +1,6 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + #nullable enable using System; @@ -17,13 +20,12 @@ public class TestSolution : IDisposable private readonly DockerHelper _dockerHelper; private readonly ProductImageData _imageData; private readonly string _appName = "app"; - private readonly string _solutionDir; private readonly string _testProjectDir; private readonly string _appProjectDir; public string SampleName { get; } - public string SolutionDir => _solutionDir; + public string SolutionDir { get; } public TestSolution(ProductImageData imageData, string sampleName, DockerHelper dockerHelper, bool injectCustomTestCode = false) { @@ -31,11 +33,11 @@ public TestSolution(ProductImageData imageData, string sampleName, DockerHelper _imageData = imageData; _dockerHelper = dockerHelper; - _solutionDir = Path.Combine(Directory.GetCurrentDirectory(), $"{sampleName}App{DateTime.Now.ToFileTime()}"); - _testProjectDir = Path.Combine(_solutionDir, "tests"); - _appProjectDir = Path.Combine(_solutionDir, _appName); + SolutionDir = Path.Combine(Directory.GetCurrentDirectory(), $"{sampleName}App{DateTime.Now.ToFileTime()}"); + _testProjectDir = Path.Combine(SolutionDir, "tests"); + _appProjectDir = Path.Combine(SolutionDir, _appName); - CreateTestSolutionWithSdkImage(_solutionDir, sampleName); + CreateTestSolutionWithSdkImage(SolutionDir, sampleName); if (injectCustomTestCode) { @@ -145,7 +147,7 @@ private void InjectCustomTestCode(string appDir) MethodDeclarationSyntax newMainMethod = mainMethod.InsertNodesBefore( mainMethod.Body.ChildNodes().First(), - new SyntaxNode[] { testHttpsConnectivityStatement }); + [testHttpsConnectivityStatement]); SyntaxNode newRoot = programTree.GetRoot().ReplaceNode(mainMethod, newMainMethod); newContent = newRoot.ToFullString(); @@ -201,7 +203,7 @@ protected virtual void Dispose(bool disposing) { if (disposing) { - Directory.Delete(_solutionDir, recursive: true); + Directory.Delete(SolutionDir, recursive: true); } _disposed = true; diff --git a/tests/Microsoft.DotNet.Docker.Tests/WindowsImageTheoryAttribute.cs b/tests/Microsoft.DotNet.Docker.Tests/WindowsImageTheoryAttribute.cs index 26e44300f0..5f5a2a5217 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/WindowsImageTheoryAttribute.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/WindowsImageTheoryAttribute.cs @@ -1,6 +1,5 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. namespace Microsoft.DotNet.Docker.Tests; diff --git a/tests/Microsoft.DotNet.Docker.Tests/YarpImageTests.cs b/tests/Microsoft.DotNet.Docker.Tests/YarpImageTests.cs index 5dc0c5cdec..7057cecf64 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/YarpImageTests.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/YarpImageTests.cs @@ -1,10 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. -using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Threading.Tasks; using Xunit; @@ -15,9 +12,6 @@ namespace Microsoft.DotNet.Docker.Tests; [Trait("Category", "yarp")] public class YarpImageTests(ITestOutputHelper outputHelper) : CommonRuntimeImageTests(outputHelper) { - private const int YarpWebPort = 5000; - - protected override DotNetImageRepo ImageRepo => DotNetImageRepo.Yarp; public static IEnumerable GetImageData() => @@ -26,11 +20,14 @@ public static IEnumerable GetImageData() => [DotNetTheory] [MemberData(nameof(GetImageData))] - public async Task VerifyBasicScenario(ProductImageData imageData) +#pragma warning disable xUnit1026 // Retain the MemberData parameter while this test body is temporarily disabled. + public Task VerifyBasicScenario(ProductImageData _) +#pragma warning restore xUnit1026 { OutputHelper.WriteLine("Skipping VerifyBasicScenario test due to known issues." + " Re-enable this test once the following issue is resolved:" + " https://github.com/dotnet/dotnet-docker/issues/6639"); + return Task.CompletedTask; // YarpBasicScenario testScenario = new(YarpWebPort, imageData, DockerHelper, OutputHelper); // await testScenario.ExecuteAsync(); @@ -48,12 +45,14 @@ public void VerifyEnvironmentVariables(ProductImageData imageData) ]; string imageTag = imageData.GetImage(ImageRepo, DockerHelper); - EnvironmentVariableInfo.Validate(expectedVariables, imageTag, imageData, DockerHelper); + EnvironmentVariableInfo.Validate(expectedVariables, imageTag, DockerHelper); } [LinuxImageTheory] [MemberData(nameof(GetImageData))] - public void VerifyInsecureFiles(ProductImageData imageData) +#pragma warning disable xUnit1026 // Retain the MemberData parameter while this test body is temporarily disabled. + public void VerifyInsecureFiles(ProductImageData _) +#pragma warning restore xUnit1026 { OutputHelper.WriteLine("Skipping VerifyInsecureFiles test due to known issues." + " Re-enable this test once the following issue is resolved:" diff --git a/tests/UpdateDependencies.Tests/AzureCredentialProviderTests.cs b/tests/UpdateDependencies.Tests/AzureCredentialProviderTests.cs index 3b863767e4..f56b3774b0 100644 --- a/tests/UpdateDependencies.Tests/AzureCredentialProviderTests.cs +++ b/tests/UpdateDependencies.Tests/AzureCredentialProviderTests.cs @@ -23,7 +23,7 @@ public sealed class AzureCredentialProviderTests [Fact] public void ConfiguredServiceConnection_UsesAzurePipelinesCredential() { - var provider = CreateProvider(ServiceConnections, systemAccessToken: "token", inPipeline: true); + AzureCredentialProvider provider = CreateProvider(ServiceConnections, systemAccessToken: "token", inPipeline: true); provider.GetCredential("staging").ShouldBeOfType(); } @@ -31,7 +31,7 @@ public void ConfiguredServiceConnection_UsesAzurePipelinesCredential() [Fact] public void UnknownServiceConnection_Throws() { - var provider = CreateProvider(ServiceConnections, systemAccessToken: "token", inPipeline: true); + AzureCredentialProvider provider = CreateProvider(ServiceConnections, systemAccessToken: "token", inPipeline: true); Should.Throw(() => provider.GetCredential("other")) .Message.ShouldContain("staging"); @@ -40,7 +40,7 @@ public void UnknownServiceConnection_Throws() [Fact] public void MissingSystemAccessToken_Throws() { - var provider = CreateProvider(ServiceConnections, systemAccessToken: null, inPipeline: true); + AzureCredentialProvider provider = CreateProvider(ServiceConnections, systemAccessToken: null, inPipeline: true); Should.Throw(() => provider.GetCredential("staging")); } @@ -48,7 +48,7 @@ public void MissingSystemAccessToken_Throws() [Fact] public void NoServiceConnectionsInPipeline_Throws() { - var provider = CreateProvider(serviceConnections: null, systemAccessToken: "token", inPipeline: true); + AzureCredentialProvider provider = CreateProvider(serviceConnections: null, systemAccessToken: "token", inPipeline: true); Should.Throw(() => provider.GetCredential("staging")) .Message.ShouldContain(""); @@ -57,7 +57,7 @@ public void NoServiceConnectionsInPipeline_Throws() [Fact] public void Locally_UsesDeveloperCredential() { - var provider = CreateProvider(ServiceConnections, systemAccessToken: null, inPipeline: false); + AzureCredentialProvider provider = CreateProvider(ServiceConnections, systemAccessToken: null, inPipeline: false); provider.GetCredential("staging").ShouldBeOfType(); } @@ -74,7 +74,7 @@ private static AzureCredentialProvider CreateProvider( }) .Build(); - var environmentService = Mock.Of(env => + IEnvironmentService environmentService = Mock.Of(env => env.IsRunningInAzurePipelines() == inPipeline && env.GetSystemAccessToken() == systemAccessToken); return new AzureCredentialProvider(configuration, environmentService); diff --git a/tests/UpdateDependencies.Tests/BuildLabelServiceTests.cs b/tests/UpdateDependencies.Tests/BuildLabelServiceTests.cs index d85538284f..dd98614dc4 100644 --- a/tests/UpdateDependencies.Tests/BuildLabelServiceTests.cs +++ b/tests/UpdateDependencies.Tests/BuildLabelServiceTests.cs @@ -10,8 +10,8 @@ public sealed class BuildLabelServiceTests [Fact] public void AddBuildTags_WritesSingleTag() { - using var writer = new StringWriter(); - var service = new BuildLabelService(writer); + using StringWriter writer = new StringWriter(); + BuildLabelService service = new BuildLabelService(writer); service.AddBuildTags("my-tag"); @@ -25,12 +25,12 @@ public void AddBuildTags_WritesSingleTag() [Fact] public void AddBuildTags_WritesMultipleTags() { - using var writer = new StringWriter(); - var service = new BuildLabelService(writer); + using StringWriter writer = new StringWriter(); + BuildLabelService service = new BuildLabelService(writer); service.AddBuildTags("tag1", "tag2", "tag3"); - var expected = """ + string expected = """ ##vso[build.addbuildtag]tag1 ##vso[build.addbuildtag]tag2 ##vso[build.addbuildtag]tag3 @@ -42,8 +42,8 @@ public void AddBuildTags_WritesMultipleTags() [Fact] public void AddBuildTags_WithEmptyEnumerable_WritesNothing() { - using var writer = new StringWriter(); - var service = new BuildLabelService(writer); + using StringWriter writer = new StringWriter(); + BuildLabelService service = new BuildLabelService(writer); service.AddBuildTags([]); diff --git a/tests/UpdateDependencies.Tests/DotNetUpdaterTests.cs b/tests/UpdateDependencies.Tests/DotNetUpdaterTests.cs index 07c30a55c0..65a7530da2 100644 --- a/tests/UpdateDependencies.Tests/DotNetUpdaterTests.cs +++ b/tests/UpdateDependencies.Tests/DotNetUpdaterTests.cs @@ -1,10 +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 Microsoft.DotNet.DarcLib; using Microsoft.DotNet.Docker.UpdateDependencies; -using Microsoft.DotNet.Docker.UpdateDependencies.Updaters; using Microsoft.DotNet.Docker.UpdateDependencies.Model.Release; -using Microsoft.DotNet.DarcLib; +using Microsoft.DotNet.Docker.UpdateDependencies.Updaters; using Microsoft.Extensions.Logging; namespace UpdateDependencies.Tests; @@ -20,9 +20,9 @@ public sealed class DotNetUpdaterTests [InlineData("11.0.2+build.123", "11.0.2")] public async Task Staging_WritesBuildVersionAndProductTag(string buildVersion, string productVersion) { - using var repo = new TempRepo(); - var variables = CreateManifest(repo.LocalPath); - var release = CreateRelease() with { RuntimeBuild = buildVersion }; + using TempRepo repo = new TempRepo(); + ManifestVariables variables = CreateManifest(repo.LocalPath); + ReleaseConfig release = CreateRelease() with { RuntimeBuild = buildVersion }; await CreateUpdater().UpdateFromStagingPipelineAsync( variables, repo.LocalPath, release, "https://example/internal", TestContext.Current.CancellationToken); @@ -38,8 +38,8 @@ await CreateUpdater().UpdateFromStagingPipelineAsync( [InlineData("11.0.1", "11.0.2")] public async Task Staging_PreservesDisabledVersionsAndAliases(string current, string expected) { - using var repo = new TempRepo(); - var variables = CreateManifest(repo.LocalPath); + using TempRepo repo = new TempRepo(); + ManifestVariables variables = CreateManifest(repo.LocalPath); variables.SetValue("runtime|11.0|build-version", current); await CreateUpdater().UpdateFromStagingPipelineAsync( diff --git a/tests/UpdateDependencies.Tests/FromStagingPipelineCommandTests.cs b/tests/UpdateDependencies.Tests/FromStagingPipelineCommandTests.cs index 45a3985443..f7b12a1cfd 100644 --- a/tests/UpdateDependencies.Tests/FromStagingPipelineCommandTests.cs +++ b/tests/UpdateDependencies.Tests/FromStagingPipelineCommandTests.cs @@ -1,14 +1,13 @@ // 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.DarcLib; using Microsoft.DotNet.Docker.UpdateDependencies; using Microsoft.DotNet.Docker.UpdateDependencies.Commands; -using Microsoft.DotNet.Docker.UpdateDependencies.Updaters; using Microsoft.DotNet.Docker.UpdateDependencies.Git; using Microsoft.DotNet.Docker.UpdateDependencies.Model.Release; using Microsoft.DotNet.Docker.UpdateDependencies.Sync; -using Microsoft.DotNet.DarcLib; -using Microsoft.Extensions.DependencyInjection; +using Microsoft.DotNet.Docker.UpdateDependencies.Updaters; using Microsoft.Extensions.Logging; namespace UpdateDependencies.Tests; @@ -19,17 +18,17 @@ public sealed class FromStagingPipelineCommandTests public async Task ExecuteAsync_InternalMode_AddsBuildTagWithStageContainer() { const string stageContainer = "stage-1234567"; - var expectedTag = $"Container - {stageContainer}"; - using var repo = CreateRepo(); + string expectedTag = $"Container - {stageContainer}"; + using TempRepo repo = CreateRepo(); - using var output = new StringWriter(); - var buildLabelService = new BuildLabelService(output); + using StringWriter output = new StringWriter(); + BuildLabelService buildLabelService = new BuildLabelService(output); - var command = CreateCommand( + FromStagingPipelineCommand command = CreateCommand( buildLabelService: buildLabelService, pipelineArtifactProvider: CreateMockPipelineArtifactProvider()); - var options = new FromStagingPipelineOptions + FromStagingPipelineOptions options = new FromStagingPipelineOptions { StageContainers = stageContainer, Internal = true, @@ -45,15 +44,15 @@ public async Task ExecuteAsync_InternalMode_AddsBuildTagWithStageContainer() [Fact] public async Task ExecuteAsync_NotInternalMode_DoesNotAddBuildTag() { - using var repo = CreateRepo(); - using var output = new StringWriter(); - var buildLabelService = new BuildLabelService(output); + using TempRepo repo = CreateRepo(); + using StringWriter output = new StringWriter(); + BuildLabelService buildLabelService = new BuildLabelService(output); - var command = CreateCommand( + FromStagingPipelineCommand command = CreateCommand( buildLabelService: buildLabelService, pipelineArtifactProvider: CreateMockPipelineArtifactProvider()); - var options = new FromStagingPipelineOptions + FromStagingPipelineOptions options = new FromStagingPipelineOptions { StageContainers = "stage-1234567", Internal = false, @@ -71,7 +70,7 @@ public async Task ExecuteAsync_NotInternalMode_DoesNotAddBuildTag() [InlineData("stage-999999999", 999999999)] public void GetStagingPipelineRunId_ValidStageContainer_ReturnsExpectedId(string stageContainer, int expectedId) { - var result = StagingPipelineOptionsExtensions.GetStagingPipelineRunId(stageContainer); + int result = StagingPipelineOptionsExtensions.GetStagingPipelineRunId(stageContainer); result.ShouldBe(expectedId); } @@ -84,7 +83,7 @@ public void GetStagingPipelineRunId_ValidStageContainer_ReturnsExpectedId(string [InlineData("")] public void GetStagingPipelineRunId_InvalidStageContainer_ThrowsArgumentException(string stageContainer) { - var exception = Should.Throw(() => + ArgumentException exception = Should.Throw(() => StagingPipelineOptionsExtensions.GetStagingPipelineRunId(stageContainer)); exception.Message.ShouldContain($"Invalid stage container name '{stageContainer}'"); exception.Message.ShouldContain("Expected format: 'stage-{buildId}'"); @@ -97,13 +96,13 @@ public void GetStagingPipelineRunId_InvalidStageContainer_ThrowsArgumentExceptio [InlineData(" stage-1234567 , stage-2345678 ", new[] { "stage-1234567", "stage-2345678" })] public void GetStageContainerList_ParsesCommaDelimitedList(string input, string[] expected) { - var options = new FromStagingPipelineOptions + FromStagingPipelineOptions options = new FromStagingPipelineOptions { StageContainers = input, RepoRoot = "/tmp/repo" }; - var result = options.GetStageContainerList(); + IReadOnlyList result = options.GetStageContainerList(); result.ShouldBe(expected); } @@ -114,13 +113,13 @@ public void GetStageContainerList_ParsesCommaDelimitedList(string input, string[ [InlineData(",,,")] public void GetStageContainerList_EmptyOrWhitespace_ReturnsEmptyList(string input) { - var options = new FromStagingPipelineOptions + FromStagingPipelineOptions options = new FromStagingPipelineOptions { StageContainers = input, RepoRoot = "/tmp/repo" }; - var result = options.GetStageContainerList(); + IReadOnlyList result = options.GetStageContainerList(); result.ShouldBeEmpty(); } @@ -134,7 +133,7 @@ private static FromStagingPipelineCommand CreateCommand( IBuildLabelService? buildLabelService = null, IGitRepoHelperFactory? gitRepoHelperFactory = null) { - var updater = new DotNetUpdater( + DotNetUpdater updater = new DotNetUpdater( Mock.Of(), Mock.Of>()); return new(new UpdateDependenciesConfiguration(), @@ -150,7 +149,7 @@ private static FromStagingPipelineCommand CreateCommand( private static TempRepo CreateRepo() { - var repo = new TempRepo(); + TempRepo repo = new TempRepo(); File.WriteAllText(Path.Combine(repo.LocalPath, "manifest.versions.json"), """ {"variables":{"branch":"nightly","sdk|9.0|build-version":"9.0.100"}} """); @@ -158,7 +157,7 @@ private static TempRepo CreateRepo() Directory.CreateDirectory(configDirectory); File.WriteAllText(Path.Combine(configDirectory, "NuGet.config.internal"), ""); File.WriteAllText(Path.Combine(configDirectory, "NuGet.config.nightly"), ""); - foreach (var (directory, file) in new[] + foreach ((string? directory, string? file) in new[] { ("dockerfile-templates", "Get-GeneratedDockerfiles.ps1"), ("readme-templates", "Get-GeneratedReadmes.ps1"), @@ -168,12 +167,13 @@ private static TempRepo CreateRepo() Directory.CreateDirectory(path); File.WriteAllText(Path.Combine(path, file), "exit 0"); } + return repo; } private static IPipelineArtifactProvider CreateMockPipelineArtifactProvider() { - var mock = new Mock(); + Mock mock = new Mock(); mock.Setup(p => p.GetReleaseConfigAsync(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(CreateReleaseConfig()); return mock.Object; @@ -181,9 +181,9 @@ private static IPipelineArtifactProvider CreateMockPipelineArtifactProvider() private static IPipelinesService CreateMockPipelinesService() { - var mock = new Mock(); + Mock mock = new Mock(); mock.Setup(p => p.GetBuildTagsAsync(It.IsAny(), It.IsAny(), It.IsAny())) - .ReturnsAsync(new List()); + .ReturnsAsync([]); return mock.Object; } diff --git a/tests/UpdateDependencies.Tests/InternalStagingBuildsTests.cs b/tests/UpdateDependencies.Tests/InternalStagingBuildsTests.cs index d927b30ff1..debdea7f93 100644 --- a/tests/UpdateDependencies.Tests/InternalStagingBuildsTests.cs +++ b/tests/UpdateDependencies.Tests/InternalStagingBuildsTests.cs @@ -2,8 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Immutable; -using Microsoft.DotNet.Docker.UpdateDependencies.Sync; using Microsoft.DotNet.Docker.Shared; +using Microsoft.DotNet.Docker.UpdateDependencies.Sync; namespace UpdateDependencies.Tests; @@ -12,15 +12,15 @@ public sealed class InternalStagingBuildsTests [Fact] public void ToStringOrdersVersionsAscending() { - var builds = new InternalStageContainers( - ImmutableDictionary.CreateRange( + InternalStageContainers builds = new InternalStageContainers( + ImmutableDictionary.CreateRange( [ KeyValuePair.Create(DotNetVersion.Parse("10.0"), "stage-1000"), KeyValuePair.Create(DotNetVersion.Parse("8.0"), "stage-800"), KeyValuePair.Create(DotNetVersion.Parse("9.0"), "stage-900") ])); - var lines = builds.ToString().Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries); + string[] lines = builds.ToString().Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries); lines.ShouldBe(["8.0=stage-800", "9.0=stage-900", "10.0=stage-1000"]); } @@ -28,8 +28,8 @@ public void ToStringOrdersVersionsAscending() [Fact] public void AddReplacesEntryForSameMajorMinorVersion() { - var builds = new InternalStageContainers( - ImmutableDictionary.CreateRange( + InternalStageContainers builds = new InternalStageContainers( + ImmutableDictionary.CreateRange( [KeyValuePair.Create(DotNetVersion.Parse("8.0"), "stage-100")])); builds = builds.Add(DotNetVersion.Parse("8.0.101"), "stage-200"); @@ -42,13 +42,13 @@ public void AddReplacesEntryForSameMajorMinorVersion() [Fact] public void ToStringOmitsPatchVersion() { - var builds = new InternalStageContainers(ImmutableDictionary.Empty); + InternalStageContainers builds = new InternalStageContainers([]); builds = builds.Add(DotNetVersion.Parse("8.0.101"), "stage-100"); builds = builds.Add(DotNetVersion.Parse("9.1.205-servicing.1"), "stage-200"); - foreach (var line in builds.ToString().Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries)) + foreach (string line in builds.ToString().Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries)) { - var versionPart = line.Split('=')[0]; + string versionPart = line.Split('=')[0]; versionPart.Count(c => c == '.').ShouldBe(1); } } diff --git a/tests/UpdateDependencies.Tests/LocalGitRepoHelperTests.cs b/tests/UpdateDependencies.Tests/LocalGitRepoHelperTests.cs index 87d70c4986..e125a711af 100644 --- a/tests/UpdateDependencies.Tests/LocalGitRepoHelperTests.cs +++ b/tests/UpdateDependencies.Tests/LocalGitRepoHelperTests.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.DotNet.Docker.UpdateDependencies.Git; using Microsoft.DotNet.DarcLib; +using Microsoft.DotNet.Docker.UpdateDependencies.Git; using Microsoft.Extensions.Logging; namespace UpdateDependencies.Tests; @@ -12,14 +12,14 @@ public sealed class LocalGitRepoHelperTests [Fact] public async Task IsBranchAncestor() { - const string AncestorBranch = "ancestor"; - const string DescendantBranch = "descendant"; + const string ancestorBranch = "ancestor"; + const string descendantBranch = "descendant"; // example.com is safe for use in testing - it is reserved per RFC2606 - var authorIdentity = (Name: "Test Author", Email: "test@example.com"); - var committerIdentity = (Name: "Test Committer", Email: "test@example.com"); + (string Name, string Email) authorIdentity = (Name: "Test Author", Email: "test@example.com"); + (string name, string email) = ("Test Committer", "test@example.com"); - var repo = await CreateLocalRepoHelperAsync( + ILocalGitRepoHelper repo = await CreateLocalRepoHelperAsync( async localRepo => { await localRepo.ExecuteGitCommand("init"); @@ -27,38 +27,38 @@ public async Task IsBranchAncestor() // Commit author and committer identity are separate concepts. // `author:` argument below sets the author identity for the commit, but we also // need to set the committer identity beforehand. - await localRepo.ExecuteGitCommand("config", "user.name", committerIdentity.Name); - await localRepo.ExecuteGitCommand("config", "user.email", committerIdentity.Email); + await localRepo.ExecuteGitCommand("config", "user.name", name); + await localRepo.ExecuteGitCommand("config", "user.email", email); // Create initial commit on main branch - var initialFilePath = Path.Join(localRepo.Path, "initialFile.txt"); + string initialFilePath = Path.Join(localRepo.Path, "initialFile.txt"); File.WriteAllText(initialFilePath, ""); await localRepo.StageAsync([initialFilePath]); await localRepo.CommitAsync("Initial commit", allowEmpty: false, author: authorIdentity); // Create ancestor branch - await localRepo.CreateBranchAsync(AncestorBranch); + await localRepo.CreateBranchAsync(ancestorBranch); // Create descendant branch - await localRepo.CreateBranchAsync(DescendantBranch); - var newFilePath = Path.Join(localRepo.Path, "newFile.txt"); + await localRepo.CreateBranchAsync(descendantBranch); + string newFilePath = Path.Join(localRepo.Path, "newFile.txt"); File.WriteAllText(newFilePath, ""); await localRepo.StageAsync([newFilePath]); await localRepo.CommitAsync("New commit", allowEmpty: false, author: authorIdentity); }); // Validate that repo helper correctly identifies ancestor/descendant relationship - var isAncestorAsync = await repo.IsAncestorAsync(AncestorBranch, DescendantBranch); + bool isAncestorAsync = await repo.IsAncestorAsync(ancestorBranch, descendantBranch); isAncestorAsync.ShouldBeTrue(); } private static async Task CreateLocalRepoHelperAsync( Func setupLocalAsync) { - var tempRepo = new TempRepo(); + TempRepo tempRepo = new TempRepo(); await setupLocalAsync(tempRepo.LocalGitRepo); - var localRepoHelper = new LocalGitRepoHelper( + LocalGitRepoHelper localRepoHelper = new LocalGitRepoHelper( localPath: tempRepo.LocalPath, localGitRepo: tempRepo.LocalGitRepo, logger: Mock.Of>()); diff --git a/tests/UpdateDependencies.Tests/ManifestVariablesTests.cs b/tests/UpdateDependencies.Tests/ManifestVariablesTests.cs index 0571b997c5..ca69e7d56d 100644 --- a/tests/UpdateDependencies.Tests/ManifestVariablesTests.cs +++ b/tests/UpdateDependencies.Tests/ManifestVariablesTests.cs @@ -15,7 +15,7 @@ public sealed class ManifestVariablesTests [InlineData("literal", true)] public void ShouldUpdateLiteral_ExcludesMissingEmptyAndReferenceValues(string name, bool expected) { - var variables = new ManifestVariables(""" + ManifestVariables variables = new ManifestVariables(""" {"variables":{"empty":"","reference":"$(literal)","literal":"1.2.3"}} """); @@ -30,7 +30,7 @@ public void SetValue_PreservesEverythingOutsideTheValueToken() const string content = "{\r\n // keep this comment\r\n \"other\": {\"version\":\"old\"},\r\n" + " \"variables\": { \"version\" \t: \"old\", \"empty\": \"\", },\r\n}\r\n"; - var variables = new ManifestVariables(content); + ManifestVariables variables = new ManifestVariables(content); variables.SetValue("version", "new").ShouldBeTrue(); @@ -50,14 +50,14 @@ public void SetValue_HandlesEscapingUnicodeAndRepeatedEdits() """; // Use actual multibyte characters before the edited token. string original = content.Replace("\\u00e9", "\u00e9\U0001F600"); - var variables = new ManifestVariables(original); + ManifestVariables variables = new ManifestVariables(original); const string replacement = "quote: \" slash: \\ newline:\n dollar: $1 \u00e9"; variables.SetValue("name", replacement).ShouldBeTrue(); variables.SetValue("later", "longer value").ShouldBeTrue(); variables.SetValue("name", replacement).ShouldBeFalse(); - var reparsed = new ManifestVariables(variables.Content); + ManifestVariables reparsed = new ManifestVariables(variables.Content); reparsed.GetRawValue("name").ShouldBe(replacement); reparsed.GetRawValue("later").ShouldBe("longer value"); variables.Content.ShouldStartWith("{\"note\":\"\u00e9\U0001F600\","); @@ -74,7 +74,7 @@ public void SetValue_PreservesOffsetsAcrossMultibyteValuesAndGaps() const string content = "{\"variables\":{\"first\":\"\u00e9\U0001F600\",/* \u00e9 */" + "\"\u00e9\":\"middle\",\"last\":\"\U0001F600\"}}"; - var variables = new ManifestVariables(content); + ManifestVariables variables = new ManifestVariables(content); // Edit out of source order, with different replacement lengths. variables.SetValue("last", "long replacement"); @@ -96,7 +96,7 @@ public void SetValue_PreservesOffsetsAcrossMultibyteValuesAndGaps() public void SetValue_UnchangedDecodedValuePreservesOriginalEscapes() { const string content = """{"variables":{"version":"\u0031"}}"""; - var variables = new ManifestVariables(content); + ManifestVariables variables = new ManifestVariables(content); variables.SetValue("version", "1").ShouldBeFalse(); @@ -106,7 +106,7 @@ public void SetValue_UnchangedDecodedValuePreservesOriginalEscapes() [Fact] public void GetValue_ResolvesCurrentValuesWithoutRewritingReferences() { - var variables = new ManifestVariables(""" + ManifestVariables variables = new ManifestVariables(""" {"variables":{ "version":"1", "alias":"$(version)", @@ -131,13 +131,13 @@ public void GetValue_ResolvesCurrentValuesWithoutRewritingReferences() public void MissingVariables_AreNotTreatedAsEmptyOrInserted() { const string content = """{"variables":{"empty":"","alias":"$(missing)"}}"""; - var variables = new ManifestVariables(content); + ManifestVariables variables = new ManifestVariables(content); variables.Contains("missing").ShouldBeFalse(); Should.Throw(() => variables.GetRawValue("missing")); Should.Throw(() => variables.GetValue("missing")); variables.SetValue("missing", "value").ShouldBeFalse(); - var error = Should.Throw(() => variables.GetValue("alias")); + KeyNotFoundException error = Should.Throw(() => variables.GetValue("alias")); error.Message.ShouldContain("alias -> missing"); variables.Content.ShouldBe(content); @@ -148,9 +148,9 @@ public void MissingVariables_AreNotTreatedAsEmptyOrInserted() [InlineData("""{"variables":{"a":"$(b)","b":"$(c)","c":"$(a)"}}""", "a -> b -> c -> a")] public void GetValue_ReportsReferenceCycles(string content, string expectedChain) { - var variables = new ManifestVariables(content); + ManifestVariables variables = new ManifestVariables(content); - var error = Should.Throw(() => variables.GetValue("a")); + InvalidOperationException error = Should.Throw(() => variables.GetValue("a")); error.Message.ShouldContain(expectedChain); variables.SetValue("a", "fixed"); @@ -171,10 +171,7 @@ public void GetValue_ReportsReferenceCycles(string content, string expectedChain [InlineData("""{"variables":{},"variables":{}}""")] [InlineData("""{"variables":{}} {}""")] [InlineData("""{"variables":{"a":"1"}""")] - public void Constructor_RejectsInvalidOrAmbiguousManifests(string content) - { - Should.Throw(() => new ManifestVariables(content)); - } + public void Constructor_RejectsInvalidOrAmbiguousManifests(string content) => Should.Throw(() => new ManifestVariables(content)); [Fact] public void Constructor_AcceptsEmptyVariablesAndSkipsUnrelatedStructures() @@ -182,7 +179,7 @@ public void Constructor_AcceptsEmptyVariablesAndSkipsUnrelatedStructures() const string content = """ {"before":[{"variables":{"a":1}}],"variables":{},"after":{"a":[1,2]}} """; - var variables = new ManifestVariables(content); + ManifestVariables variables = new ManifestVariables(content); variables.Names.ShouldBeEmpty(); variables.Content.ShouldBe(content); diff --git a/tests/UpdateDependencies.Tests/MonitorUpdaterTests.cs b/tests/UpdateDependencies.Tests/MonitorUpdaterTests.cs index 1ac47df15c..f6f0a16f48 100644 --- a/tests/UpdateDependencies.Tests/MonitorUpdaterTests.cs +++ b/tests/UpdateDependencies.Tests/MonitorUpdaterTests.cs @@ -16,7 +16,7 @@ public sealed class MonitorUpdaterTests [InlineData("9.0.5", "9.0.5")] public async Task Version_WritesBuildVersionAndProductTag(string version, string productVersion) { - var variables = new ManifestVariables(""" + ManifestVariables variables = new ManifestVariables(""" { "variables": { "branch": "nightly", @@ -25,8 +25,8 @@ public async Task Version_WritesBuildVersionAndProductTag(string version, string } } """); - using var httpClient = new HttpClient(); - var updater = CreateUpdater(httpClient); + using HttpClient httpClient = new HttpClient(); + MonitorUpdater updater = CreateUpdater(httpClient); DependencyUpdate update = updater.ResolveFromVersion(version); await update.ApplyAsync(variables, "", TestContext.Current.CancellationToken); @@ -38,7 +38,7 @@ public async Task Version_WritesBuildVersionAndProductTag(string version, string [Fact] public async Task Version_PreservesAliasesAndOtherVersionChecksums() { - var variables = new ManifestVariables(""" + ManifestVariables variables = new ManifestVariables(""" { "variables": { "branch": "nightly", @@ -50,8 +50,8 @@ public async Task Version_PreservesAliasesAndOtherVersionChecksums() } } """); - using var httpClient = new HttpClient(); - var updater = CreateUpdater(httpClient); + using HttpClient httpClient = new HttpClient(); + MonitorUpdater updater = CreateUpdater(httpClient); DependencyUpdate update = updater.ResolveFromVersion("9.0.5"); await update.ApplyAsync(variables, "", TestContext.Current.CancellationToken); diff --git a/tests/UpdateDependencies.Tests/NuGetConfigUpdaterTests.cs b/tests/UpdateDependencies.Tests/NuGetConfigUpdaterTests.cs index 97abc70171..acc1b78b8e 100644 --- a/tests/UpdateDependencies.Tests/NuGetConfigUpdaterTests.cs +++ b/tests/UpdateDependencies.Tests/NuGetConfigUpdaterTests.cs @@ -13,8 +13,8 @@ public sealed class NuGetConfigUpdaterTests [InlineData("nightly", ".nightly")] public void Update_RemovesInternalFeedAndPreservesOtherVersionCredentials(string branch, string suffix) { - using var repo = new TempRepo(); - var variables = new ManifestVariables("""{"variables":{"branch":"nightly"}}"""); + using TempRepo repo = new TempRepo(); + ManifestVariables variables = new ManifestVariables("""{"variables":{"branch":"nightly"}}"""); variables.SetValue("branch", branch); string configDirectory = Path.Combine(repo.LocalPath, "tests", "Microsoft.DotNet.Docker.Tests", "TestAppArtifacts"); Directory.CreateDirectory(configDirectory); diff --git a/tests/UpdateDependencies.Tests/PowerShellUpdaterTests.cs b/tests/UpdateDependencies.Tests/PowerShellUpdaterTests.cs index 40e6cb0d32..c967bef037 100644 --- a/tests/UpdateDependencies.Tests/PowerShellUpdaterTests.cs +++ b/tests/UpdateDependencies.Tests/PowerShellUpdaterTests.cs @@ -37,27 +37,27 @@ public sealed class PowerShellUpdaterTests } """; - private static readonly string s_alpineSha = new('a', 64); - private static readonly string s_linuxSha = new('b', 64); - private static readonly string s_windowsSha = new('c', 64); + private static readonly string AlpineSha = new('a', 64); + private static readonly string LinuxSha = new('b', 64); + private static readonly string WindowsSha = new('c', 64); [Fact] public async Task Public_UpdatesMatchingMajorMinorVersion() { - var handler = new StubHandler(new() + StubHandler handler = new StubHandler(new() { [$"{PublicRoot}/7.5.12/SHA512SUMS"] = CreateChecksums("7.5.12"), }); - var variables = new ManifestVariables(Manifest); + ManifestVariables variables = new ManifestVariables(Manifest); await ApplyAsync(CreateUpdater(handler), variables, "7.5.12"); variables.GetRawValue("powershell|9.0|build-version").ShouldBe("7.5.12"); variables.GetRawValue("powershell|9.0|base-url").ShouldBe("$(powershell|base-url|public)/$(powershell|9.0|build-version)"); - variables.GetRawValue("powershell|9.0|Linux.Alpine|sha").ShouldBe(s_alpineSha); - variables.GetRawValue("powershell|9.0|Linux|x64|sha").ShouldBe(s_linuxSha); - variables.GetRawValue("powershell|9.0|Windows|x64|sha").ShouldBe(s_windowsSha); + variables.GetRawValue("powershell|9.0|Linux.Alpine|sha").ShouldBe(AlpineSha); + variables.GetRawValue("powershell|9.0|Linux|x64|sha").ShouldBe(LinuxSha); + variables.GetRawValue("powershell|9.0|Windows|x64|sha").ShouldBe(WindowsSha); variables.GetRawValue("powershell|10.0|build-version").ShouldBe("7.6.6"); variables.GetRawValue("powershell|10.0|Linux|x64|sha").ShouldBe("unchanged"); handler.Requests.ShouldHaveSingleItem().Headers.Authorization.ShouldBeNull(); @@ -66,19 +66,19 @@ public async Task Public_UpdatesMatchingMajorMinorVersion() [Fact] public async Task Internal_UsesInternalLayoutAndAuthenticates() { - var handler = new StubHandler(new() + StubHandler handler = new StubHandler(new() { [$"{InternalRoot}/v7-7-0-preview-6-nuget/globaltool/SHA512SUMS"] = CreateChecksums("7.7.0-preview.6"), }); - var variables = new ManifestVariables(Manifest); + ManifestVariables variables = new ManifestVariables(Manifest); await ApplyAsync(CreateInternalUpdater(handler), variables, "7.7.0-preview.6", isInternal: true); variables.GetRawValue("powershell|11.0|build-version").ShouldBe("7.7.0-preview.6"); variables.GetRawValue("powershell|11.0|base-url") .ShouldBe("$(powershell|base-url|internal)/v7-7-0-preview-6-nuget/globaltool"); - variables.GetRawValue("powershell|11.0|Linux|x64|sha").ShouldBe(s_linuxSha); + variables.GetRawValue("powershell|11.0|Linux|x64|sha").ShouldBe(LinuxSha); variables.GetRawValue("powershell|11.0|sha-function").ShouldBe("256"); handler.Requests.ShouldHaveSingleItem().Headers.Authorization.ShouldBe(new("Bearer", "test-token")); } @@ -87,39 +87,39 @@ public async Task Internal_UsesInternalLayoutAndAuthenticates() public async Task BaseUrl_IsWrittenLiterally() { const string baseUrl = "https://custom.example.com/powershell/7.5.12"; - var handler = new StubHandler(new() + StubHandler handler = new StubHandler(new() { [$"{baseUrl}/SHA512SUMS"] = CreateChecksums("7.5.12"), }); - var variables = new ManifestVariables(Manifest); + ManifestVariables variables = new ManifestVariables(Manifest); await ApplyAsync(CreateUpdater(handler), variables, "7.5.12", baseUrl: baseUrl); variables.GetRawValue("powershell|9.0|base-url").ShouldBe(baseUrl); - variables.GetRawValue("powershell|9.0|Linux|x64|sha").ShouldBe(s_linuxSha); + variables.GetRawValue("powershell|9.0|Linux|x64|sha").ShouldBe(LinuxSha); } [Fact] public async Task DotnetVersion_SelectsImagesOutsideTheMajorMinorVersion() { - var handler = new StubHandler(new() + StubHandler handler = new StubHandler(new() { [$"{PublicRoot}/7.8.0-preview.1/SHA512SUMS"] = CreateChecksums("7.8.0-preview.1"), }); - var variables = new ManifestVariables(Manifest); + ManifestVariables variables = new ManifestVariables(Manifest); await ApplyAsync(CreateUpdater(handler), variables, "7.8.0-preview.1", dotnetVersion: "11.0"); variables.GetRawValue("powershell|11.0|build-version").ShouldBe("7.8.0-preview.1"); - variables.GetRawValue("powershell|11.0|Linux|x64|sha").ShouldBe(s_linuxSha); + variables.GetRawValue("powershell|11.0|Linux|x64|sha").ShouldBe(LinuxSha); } [Fact] public async Task NoMatchingMajorMinorVersion_Throws() { - var variables = new ManifestVariables(Manifest); + ManifestVariables variables = new ManifestVariables(Manifest); await Should.ThrowAsync(() => ApplyAsync(CreateUpdater(new StubHandler([])), variables, "7.8.0")); @@ -128,12 +128,12 @@ await Should.ThrowAsync(() => [Fact] public async Task MissingChecksum_Throws() { - var handler = new StubHandler(new() + StubHandler handler = new StubHandler(new() { - [$"{PublicRoot}/7.5.12/SHA512SUMS"] = $"{s_linuxSha} *PowerShell.Linux.x64.7.5.12.nupkg\r\n", + [$"{PublicRoot}/7.5.12/SHA512SUMS"] = $"{LinuxSha} *PowerShell.Linux.x64.7.5.12.nupkg\r\n", }); - var variables = new ManifestVariables(Manifest); + ManifestVariables variables = new ManifestVariables(Manifest); await Should.ThrowAsync(() => ApplyAsync(CreateUpdater(handler), variables, "7.5.12")); } @@ -141,7 +141,7 @@ public async Task MissingChecksum_Throws() [Fact] public async Task Latest_FollowsPreviewThenRcThenRelease() { - var handler = new StubHandler(new() + StubHandler handler = new StubHandler(new() { [$"{PublicRoot}/7.7.0-preview.6/SHA512SUMS"] = "", [$"{PublicRoot}/7.7.0-rc.1/SHA512SUMS"] = "", @@ -149,7 +149,7 @@ public async Task Latest_FollowsPreviewThenRcThenRelease() [$"{PublicRoot}/7.7.1/SHA512SUMS"] = CreateChecksums("7.7.1"), }); - var variables = new ManifestVariables(Manifest); + ManifestVariables variables = new ManifestVariables(Manifest); PowerShellUpdater updater = CreateUpdater(handler); @@ -165,12 +165,12 @@ public async Task Latest_FollowsPreviewThenRcThenRelease() [Fact] public async Task Latest_Internal_UsesInternalLayout() { - var handler = new StubHandler(new() + StubHandler handler = new StubHandler(new() { [$"{InternalRoot}/v7-5-12-nuget/globaltool/SHA512SUMS"] = CreateChecksums("7.5.12"), }); - var variables = new ManifestVariables(Manifest); + ManifestVariables variables = new ManifestVariables(Manifest); PowerShellUpdater updater = CreateInternalUpdater(handler); @@ -201,21 +201,21 @@ private static PowerShellUpdater CreateUpdater(StubHandler handler) => private static PowerShellUpdater CreateInternalUpdater(StubHandler handler) { - var credential = new Mock(); + Mock credential = new Mock(); credential .Setup(c => c.GetTokenAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(new AccessToken("test-token", DateTimeOffset.MaxValue)); - var credentialProvider = Mock.Of( + IAzureCredentialProvider credentialProvider = Mock.Of( p => p.GetCredential(ServiceConnectionNames.Staging) == credential.Object); return new(new HttpClient(handler), credentialProvider, Mock.Of>()); } private static string CreateChecksums(string version) => - $"{s_alpineSha} *PowerShell.Linux.Alpine.{version}.nupkg\r\n" - + $"{s_linuxSha} *PowerShell.Linux.x64.{version}.nupkg\r\n" - + $"{s_windowsSha} *PowerShell.Windows.x64.{version}.nupkg\r\n"; + $"{AlpineSha} *PowerShell.Linux.Alpine.{version}.nupkg\r\n" + + $"{LinuxSha} *PowerShell.Linux.x64.{version}.nupkg\r\n" + + $"{WindowsSha} *PowerShell.Windows.x64.{version}.nupkg\r\n"; private sealed class StubHandler(Dictionary responses) : HttpMessageHandler { diff --git a/tests/UpdateDependencies.Tests/SyncInternalReleaseTests.cs b/tests/UpdateDependencies.Tests/SyncInternalReleaseTests.cs index 3c47e8a3cb..21c215393a 100644 --- a/tests/UpdateDependencies.Tests/SyncInternalReleaseTests.cs +++ b/tests/UpdateDependencies.Tests/SyncInternalReleaseTests.cs @@ -2,12 +2,12 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Immutable; +using Microsoft.DotNet.DarcLib; +using Microsoft.DotNet.Docker.Shared; using Microsoft.DotNet.Docker.UpdateDependencies; using Microsoft.DotNet.Docker.UpdateDependencies.Commands; using Microsoft.DotNet.Docker.UpdateDependencies.Git; using Microsoft.DotNet.Docker.UpdateDependencies.Sync; -using Microsoft.DotNet.DarcLib; -using Microsoft.DotNet.Docker.Shared; using Microsoft.Extensions.Logging; namespace UpdateDependencies.Tests; @@ -28,7 +28,7 @@ public sealed class SyncInternalReleaseTests /// The primary thing that changes the behavior of the command is the state of the remote git /// repository, which is mocked in the tests. /// - private static readonly SyncInternalReleaseOptions s_defaultOptions = new() + private static readonly SyncInternalReleaseOptions DefaultOptions = new() { SourceBranch = ReleaseBranch, TargetBranch = InternalReleaseBranch @@ -41,7 +41,7 @@ public sealed class SyncInternalReleaseTests [Fact] public async Task WhitespaceArgumentsFails() { - var configuration = new UpdateDependenciesConfiguration + UpdateDependenciesConfiguration configuration = new UpdateDependenciesConfiguration { AzureDevOps = new() { @@ -50,9 +50,9 @@ public async Task WhitespaceArgumentsFails() Repository = " ", }, }; - var options = s_defaultOptions with { SourceBranch = " ", TargetBranch = " " }; - var repoFactory = new Mock(MockBehavior.Strict); - var command = CreateCommand(repoFactory: repoFactory.Object, configuration: configuration); + SyncInternalReleaseOptions options = DefaultOptions with { SourceBranch = " ", TargetBranch = " " }; + Mock repoFactory = new Mock(MockBehavior.Strict); + SyncInternalReleaseCommand command = CreateCommand(repoFactory: repoFactory.Object, configuration: configuration); await Should.ThrowAsync(() => command.ExecuteAsync(options)); @@ -67,9 +67,9 @@ public async Task WhitespaceArgumentsFails() [Fact] public async Task InternalSourceBranchFails() { - var options = s_defaultOptions with { SourceBranch = "internal/foo" }; + SyncInternalReleaseOptions options = DefaultOptions with { SourceBranch = "internal/foo" }; - var command = CreateCommand(); + SyncInternalReleaseCommand command = CreateCommand(); await Should.ThrowAsync(() => command.ExecuteAsync(options)); } @@ -81,10 +81,10 @@ public async Task InternalSourceBranchFails() [Fact] public async Task CreateInternalBranch() { - var options = s_defaultOptions; + SyncInternalReleaseOptions options = DefaultOptions; - var repoMock = new Mock(); - var repoFactoryMock = new Mock(); + Mock repoMock = new Mock(); + Mock repoFactoryMock = new Mock(); repoFactoryMock.Setup(f => f.CreateAndCloneAsync(RemoteAzdoUrl, null, It.IsAny<(string, string)?>())).ReturnsAsync(repoMock.Object); // Setup: @@ -93,9 +93,9 @@ public async Task CreateInternalBranch() // Source branch exists on remote repoMock.Setup(r => r.Remote.RemoteBranchExistsAsync(options.SourceBranch)).ReturnsAsync(true); - var command = CreateCommand(repoFactory: repoFactoryMock.Object); + SyncInternalReleaseCommand command = CreateCommand(repoFactory: repoFactoryMock.Object); - var exitCode = await command.ExecuteAsync(options); + int exitCode = await command.ExecuteAsync(options); exitCode.ShouldBe(0); // The command should have created the target branch based off of the source branch. @@ -109,27 +109,27 @@ public async Task CreateInternalBranch() [Fact] public async Task AlreadyUpToDate() { - var options = s_defaultOptions; + SyncInternalReleaseOptions options = DefaultOptions; // Strict mock behavior ensures that no extra calls are made that are // not explicitly set up in this test. - var repoMock = new Mock(MockBehavior.Strict); - var repoFactoryMock = new Mock(); + Mock repoMock = new Mock(MockBehavior.Strict); + Mock repoFactoryMock = new Mock(); repoFactoryMock.Setup(f => f.CreateAndCloneAsync(RemoteAzdoUrl, null, It.IsAny<(string, string)?>())).ReturnsAsync(repoMock.Object); // Setup: Both target and source branches exist on remote. repoMock.Setup(r => r.Remote.RemoteBranchExistsAsync(options.TargetBranch)).ReturnsAsync(true); repoMock.Setup(r => r.Remote.RemoteBranchExistsAsync(options.SourceBranch)).ReturnsAsync(true); // They point to the same commit. - const string Sha = "0000000000000000000000000000000000000001"; - repoMock.Setup(r => r.Remote.GetRemoteBranchShaAsync(options.TargetBranch)).ReturnsAsync(Sha); - repoMock.Setup(r => r.Remote.GetRemoteBranchShaAsync(options.SourceBranch)).ReturnsAsync(Sha); + const string sha = "0000000000000000000000000000000000000001"; + repoMock.Setup(r => r.Remote.GetRemoteBranchShaAsync(options.TargetBranch)).ReturnsAsync(sha); + repoMock.Setup(r => r.Remote.GetRemoteBranchShaAsync(options.SourceBranch)).ReturnsAsync(sha); repoMock.Setup(r => r.Dispose()); - var command = CreateCommand(repoFactory: repoFactoryMock.Object); + SyncInternalReleaseCommand command = CreateCommand(repoFactory: repoFactoryMock.Object); // Command should succeed. - var exitCode = await command.ExecuteAsync(options); + int exitCode = await command.ExecuteAsync(options); exitCode.ShouldBe(0); // Command should not have done anything else that we didn't expect. repoMock.VerifyAll(); @@ -142,16 +142,16 @@ public async Task AlreadyUpToDate() [Fact] public async Task FastForward() { - var options = s_defaultOptions; + SyncInternalReleaseOptions options = DefaultOptions; - var localRepoMock = new Mock(); - var remoteRepoMock = new Mock(); + Mock localRepoMock = new Mock(); + Mock remoteRepoMock = new Mock(); - var repoMock = new Mock(); + Mock repoMock = new Mock(); repoMock.Setup(r => r.Local).Returns(localRepoMock.Object); repoMock.Setup(r => r.Remote).Returns(remoteRepoMock.Object); - var repoFactoryMock = new Mock(); + Mock repoFactoryMock = new Mock(); repoFactoryMock.Setup(f => f.CreateAndCloneAsync(RemoteAzdoUrl, null, It.IsAny<(string, string)?>())).ReturnsAsync(repoMock.Object); // Setup: Both target and source branches exist on remote. @@ -159,10 +159,10 @@ public async Task FastForward() repoMock.Setup(r => r.Remote.RemoteBranchExistsAsync(options.SourceBranch)).ReturnsAsync(true); // They point to different commits. - const string OldSha = "0000000000000000000000000000000000000001"; - const string NewSha = "0000000000000000000000000000000000000002"; - remoteRepoMock.Setup(r => r.GetRemoteBranchShaAsync(options.TargetBranch)).ReturnsAsync(OldSha); - remoteRepoMock.Setup(r => r.GetRemoteBranchShaAsync(options.SourceBranch)).ReturnsAsync(NewSha); + const string oldSha = "0000000000000000000000000000000000000001"; + const string newSha = "0000000000000000000000000000000000000002"; + remoteRepoMock.Setup(r => r.GetRemoteBranchShaAsync(options.TargetBranch)).ReturnsAsync(oldSha); + remoteRepoMock.Setup(r => r.GetRemoteBranchShaAsync(options.SourceBranch)).ReturnsAsync(newSha); // The target branch is behind the source branch. localRepoMock @@ -174,10 +174,10 @@ public async Task FastForward() .Setup(r => r.GetPullRequestInfoAsync(It.IsAny())) .ReturnsAsync(Mock.Of()); - var command = CreateCommand(repoFactory: repoFactoryMock.Object); + SyncInternalReleaseCommand command = CreateCommand(repoFactory: repoFactoryMock.Object); // Command should succeed. - var exitCode = await command.ExecuteAsync(options); + int exitCode = await command.ExecuteAsync(options); exitCode.ShouldBe(0); // Should have created a pull request. @@ -200,48 +200,48 @@ public async Task FastForward() [Fact] public async Task Sync() { - var options = s_defaultOptions with + SyncInternalReleaseOptions options = DefaultOptions with { StagingStorageAccount = "dotnetstage" }; // Target and source branches both exist, but at different commits, // and the target branch is not an ancestor of the source branch. - var gitScenario = new GitTestScenario(localRepoPath: LocalRepoPath, remoteRepoUrl: RemoteAzdoUrl) + GitTestScenario gitScenario = new GitTestScenario(localRepoPath: LocalRepoPath, remoteRepoUrl: RemoteAzdoUrl) .AddRemoteBranch(options.SourceBranch, "0000000000000000000000000000000000000001") .AddRemoteBranch(options.TargetBranch, "0000000000000000000000000000000000000002"); // Actual value of the pull request URL is not important here, just that we allow one to // be created and pretend it exists afterwards. - const string PullRequestUrl = nameof(PullRequestUrl); - var pullRequestIsCreated = false; + const string pullRequestUrl = nameof(pullRequestUrl); + bool pullRequestIsCreated = false; gitScenario.RepoMock .Setup(repo => repo.Remote.CreatePullRequestAsync(It.IsAny())) .Callback(() => pullRequestIsCreated = true) - .ReturnsAsync(PullRequestUrl); + .ReturnsAsync(pullRequestUrl); gitScenario.RepoMock - .Setup(repo => repo.Remote.GetPullRequestInfoAsync(PullRequestUrl)) + .Setup(repo => repo.Remote.GetPullRequestInfoAsync(pullRequestUrl)) .ReturnsAsync(() => pullRequestIsCreated ? Mock.Of() - : throw new Exception($"PR {PullRequestUrl} was not created first")); + : throw new Exception($"PR {pullRequestUrl} was not created first")); // For this scenario, two internal versions have been checked in to this repo. - var internalVersions = new Dictionary + Dictionary internalVersions = new Dictionary { { DotNetVersion.Parse("8.0"), "stage-8000000" }, { DotNetVersion.Parse("10.0"), "stage-1000000" } }; - var internalVersionsService = CreateInternalVersionsService(LocalRepoPath, internalVersions); + IInternalVersionsService internalVersionsService = CreateInternalVersionsService(LocalRepoPath, internalVersions); - var fromStagingPipelineCommandMock = new Mock>(); + Mock> fromStagingPipelineCommandMock = new Mock>(); - var syncCommand = CreateCommand( + SyncInternalReleaseCommand syncCommand = CreateCommand( repoFactory: gitScenario.RepoFactoryMock.Object, fromStagingPipelineCommand: fromStagingPipelineCommandMock.Object, internalVersionsService: internalVersionsService); // Run the command - var exitCode = await syncCommand.ExecuteAsync(options); + int exitCode = await syncCommand.ExecuteAsync(options); exitCode.ShouldBe(0); // Verify that we re-applied all internal versions by calling the downstream command. @@ -261,7 +261,7 @@ public async Task Sync() // There should be one commit for resetting the target branch to match // the source branch, and then one commit for each internal version // that was re-applied. - var numberOfCommits = 1 + internalVersions.Count; + int numberOfCommits = 1 + internalVersions.Count; gitScenario.RepoMock.Verify( repo => repo.Local.CommitAsync( It.IsAny(), @@ -299,16 +299,16 @@ private SyncInternalReleaseCommand CreateCommand( UpdateDependenciesConfiguration? configuration = null) => // New parameters should be null by default and initialized with mocks if not specified. new(configuration ?? new UpdateDependenciesConfiguration + { + User = "Test User", + Email = "test@example.com", + AzureDevOps = new() { - User = "Test User", - Email = "test@example.com", - AzureDevOps = new() - { - Organization = AzdoOrgUrl, - Project = AzdoProject, - Repository = AzdoRepo, - }, + Organization = AzdoOrgUrl, + Project = AzdoProject, + Repository = AzdoRepo, }, + }, repoFactory ?? Mock.Of(), fromStagingPipelineCommand ?? Mock.Of>(), internalVersionsService ?? Mock.Of(), @@ -321,9 +321,9 @@ private SyncInternalReleaseCommand CreateCommand( private static IInternalVersionsService CreateInternalVersionsService( string repoPath, Dictionary versions) { - var internalStagingBuilds = new InternalStageContainers(versions.ToImmutableDictionary()); + InternalStageContainers internalStagingBuilds = new InternalStageContainers(versions.ToImmutableDictionary()); - var mock = new Mock(); + Mock mock = new Mock(); mock.Setup(s => s.GetInternalStagingBuilds(repoPath)) .Returns(internalStagingBuilds); diff --git a/tests/UpdateDependencies.Tests/TempRepo.cs b/tests/UpdateDependencies.Tests/TempRepo.cs index dc139e466b..7c194c064a 100644 --- a/tests/UpdateDependencies.Tests/TempRepo.cs +++ b/tests/UpdateDependencies.Tests/TempRepo.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 Maestro.Common; using Maestro.Common.Telemetry; using Microsoft.DotNet.DarcLib; using Microsoft.DotNet.DarcLib.Helpers; @@ -20,15 +19,15 @@ public TempRepo() Directory.CreateDirectory(LocalPath); // Create a temp repo that can be used for testing commands using a real git process - var logger = Mock.Of(); - var processManager = new ProcessManager(logger, "git"); - var git = new LocalGitClient( + ILogger logger = Mock.Of(); + ProcessManager processManager = new ProcessManager(logger, "git"); + LocalGitClient git = new LocalGitClient( Mock.Of(), Mock.Of(), processManager, new FileSystem(), logger); - var localGitRepoFactory = new LocalGitRepoFactory(git, processManager); + LocalGitRepoFactory localGitRepoFactory = new LocalGitRepoFactory(git, processManager); LocalGitRepo = localGitRepoFactory.Create(new NativePath(LocalPath)); } diff --git a/tests/UpdateDependencies.Tests/VersionHelperTests.cs b/tests/UpdateDependencies.Tests/VersionHelperTests.cs index 90deb76a73..e74f8d13c6 100644 --- a/tests/UpdateDependencies.Tests/VersionHelperTests.cs +++ b/tests/UpdateDependencies.Tests/VersionHelperTests.cs @@ -12,8 +12,5 @@ public sealed class VersionHelperTests [InlineData("10.0.100", "9.0.100", "10.0.100")] [InlineData("11.0.100", "11.0.99", "11.0.100")] [InlineData("11.0.99-servicing.12345.1", "11.0.100-servicing.12345.1", "11.0.100-servicing.12345.1")] - public void GetHighestSdkVersion_UsesNumericOrder(string first, string second, string expected) - { - VersionHelper.GetHighestSdkVersion([first, second]).ShouldBe(expected); - } + public void GetHighestSdkVersion_UsesNumericOrder(string first, string second, string expected) => VersionHelper.GetHighestSdkVersion([first, second]).ShouldBe(expected); } From be5e66bccc8788c6c3feade0affa06de885dc801 Mon Sep 17 00:00:00 2001 From: Logan Bussell Date: Tue, 27 Oct 2026 18:25:28 -0700 Subject: [PATCH 2/2] Enable recommended .NET code analysis Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .editorconfig | 12 +++++++++ Directory.Build.props | 1 + eng/shared/DotNetVersion.cs | 7 ++++-- .../Microsoft.DotNet.Docker.Shared.csproj | 4 --- eng/update-dependencies/BuildAssetService.cs | 14 +++++------ eng/update-dependencies/ChecksumHelper.cs | 3 ++- .../Commands/FromStagingPipelineOptions.cs | 5 ++-- .../Commands/SyncInternalReleaseCommand.cs | 4 +-- .../Git/LocalGitRepoHelper.cs | 4 +-- .../Git/RemoteGitRepoFactory.cs | 2 +- .../PipelineArtifactProvider.cs | 6 ++--- .../Updaters/AspireUpdater.cs | 3 ++- .../Updaters/DotNetUpdater.cs | 2 +- .../Updaters/NuGetConfigUpdater.cs | 5 ++-- .../Updaters/PowerShellUpdater.cs | 5 ++-- eng/update-dependencies/VariableHelper.cs | 5 ++-- eng/update-dependencies/VersionHelper.cs | 4 ++- .../CommonRuntimeImageTests.cs | 5 ++-- tests/Microsoft.DotNet.Docker.Tests/Config.cs | 2 +- .../DockerHelper.cs | 2 +- .../DockerRunArgsBuilder.cs | 2 +- .../DockerfileInfo.cs | 2 +- .../DotNetTheoryDiscoverer.cs | 2 +- .../EnvironmentVariableInfo.cs | 8 +++--- .../ImageVersion.cs | 4 +-- .../ManifestHelper.cs | 3 +-- .../MonitorImageTests.cs | 9 ++++--- tests/Microsoft.DotNet.Docker.Tests/OSInfo.cs | 2 +- .../ProductImageData.cs | 4 +-- .../ProductImageTests.cs | 25 ++++++++++--------- .../SampleImageData.cs | 4 +-- .../SampleImageTests.cs | 7 +++--- .../SdkImageTests.cs | 11 ++++---- .../SkippableTheoryAttribute.cs | 2 +- .../StaticTagTests.cs | 8 +++--- .../SyftHelper.cs | 2 +- .../Microsoft.DotNet.Docker.Tests/TestData.cs | 2 +- .../ProjectTemplateTestScenario.cs | 4 +-- .../ReverseProxyBasicScenario.cs | 2 +- .../TestScenarios/WebScenario.cs | 4 +-- .../TestSolution.cs | 6 ++--- .../SyncInternalReleaseTests.cs | 6 ++--- 42 files changed, 118 insertions(+), 96 deletions(-) diff --git a/.editorconfig b/.editorconfig index d303bca2be..23c678bc5f 100644 --- a/.editorconfig +++ b/.editorconfig @@ -482,6 +482,18 @@ dotnet_naming_style.s_camelcase.required_suffix = dotnet_naming_style.s_camelcase.word_separator = dotnet_naming_style.s_camelcase.capitalization = camel_case +[eng/update-dependencies/**.cs] +# CA1848: Prefer LoggerMessage delegates over ILogger extension methods. +# CA1873: Guard potentially expensive logging arguments. +# Source-generated logging is overkill for this project. +dotnet_diagnostic.CA1848.severity = none +dotnet_diagnostic.CA1873.severity = none + +[tests/**.cs] +# CA1707: Remove underscores from identifier names. +# Keep descriptive test names and version/repository identifiers. +dotnet_diagnostic.CA1707.severity = none + [tests/Microsoft.DotNet.Docker.Tests/TestAppArtifacts/**.cs] # These are inputs to generated test applications, not part of the test runner. generated_code = true diff --git a/Directory.Build.props b/Directory.Build.props index 110ec78439..8ba22d031b 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,5 +1,6 @@ + Recommended true true diff --git a/eng/shared/DotNetVersion.cs b/eng/shared/DotNetVersion.cs index 501a6b4021..f6d3502be2 100644 --- a/eng/shared/DotNetVersion.cs +++ b/eng/shared/DotNetVersion.cs @@ -1,10 +1,13 @@ // 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 System.Text.RegularExpressions; using NuGet.Versioning; +#pragma warning disable CA1716 // Avoid language keywords in namespaces: this shared code is consumed only by C# projects. namespace Microsoft.DotNet.Docker.Shared; +#pragma warning restore CA1716 /// /// Represents the version of a .NET build artifact. @@ -42,8 +45,8 @@ public sealed partial class DotNetVersion(SemanticVersion version) : SemanticVer // Don't use System.Version.Parse here, because it defaults unmatched // minor/patch versions to -1, whereas we want them to default to 0. GroupCollection matchGroups = MajorMinorVersionRegex.Match(versionString).Groups; - int major = int.Parse(matchGroups["major"].Value); - int minor = int.TryParse(matchGroups["minor"].Value.TrimStart('.'), out int minorValue) ? minorValue : 0; + int major = int.Parse(matchGroups["major"].Value, CultureInfo.InvariantCulture); + int minor = int.TryParse(matchGroups["minor"].Value.TrimStart('.'), NumberStyles.Integer, CultureInfo.InvariantCulture, out int minorValue) ? minorValue : 0; return new DotNetVersion(new SemanticVersion(major, minor, 0)); } diff --git a/eng/shared/Microsoft.DotNet.Docker.Shared.csproj b/eng/shared/Microsoft.DotNet.Docker.Shared.csproj index a2d8181b00..a405a9b697 100644 --- a/eng/shared/Microsoft.DotNet.Docker.Shared.csproj +++ b/eng/shared/Microsoft.DotNet.Docker.Shared.csproj @@ -4,11 +4,7 @@ net10.0 enable enable - true Microsoft.DotNet.Docker.Shared - - - true diff --git a/eng/update-dependencies/BuildAssetService.cs b/eng/update-dependencies/BuildAssetService.cs index b544d5ee99..509010351a 100644 --- a/eng/update-dependencies/BuildAssetService.cs +++ b/eng/update-dependencies/BuildAssetService.cs @@ -22,14 +22,14 @@ public async Task GetAssetTextContentsAsync(Asset asset) string url = ResolveAssetUrl(asset); try { - _logger.LogInformation("Fetching contents from {url}", url); + _logger.LogInformation("Fetching contents from {Url}", url); HttpResponseMessage response = await _httpClient.GetAsync(url); response.EnsureSuccessStatusCode(); string content = await response.Content.ReadAsStringAsync(); _logger.LogInformation( """ - Successfully fetched contents from {url}: - {content} + Successfully fetched contents from {Url}: + {Content} """, url, content); // Remove trailing newlines @@ -37,7 +37,7 @@ public async Task GetAssetTextContentsAsync(Asset asset) } catch (Exception ex) { - _logger.LogError(ex, "Failed to fetch contents from {url}", url); + _logger.LogError(ex, "Failed to fetch contents from {Url}", url); throw; } } @@ -51,8 +51,8 @@ private string ResolveAssetUrl(Asset asset) string.Join(Environment.NewLine, asset.Locations.Select(l => $"Type: {l.Type}; Url: {l.Location}")); _logger.LogInformation( """ - Asset {asset.Name} has {asset.Locations.Count} locations: - {allLocations} + Asset {AssetName} has {LocationCount} locations: + {AllLocations} """, asset.Name, asset.Locations.Count, allLocations); @@ -63,7 +63,7 @@ private string ResolveAssetUrl(Asset asset) ?? throw new InvalidOperationException(FormatErrorMessage(asset, "does not have any valid locations")); string url = $"{bestLocation.Location}/{asset.Name}"; - _logger.LogInformation("Using location {url}", url); + _logger.LogInformation("Using location {Url}", url); return url; } diff --git a/eng/update-dependencies/ChecksumHelper.cs b/eng/update-dependencies/ChecksumHelper.cs index 92acdc4880..83b1ce268e 100644 --- a/eng/update-dependencies/ChecksumHelper.cs +++ b/eng/update-dependencies/ChecksumHelper.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using System.Globalization; using System.Security.Cryptography; using System.Text; @@ -27,7 +28,7 @@ internal static class ChecksumHelper StringBuilder stringBuilder = new(128); foreach (byte b in hashedInputBytes) { - stringBuilder.Append(b.ToString("X2")); + stringBuilder.Append(b.ToString("X2", CultureInfo.InvariantCulture)); } sha = stringBuilder.ToString(); diff --git a/eng/update-dependencies/Commands/FromStagingPipelineOptions.cs b/eng/update-dependencies/Commands/FromStagingPipelineOptions.cs index 5e73cd5467..83145a6c5e 100644 --- a/eng/update-dependencies/Commands/FromStagingPipelineOptions.cs +++ b/eng/update-dependencies/Commands/FromStagingPipelineOptions.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.CommandLine; +using System.Globalization; using System.Text.RegularExpressions; namespace Microsoft.DotNet.Docker.UpdateDependencies.Commands; @@ -20,7 +21,7 @@ internal partial record FromStagingPipelineOptions : CreatePullRequestOptions /// /// Whether or not to use the internal versions of the staged build. /// - public bool Internal { get; init; } = false; + public bool Internal { get; init; } /// /// This Azure Storage Account will be used as a source for the update. @@ -94,6 +95,6 @@ public static int GetStagingPipelineRunId(string stageContainer) $"Invalid stage container name '{stageContainer}'. Expected format: 'stage-{{buildId}}' (e.g., 'stage-1234567')"); } - return int.Parse(match.Groups[1].Value); + return int.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture); } } diff --git a/eng/update-dependencies/Commands/SyncInternalReleaseCommand.cs b/eng/update-dependencies/Commands/SyncInternalReleaseCommand.cs index 312c682c7a..8a9f4c78ef 100644 --- a/eng/update-dependencies/Commands/SyncInternalReleaseCommand.cs +++ b/eng/update-dependencies/Commands/SyncInternalReleaseCommand.cs @@ -85,9 +85,9 @@ await repo.Remote.CreateRemoteBranchAsync( } string sourceSha = await repo.Remote.GetRemoteBranchShaAsync(options.SourceBranch) - ?? throw new Exception("Failed to get source branch SHA even though branch exists on remote."); + ?? throw new InvalidOperationException("Failed to get source branch SHA even though branch exists on remote."); string targetSha = await repo.Remote.GetRemoteBranchShaAsync(options.TargetBranch) - ?? throw new Exception("Failed to get target branch SHA even though branch exists on remote."); + ?? throw new InvalidOperationException("Failed to get target branch SHA even though branch exists on remote."); // If both branches are at the same commit, then there is nothing to do. if (sourceSha == targetSha) diff --git a/eng/update-dependencies/Git/LocalGitRepoHelper.cs b/eng/update-dependencies/Git/LocalGitRepoHelper.cs index 0146457921..fe8540f877 100644 --- a/eng/update-dependencies/Git/LocalGitRepoHelper.cs +++ b/eng/update-dependencies/Git/LocalGitRepoHelper.cs @@ -61,7 +61,7 @@ public async Task CommitAsync(string message, (string Name, string Email _logger.LogInformation( """ Git status: - {gitStatus} + {GitStatus} """, gitStatus); @@ -69,7 +69,7 @@ public async Task CommitAsync(string message, (string Name, string Email string commitSha = await _localGitRepo.GetShaForRefAsync("HEAD"); _logger.LogInformation( - "Created commit {commitSha}, message: '{commitMessage}' ({authorName} <{authorEmail}>)", + "Created commit {CommitSha}, message: '{CommitMessage}' ({AuthorName} <{AuthorEmail}>)", commitSha, message, author.Name, author.Email); return commitSha; diff --git a/eng/update-dependencies/Git/RemoteGitRepoFactory.cs b/eng/update-dependencies/Git/RemoteGitRepoFactory.cs index 0716d4dfbb..ff44acf1d9 100644 --- a/eng/update-dependencies/Git/RemoteGitRepoFactory.cs +++ b/eng/update-dependencies/Git/RemoteGitRepoFactory.cs @@ -53,7 +53,7 @@ private static GitRemote ParseGitRemoteFromUrl(string repoUrl) { { Host: "github.com" } => GitRemote.GitHub, { Host: var host } when host is "dev.azure.com" - || host.EndsWith("visualstudio.com") => GitRemote.AzureDevOps, + || host.EndsWith("visualstudio.com", StringComparison.Ordinal) => GitRemote.AzureDevOps, _ => GitRemote.None, }; } diff --git a/eng/update-dependencies/PipelineArtifactProvider.cs b/eng/update-dependencies/PipelineArtifactProvider.cs index fdd91e9d24..08d36c0f97 100644 --- a/eng/update-dependencies/PipelineArtifactProvider.cs +++ b/eng/update-dependencies/PipelineArtifactProvider.cs @@ -108,7 +108,7 @@ public async Task GetArtifactTextContentAsync( foreach (PipelineArtifactFile pipelineArtifact in artifactsToTry) { _logger.LogInformation( - "Trying to get artifact from pipeline run {pipelineRunId} using {artifactFile}", + "Trying to get artifact from pipeline run {PipelineRunId} using {ArtifactFile}", pipelineRunId, pipelineArtifact); try @@ -127,7 +127,7 @@ public async Task GetArtifactTextContentAsync( $"/content?format=file&subPath=%2F{pipelineArtifact.SubPath}"); _logger.LogInformation( - "Downloading artifact {pipelineArtifact} from {downloadUrl}", + "Downloading artifact {PipelineArtifact} from {DownloadUrl}", pipelineArtifact, downloadUrl); using HttpResponseMessage response = await _azdoHttpClient.GetAsync(downloadUrl); response.EnsureSuccessStatusCode(); @@ -137,7 +137,7 @@ public async Task GetArtifactTextContentAsync( catch (Exception e) { // Log the exception and continue to the next artifact file - _logger.LogError(e, "Failed to retrieve {artifactFile}: {message}", pipelineArtifact, e.Message); + _logger.LogError(e, "Failed to retrieve {ArtifactFile}: {Message}", pipelineArtifact, e.Message); exceptions.Add(e); } } diff --git a/eng/update-dependencies/Updaters/AspireUpdater.cs b/eng/update-dependencies/Updaters/AspireUpdater.cs index e3dd48e064..d952a4f207 100644 --- a/eng/update-dependencies/Updaters/AspireUpdater.cs +++ b/eng/update-dependencies/Updaters/AspireUpdater.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.Globalization; using Microsoft.DotNet.DarcLib; using Microsoft.DotNet.ProductConstructionService.Client.Models; using Microsoft.Extensions.Logging; @@ -77,7 +78,7 @@ private async Task ApplyAsync( variables.SetValue("aspire-dashboard|product-version", productVersion); variables.SetValue("aspire-dashboard|fixed-tag", productVersion); variables.SetValue("aspire-dashboard|minor-tag", majorMinorVersion.ToString(2)); - variables.SetValue("aspire-dashboard|major-tag", majorMinorVersion.Major.ToString()); + variables.SetValue("aspire-dashboard|major-tag", majorMinorVersion.Major.ToString(CultureInfo.InvariantCulture)); foreach ((Asset? asset, string? checksum) in dashboardAssets.Zip(dashboardChecksums)) { diff --git a/eng/update-dependencies/Updaters/DotNetUpdater.cs b/eng/update-dependencies/Updaters/DotNetUpdater.cs index 2c6bc309c7..f7836d2035 100644 --- a/eng/update-dependencies/Updaters/DotNetUpdater.cs +++ b/eng/update-dependencies/Updaters/DotNetUpdater.cs @@ -91,7 +91,7 @@ private void ApplyProductVersions( ManifestVariables variables, string repoRoot, string dockerfileVersion, - IReadOnlyDictionary productVersions, + Dictionary productVersions, string internalBaseUrl) { NuGetConfigUpdater.Update(variables, repoRoot, dockerfileVersion, productVersions["sdk"], !string.IsNullOrEmpty(internalBaseUrl)); diff --git a/eng/update-dependencies/Updaters/NuGetConfigUpdater.cs b/eng/update-dependencies/Updaters/NuGetConfigUpdater.cs index 1cd9ade46a..ae5757fbde 100644 --- a/eng/update-dependencies/Updaters/NuGetConfigUpdater.cs +++ b/eng/update-dependencies/Updaters/NuGetConfigUpdater.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.Globalization; using System.Text; using System.Xml.Linq; using Microsoft.DotNet.Docker.Shared; @@ -130,7 +131,7 @@ private static void UpdatePackageSources( private static void RemoveAllInternalPackageSources(XElement packageSources) { IEnumerable elements = packageSources.Elements() - .Where(pkgSrc => pkgSrc.Attribute("key")?.Value.EndsWith(PkgSrcSuffix) == true) + .Where(pkgSrc => pkgSrc.Attribute("key")?.Value.EndsWith(PkgSrcSuffix, StringComparison.Ordinal) == true) .ToList(); foreach (XElement element in elements) { @@ -177,7 +178,7 @@ private static T GetOrCreateXObject(T? node, XContainer parent, Func creat return node; } - private class Utf8StringWriter(StringBuilder stringBuilder) : StringWriter(stringBuilder) + private class Utf8StringWriter(StringBuilder stringBuilder) : StringWriter(stringBuilder, CultureInfo.InvariantCulture) { public override Encoding Encoding => Encoding.UTF8; } diff --git a/eng/update-dependencies/Updaters/PowerShellUpdater.cs b/eng/update-dependencies/Updaters/PowerShellUpdater.cs index 1757b40e6b..8f5adb5d1c 100644 --- a/eng/update-dependencies/Updaters/PowerShellUpdater.cs +++ b/eng/update-dependencies/Updaters/PowerShellUpdater.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.Globalization; using System.Net; using Azure; using Azure.Core; @@ -304,13 +305,13 @@ SemanticVersion GetPrerelease(string label, int number) => [] => [new SemanticVersion(version.Major, version.Minor, version.Patch + 1)], ["preview", var number] => [ - GetPrerelease("preview", int.Parse(number) + 1), + GetPrerelease("preview", int.Parse(number, CultureInfo.InvariantCulture) + 1), GetPrerelease("rc", 1), stableRelease ], ["rc", var number] => [ - GetPrerelease("rc", int.Parse(number) + 1), + GetPrerelease("rc", int.Parse(number, CultureInfo.InvariantCulture) + 1), stableRelease ], _ => throw new FormatException($"Unexpected PowerShell version '{version}'."), diff --git a/eng/update-dependencies/VariableHelper.cs b/eng/update-dependencies/VariableHelper.cs index cec92644b4..7b1bceb6f5 100644 --- a/eng/update-dependencies/VariableHelper.cs +++ b/eng/update-dependencies/VariableHelper.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.Globalization; using System.Text.RegularExpressions; namespace Microsoft.DotNet.Docker.UpdateDependencies; @@ -19,8 +20,8 @@ public static Regex GetValueRegex(params string[] variableNames) { string variableGroupFormat = $"(?<{VariableGroupName}>({{0}}))"; string joinedVariableNames = string.Join('|', variableNames); - string upperCasedVariableGroup = string.Format(variableGroupFormat, joinedVariableNames.ToUpperInvariant()); - string lowerCasedVariableGroup = string.Format(variableGroupFormat, joinedVariableNames.ToLowerInvariant()); + string upperCasedVariableGroup = string.Format(CultureInfo.InvariantCulture, variableGroupFormat, joinedVariableNames.ToUpperInvariant()); + string lowerCasedVariableGroup = string.Format(CultureInfo.InvariantCulture, variableGroupFormat, joinedVariableNames.ToLowerInvariant()); string argEnvPattern = $" {upperCasedVariableGroup}[= ](?<{ValueGroupName}>[^\\s$]+)"; string linuxPattern = $"\\b{lowerCasedVariableGroup}=(?<{ValueGroupName}>[^\\s]+)"; diff --git a/eng/update-dependencies/VersionHelper.cs b/eng/update-dependencies/VersionHelper.cs index 684398d5a0..6c20a92088 100644 --- a/eng/update-dependencies/VersionHelper.cs +++ b/eng/update-dependencies/VersionHelper.cs @@ -1,6 +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 System.Globalization; + namespace Microsoft.DotNet.Docker.UpdateDependencies; internal static class VersionHelper @@ -13,7 +15,7 @@ public static Version ResolveMajorMinorVersion(string versionString) throw new InvalidOperationException($"Could not parse major-minor version from '{versionString}'."); } - return new Version(major: int.Parse(versionParts[0]), minor: int.Parse(versionParts[1])); + return new Version(major: int.Parse(versionParts[0], CultureInfo.InvariantCulture), minor: int.Parse(versionParts[1], CultureInfo.InvariantCulture)); } /// diff --git a/tests/Microsoft.DotNet.Docker.Tests/CommonRuntimeImageTests.cs b/tests/Microsoft.DotNet.Docker.Tests/CommonRuntimeImageTests.cs index 99ed56bf18..0cacd779b3 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/CommonRuntimeImageTests.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/CommonRuntimeImageTests.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Threading.Tasks; using Microsoft.DotNet.Docker.Tests.TestScenarios; @@ -28,10 +29,10 @@ protected void VerifyCommonEnvironmentVariables( if (!imageData.IsWindows) { - variables.Add(EnvironmentVariableInfo.Require("APP_UID", imageData.NonRootUID?.ToString())); + variables.Add(EnvironmentVariableInfo.Require("APP_UID", imageData.NonRootUID?.ToString(CultureInfo.InvariantCulture))); } - variables.Add(EnvironmentVariableInfo.Require("ASPNETCORE_HTTP_PORTS", imageData.DefaultPort.ToString())); + variables.Add(EnvironmentVariableInfo.Require("ASPNETCORE_HTTP_PORTS", imageData.DefaultPort.ToString(CultureInfo.InvariantCulture))); if (customVariables != null) { diff --git a/tests/Microsoft.DotNet.Docker.Tests/Config.cs b/tests/Microsoft.DotNet.Docker.Tests/Config.cs index 6e4cb720fd..bf6b6dfab1 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/Config.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/Config.cs @@ -112,7 +112,7 @@ public static string GetBuildVersion(DotNetImageRepo imageRepo, string dotnetVer } return GetVariableValue( - $"{imageRepo.ToString().ToLower()}|{dotnetVersion}|build-version", + $"{imageRepo.ToString().ToLowerInvariant()}|{dotnetVersion}|build-version", (JObject)ManifestVersions.Value["variables"]); } diff --git a/tests/Microsoft.DotNet.Docker.Tests/DockerHelper.cs b/tests/Microsoft.DotNet.Docker.Tests/DockerHelper.cs index df16592363..10f7542516 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/DockerHelper.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/DockerHelper.cs @@ -271,7 +271,7 @@ public string GetContainerHostPort(string container, int containerPort = 80) => ExecuteWithLogging( $"inspect -f \"{{{{(index (index .NetworkSettings.Ports \\\"{containerPort}/tcp\\\") 0).HostPort}}}}\" {container}"); - public string GetContainerWorkPath(string relativePath) + public static string GetContainerWorkPath(string relativePath) { string separator = IsLinuxContainerModeEnabled ? "/" : "\\"; return $"{ContainerWorkDir}{separator}{relativePath}"; diff --git a/tests/Microsoft.DotNet.Docker.Tests/DockerRunArgsBuilder.cs b/tests/Microsoft.DotNet.Docker.Tests/DockerRunArgsBuilder.cs index 85e368d2e0..1277ab3876 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/DockerRunArgsBuilder.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/DockerRunArgsBuilder.cs @@ -6,7 +6,7 @@ namespace Microsoft.DotNet.Docker.Tests; -internal class DockerRunArgsBuilder +internal sealed class DockerRunArgsBuilder { private readonly StringBuilder _builder = new StringBuilder(); diff --git a/tests/Microsoft.DotNet.Docker.Tests/DockerfileInfo.cs b/tests/Microsoft.DotNet.Docker.Tests/DockerfileInfo.cs index 087e02af88..2279f1332c 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/DockerfileInfo.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/DockerfileInfo.cs @@ -62,7 +62,7 @@ public static DockerfileInfo Create(string dockerfilePath, Repo repo, Image imag Match match = DockerfileRegex.Match(dockerfilePath); if (!match.Success) { - throw new Exception($"Failed to parse dockerfile: {dockerfilePath}"); + throw new FormatException($"Failed to parse dockerfile: {dockerfilePath}"); } return new DockerfileInfo( diff --git a/tests/Microsoft.DotNet.Docker.Tests/DotNetTheoryDiscoverer.cs b/tests/Microsoft.DotNet.Docker.Tests/DotNetTheoryDiscoverer.cs index debdc290bd..dfae7b3877 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/DotNetTheoryDiscoverer.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/DotNetTheoryDiscoverer.cs @@ -17,7 +17,7 @@ public override IEnumerable Discover(ITestFrameworkDiscoveryOpti if (results.Count() == 1 && results.First() is ExecutionErrorTestCase errorTestCase && - errorTestCase.ErrorMessage.StartsWith("No data found for")) + errorTestCase.ErrorMessage.StartsWith("No data found for", StringComparison.Ordinal)) { return CreateTestCasesForSkippedDataRow(discoveryOptions, testMethod, theoryAttribute, [], errorTestCase.ErrorMessage); } diff --git a/tests/Microsoft.DotNet.Docker.Tests/EnvironmentVariableInfo.cs b/tests/Microsoft.DotNet.Docker.Tests/EnvironmentVariableInfo.cs index 6926fed0c7..3853abd53b 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/EnvironmentVariableInfo.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/EnvironmentVariableInfo.cs @@ -10,15 +10,15 @@ namespace Microsoft.DotNet.Docker.Tests; public record EnvironmentVariableInfo { - public bool AllowAnyValue { get; init; } = false; - public string? ExpectedValue { get; init; } = null; + public bool AllowAnyValue { get; init; } + public string? ExpectedValue { get; init; } public string Name { get; init; } - public bool IsProductVersion { get; init; } = false; + public bool IsProductVersion { get; init; } /// /// When true, the variable is expected to NOT be set on the image. /// - public bool ShouldNotExist { get; init; } = false; + public bool ShouldNotExist { get; init; } private EnvironmentVariableInfo(string name) => Name = name; diff --git a/tests/Microsoft.DotNet.Docker.Tests/ImageVersion.cs b/tests/Microsoft.DotNet.Docker.Tests/ImageVersion.cs index 30943834b8..fa241098e8 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/ImageVersion.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/ImageVersion.cs @@ -34,13 +34,13 @@ public ImageVersion(Version version, bool isPreview) public static string TrimBuildVersionForRelease(string buildVersion) { - int servicingIndex = buildVersion.IndexOf("-servicing."); + int servicingIndex = buildVersion.IndexOf("-servicing.", StringComparison.Ordinal); if (servicingIndex != -1) { buildVersion = buildVersion[..servicingIndex]; } - int rtmIndex = buildVersion.IndexOf("-rtm."); + int rtmIndex = buildVersion.IndexOf("-rtm.", StringComparison.Ordinal); if (rtmIndex != -1) { buildVersion = buildVersion[..rtmIndex]; diff --git a/tests/Microsoft.DotNet.Docker.Tests/ManifestHelper.cs b/tests/Microsoft.DotNet.Docker.Tests/ManifestHelper.cs index 824cce653a..398cbc21bd 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/ManifestHelper.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/ManifestHelper.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; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; @@ -15,7 +14,7 @@ public static class ManifestHelper { public static Manifest GetManifest() => JsonConvert.DeserializeObject(Config.Manifest.Value.ToString()) ?? - throw new Exception("Failed to deserialize manifest"); + throw new JsonSerializationException("Failed to deserialize manifest"); public static string GetResolvedProductVersion(Image image) => GetVariableValue(image.ProductVersion); diff --git a/tests/Microsoft.DotNet.Docker.Tests/MonitorImageTests.cs b/tests/Microsoft.DotNet.Docker.Tests/MonitorImageTests.cs index a271646426..2d47bd56ad 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/MonitorImageTests.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/MonitorImageTests.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Globalization; using System.IO; using System.Linq; using System.Net; @@ -580,12 +581,12 @@ private static string GetMonitorAdditionalArgs(bool noAuthentication) if (addDefaultArgs) { - builtCommandline.AppendFormat("{0} ", Switch_DefaultImageCmd); + builtCommandline.AppendFormat(CultureInfo.InvariantCulture, "{0} ", Switch_DefaultImageCmd); } if (noAuthentication) { - builtCommandline.AppendFormat("{0} ", Switch_NoAuthentication); + builtCommandline.AppendFormat(CultureInfo.InvariantCulture, "{0} ", Switch_NoAuthentication); } string cmdsResult = builtCommandline.ToString().Trim(spaceChar); @@ -605,14 +606,14 @@ private void GetNames(SampleImageData imageData, out string imageName, out strin { // Need to allow pulling of the sample image since these are not built in the same pipeline // as the other images; otherwise, these tests will fail due to lack of sample image. - string tag = imageData.GetTagNameBase(SampleImageType.Aspnetapp); + string tag = SampleImageData.GetTagNameBase(SampleImageType.Aspnetapp); imageName = imageData.GetImage(tag, DockerHelper, allowPull: true); containerName = imageData.GetIdentifier("monitortest-sample"); // If user is empty, then image is running as same as docker daemon (typically root) userIdentifier = string.IsNullOrEmpty(DockerHelper.GetImageUser(imageName)) ? 0 : imageData.NonRootUID.Value; } - private void VerifyStatusCode(HttpResponseMessage message, HttpStatusCode statusCode) + private static void VerifyStatusCode(HttpResponseMessage message, HttpStatusCode statusCode) { if (message.StatusCode != statusCode) { diff --git a/tests/Microsoft.DotNet.Docker.Tests/OSInfo.cs b/tests/Microsoft.DotNet.Docker.Tests/OSInfo.cs index 5b793085d8..682fa6e4e2 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/OSInfo.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/OSInfo.cs @@ -124,7 +124,7 @@ public string DisplayName /// Checks if the TagName starts with the specified value. /// Provides backward compatibility with string-based OS checks. /// - public bool StartsWith(string value) => TagName.StartsWith(value); + public bool StartsWith(string value) => TagName.StartsWith(value, StringComparison.Ordinal); /// /// Implicit conversion to string for backward compatibility with existing code. diff --git a/tests/Microsoft.DotNet.Docker.Tests/ProductImageData.cs b/tests/Microsoft.DotNet.Docker.Tests/ProductImageData.cs index 151a3e5e9f..9492d7cba1 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/ProductImageData.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/ProductImageData.cs @@ -111,11 +111,11 @@ private string GetVariantSuffix() => public static string GetImageVariantName(DotNetImageVariant imageVariant) { IEnumerable imageVariants = []; - foreach (DotNetImageVariant enumValue in Enum.GetValues(typeof(DotNetImageVariant))) + foreach (DotNetImageVariant enumValue in Enum.GetValues()) { if (enumValue != DotNetImageVariant.None && imageVariant.HasFlag(enumValue)) { - imageVariants = imageVariants.Append(Enum.GetName(typeof(DotNetImageVariant), enumValue).ToLowerInvariant()); + imageVariants = imageVariants.Append(Enum.GetName(enumValue).ToLowerInvariant()); } } diff --git a/tests/Microsoft.DotNet.Docker.Tests/ProductImageTests.cs b/tests/Microsoft.DotNet.Docker.Tests/ProductImageTests.cs index 7fa243b705..690137761d 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/ProductImageTests.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/ProductImageTests.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Text.Json.Nodes; using System.Text.RegularExpressions; @@ -80,7 +81,7 @@ protected void VerifyCommonInsecureFiles(ProductImageData imageData) ); string rootFsPathWithTrailingSlash = rootFsPath; - if (!rootFsPathWithTrailingSlash.EndsWith("/")) + if (!rootFsPathWithTrailingSlash.EndsWith('/')) { rootFsPathWithTrailingSlash += "/"; } @@ -89,7 +90,7 @@ protected void VerifyCommonInsecureFiles(ProductImageData imageData) .ReplaceLineEndings() .Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries) // Writable files in the /tmp/.dotnet directory are allowed for global mutexes - .Where(line => !line.StartsWith($"{rootFsPathWithTrailingSlash}tmp/.dotnet")) + .Where(line => !line.StartsWith($"{rootFsPathWithTrailingSlash}tmp/.dotnet", StringComparison.Ordinal)) // Exclude the non-root user's home directory. It will show up as a directory without a user or group // because we're not examining the distroless container directly, but rather copying its contents into // a container with a shell but doesn't have the non-root user defined. @@ -104,7 +105,7 @@ protected void VerifyCommonDefaultUser(ProductImageData imageData) string actualUser = DockerHelper.GetImageUser(imageTag); string expectedUser = imageData.IsDistroless && ImageRepo != DotNetImageRepo.SDK - ? imageData.NonRootUID.ToString() + ? imageData.NonRootUID?.ToString(CultureInfo.InvariantCulture) ?? string.Empty // For Windows, only Nano Server defines a user, which seems wrong. // I've logged https://dev.azure.com/microsoft/OS/_workitems/edit/40146885 for this. : imageData.OS.Family == OSFamily.NanoServer ? "ContainerUser" : string.Empty; @@ -139,7 +140,7 @@ protected void VerifyNonRootUID(ProductImageData imageData) optionalRunArgs: $"--entrypoint /bin/sh" ); - int uid = int.Parse(uidString); + int uid = int.Parse(uidString, CultureInfo.InvariantCulture); // UIDs below 1000 are reserved for system accounts Assert.True(uid >= 1000); @@ -251,10 +252,10 @@ internal static IEnumerable GetExpectedPackages(ProductImageData imageDa return expectedPackages.Distinct().OrderBy(s => s); } - private static IEnumerable GetDistrolessBasePackages(ProductImageData imageData) => imageData switch + private static string[] GetDistrolessBasePackages(ProductImageData imageData) => imageData switch { - { OS.Family: OSFamily.AzureLinux } => new[] - { + { OS.Family: OSFamily.AzureLinux } => + [ "SymCrypt", "SymCrypt-OpenSSL", "azurelinux-release", @@ -262,7 +263,7 @@ internal static IEnumerable GetExpectedPackages(ProductImageData imageDa "filesystem", "prebuilt-ca-certificates", "tzdata" - }, + ], { OS.Family: OSFamily.Ubuntu } => [ "base-files" @@ -380,13 +381,13 @@ private static string GetZLibPackage(OSInfo os) private static IEnumerable GetResoluteChiseledArchSpecificPackages(ProductImageData imageData) => imageData.Arch == Arch.Amd64 ? ["gcc-16"] : []; - private static IEnumerable GetExtraPackages(ProductImageData imageData) => imageData switch + private static string[] GetExtraPackages(ProductImageData imageData) => imageData switch { - { IsDistroless: true, OS.Family: OSFamily.AzureLinux } => new[] - { + { IsDistroless: true, OS.Family: OSFamily.AzureLinux } => + [ "icu", "tzdata" - }, + ], { OS: var os } when os == OS.ResoluteChiseled => [ "icu", diff --git a/tests/Microsoft.DotNet.Docker.Tests/SampleImageData.cs b/tests/Microsoft.DotNet.Docker.Tests/SampleImageData.cs index 5d127cfb83..9be7b9d4ee 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/SampleImageData.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/SampleImageData.cs @@ -40,8 +40,8 @@ public string GetImage(string tag, DockerHelper dockerHelper, bool allowPull = f return imageName; } - public string GetTagNameBase(SampleImageType imageType) => - Enum.GetName(typeof(SampleImageType), imageType).ToLowerInvariant(); + public static string GetTagNameBase(SampleImageType imageType) => + Enum.GetName(imageType).ToLowerInvariant(); public static string GetImageName(string tag) => GetImageName(tag, "samples", string.Empty); diff --git a/tests/Microsoft.DotNet.Docker.Tests/SampleImageTests.cs b/tests/Microsoft.DotNet.Docker.Tests/SampleImageTests.cs index baf618fde6..7b3f00aed9 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/SampleImageTests.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/SampleImageTests.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Threading.Tasks; @@ -40,7 +41,7 @@ public async Task VerifyDotnetSample(SampleImageData imageData) await VerifySampleAsync(imageData, SampleImageType.Dotnetapp, (image, containerName) => { string output = DockerHelper.Run(image, containerName); - Assert.True(output.Contains("42") || output.StartsWith("Hello")); + Assert.True(output.Contains("42") || output.StartsWith("Hello", StringComparison.Ordinal)); ValidateEnvironmentVariables(imageData, image, SampleImageType.Dotnetapp); @@ -139,7 +140,7 @@ private async Task VerifySampleAsync( Func verifyImageAsync) { string image = imageData.GetImage(sampleImageType, DockerHelper); - string imageType = Enum.GetName(typeof(SampleImageType), sampleImageType).ToLowerInvariant(); + string imageType = Enum.GetName(sampleImageType).ToLowerInvariant(); try { if (!imageData.IsPublished) @@ -178,7 +179,7 @@ private void ValidateEnvironmentVariables(SampleImageData imageData, string imag if (imageType == SampleImageType.Aspnetapp) { - variables.Add(EnvironmentVariableInfo.Require("ASPNETCORE_HTTP_PORTS", imageData.DefaultPort.ToString())); + variables.Add(EnvironmentVariableInfo.Require("ASPNETCORE_HTTP_PORTS", imageData.DefaultPort.ToString(CultureInfo.InvariantCulture))); } EnvironmentVariableInfo.Validate( diff --git a/tests/Microsoft.DotNet.Docker.Tests/SdkImageTests.cs b/tests/Microsoft.DotNet.Docker.Tests/SdkImageTests.cs index d5dd8ec13c..802cbfd222 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/SdkImageTests.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/SdkImageTests.cs @@ -246,7 +246,7 @@ private IEnumerable GetActualSdkContents(ProductImageData im } string baseImage = imageData.GetImage(ImageRepo, DockerHelper); - string tag = imageData.GetIdentifier("SdkContents").ToLower(); + string tag = imageData.GetIdentifier("SdkContents").ToLowerInvariant(); DockerHelper.Build( tag: tag, @@ -377,7 +377,7 @@ private string GetSdkUrl(ProductImageData imageData) return url; } - private record SdkContentFileInfo + private sealed record SdkContentFileInfo { public string Path { get; init; } public string Sha512 { get; init; } @@ -385,7 +385,7 @@ private record SdkContentFileInfo public SdkContentFileInfo(string path, string sha512) { Path = NormalizePath(path); - Sha512 = sha512.ToLower(); + Sha512 = sha512.ToLowerInvariant(); } private static string NormalizePath(string path) @@ -395,7 +395,7 @@ private static string NormalizePath(string path) .Replace("/usr/share/dotnet", string.Empty, StringComparison.OrdinalIgnoreCase) .Replace("c:/program files/dotnet", string.Empty, StringComparison.OrdinalIgnoreCase) .TrimStart('/') - .ToLower(); + .ToLowerInvariant(); } } @@ -407,8 +407,7 @@ private static HttpClient CreateHttpClient() { client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( "Basic", - Convert.ToBase64String(Encoding.ASCII.GetBytes(string.Format("{0}:{1}", "", - Config.InternalAccessToken)))); + Convert.ToBase64String(Encoding.ASCII.GetBytes($":{Config.InternalAccessToken}"))); } ; diff --git a/tests/Microsoft.DotNet.Docker.Tests/SkippableTheoryAttribute.cs b/tests/Microsoft.DotNet.Docker.Tests/SkippableTheoryAttribute.cs index 85efedc2ad..d92ab2eb68 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/SkippableTheoryAttribute.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/SkippableTheoryAttribute.cs @@ -10,7 +10,7 @@ public class SkippableTheoryAttribute : DotNetTheoryAttribute { public SkippableTheoryAttribute(params string[] skippedOperatingSystems) { - if (Config.OsNames.Any() && !Config.OsNames.Any(osName => osName != "*")) + if (Config.OsNames.Length != 0 && !Config.OsNames.Any(osName => osName != "*")) { foreach (string osName in Config.OsNames) { diff --git a/tests/Microsoft.DotNet.Docker.Tests/StaticTagTests.cs b/tests/Microsoft.DotNet.Docker.Tests/StaticTagTests.cs index 60f6c7f501..b73e828353 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/StaticTagTests.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/StaticTagTests.cs @@ -145,9 +145,9 @@ public void PlatformTag_TagExists( // (which only .NET Monitor had at the time) and the update from CBL-Mariner // to Azure Linux. Rewrite the OS to match CBL-Mariner in this instance. string os = dockerfileInfo.OsDir; - if (repo.Name.EndsWith("monitor") && - dockerfileInfo.VersionDir.StartsWith("8.") && - OS.AzureLinuxDistroless.Equals(os)) + if (repo.Name.EndsWith("monitor", StringComparison.Ordinal) && + dockerfileInfo.VersionDir.StartsWith("8.", StringComparison.Ordinal) && + string.Equals(OS.AzureLinuxDistroless, os, StringComparison.Ordinal)) { os = OS.MarinerDistroless; } @@ -279,7 +279,7 @@ public void VersionTag_SameOsAndVersion(Repo repo, VersionType versionType) } } - private static IEnumerable GetTagTestObjects(TestType testType) + private static object[][] GetTagTestObjects(TestType testType) { List testObjects = []; foreach (Repo repo in GetManifest().Repos) diff --git a/tests/Microsoft.DotNet.Docker.Tests/SyftHelper.cs b/tests/Microsoft.DotNet.Docker.Tests/SyftHelper.cs index 52f85cb9a3..575be1ce5d 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/SyftHelper.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/SyftHelper.cs @@ -120,7 +120,7 @@ IEnumerable excludePaths { // Syft expects relative paths. The paths are relative to the scan directory, which in // this case is /rootfs/. - if (!path.StartsWith("./")) + if (!path.StartsWith("./", StringComparison.Ordinal)) { path = "./" + path.TrimStart('/'); } diff --git a/tests/Microsoft.DotNet.Docker.Tests/TestData.cs b/tests/Microsoft.DotNet.Docker.Tests/TestData.cs index b8acaf2eb2..8efbf5397d 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/TestData.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/TestData.cs @@ -577,7 +577,7 @@ public static IEnumerable FilterImagesByArch(this IEnumerable archFilterPattern == null - || Regex.IsMatch(Enum.GetName(typeof(Arch), imageData.Arch), archFilterPattern, RegexOptions.IgnoreCase)); + || Regex.IsMatch(Enum.GetName(imageData.Arch), archFilterPattern, RegexOptions.IgnoreCase)); } public static IEnumerable FilterImagesByOs(this IEnumerable imageData) diff --git a/tests/Microsoft.DotNet.Docker.Tests/TestScenarios/ProjectTemplateTestScenario.cs b/tests/Microsoft.DotNet.Docker.Tests/TestScenarios/ProjectTemplateTestScenario.cs index 4e840f837c..eb1e239506 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/TestScenarios/ProjectTemplateTestScenario.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/TestScenarios/ProjectTemplateTestScenario.cs @@ -26,8 +26,8 @@ public abstract class ProjectTemplateTestScenario( protected virtual bool NonRootUserSupported => DockerHelper.IsLinuxContainerModeEnabled; - protected virtual bool InjectCustomTestCode { get; } = false; - protected virtual bool OutputIsStatic { get; } = false; + protected virtual bool InjectCustomTestCode { get; } + protected virtual bool OutputIsStatic { get; } protected virtual string[] CustomDockerBuildArgs { get; } = []; protected abstract string SampleName { get; } diff --git a/tests/Microsoft.DotNet.Docker.Tests/TestScenarios/ReverseProxyBasicScenario.cs b/tests/Microsoft.DotNet.Docker.Tests/TestScenarios/ReverseProxyBasicScenario.cs index aa4c438c96..f18df7c255 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/TestScenarios/ReverseProxyBasicScenario.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/TestScenarios/ReverseProxyBasicScenario.cs @@ -179,7 +179,7 @@ public async Task ExecuteAsync() """; // Needs to be kept in sync with {RepoRoot}\tests\Microsoft.DotNet.Docker.Tests\TestAppArtifacts\otlptestlistener\OtlpTestListener\DataModel\TelemetryResults.cs - private class TelemetryResults + private sealed class TelemetryResults { public int SpanIdCount { get; set; } public int LogMessageCount { get; set; } diff --git a/tests/Microsoft.DotNet.Docker.Tests/TestScenarios/WebScenario.cs b/tests/Microsoft.DotNet.Docker.Tests/TestScenarios/WebScenario.cs index 0a2a0d4989..ad34f8f26d 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/TestScenarios/WebScenario.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/TestScenarios/WebScenario.cs @@ -16,9 +16,9 @@ namespace Microsoft.DotNet.Docker.Tests.TestScenarios; public abstract class WebScenario(ProductImageData imageData, DockerHelper dockerHelper, ITestOutputHelper outputHelper) : ConsoleAppScenario(imageData, dockerHelper, outputHelper) { - protected virtual int? PortOverride { get; } = null; + protected virtual int? PortOverride { get; } - protected virtual string? Endpoint { get; } = null; + protected virtual string? Endpoint { get; } protected override string SampleName { get; } = "web"; diff --git a/tests/Microsoft.DotNet.Docker.Tests/TestSolution.cs b/tests/Microsoft.DotNet.Docker.Tests/TestSolution.cs index 6b3f893f97..6245677a06 100644 --- a/tests/Microsoft.DotNet.Docker.Tests/TestSolution.cs +++ b/tests/Microsoft.DotNet.Docker.Tests/TestSolution.cs @@ -16,7 +16,7 @@ namespace Microsoft.DotNet.Docker.Tests; public class TestSolution : IDisposable { - private bool _disposed = false; + private bool _disposed; private readonly DockerHelper _dockerHelper; private readonly ProductImageData _imageData; private readonly string _appName = "app"; @@ -92,7 +92,7 @@ private string CreateTestSolutionWithSdkImage(string solutionDir, string appType return solutionDir; } - private void InjectCustomTestCode(string appDir) + private static void InjectCustomTestCode(string appDir) { string programFilePath = Path.Combine(appDir, "Program.cs"); @@ -156,7 +156,7 @@ private void InjectCustomTestCode(string appDir) File.WriteAllText(programFilePath, newContent); } - private string GetUserDirectoryWriteAccessValidationCode() + private static string GetUserDirectoryWriteAccessValidationCode() { string userDirEnvVarName = DockerHelper.IsLinuxContainerModeEnabled ? "HOME" : "USERPROFILE"; return diff --git a/tests/UpdateDependencies.Tests/SyncInternalReleaseTests.cs b/tests/UpdateDependencies.Tests/SyncInternalReleaseTests.cs index 21c215393a..795b5b9ec4 100644 --- a/tests/UpdateDependencies.Tests/SyncInternalReleaseTests.cs +++ b/tests/UpdateDependencies.Tests/SyncInternalReleaseTests.cs @@ -223,7 +223,7 @@ public async Task Sync() .Setup(repo => repo.Remote.GetPullRequestInfoAsync(pullRequestUrl)) .ReturnsAsync(() => pullRequestIsCreated ? Mock.Of() - : throw new Exception($"PR {pullRequestUrl} was not created first")); + : throw new InvalidOperationException($"PR {pullRequestUrl} was not created first")); // For this scenario, two internal versions have been checked in to this repo. Dictionary internalVersions = new Dictionary @@ -290,7 +290,7 @@ public async Task Sync() /// Helper method to create a instance with optional /// mocked dependencies. If dependencies are not provided, default mocks will be used. /// - private SyncInternalReleaseCommand CreateCommand( + private static SyncInternalReleaseCommand CreateCommand( IGitRepoHelperFactory? repoFactory = null, ICommand? fromStagingPipelineCommand = null, IInternalVersionsService? internalVersionsService = null, @@ -333,7 +333,7 @@ private static IInternalVersionsService CreateInternalVersionsService( /// /// Helper class for setting up scenarios on a mock. /// - private class GitTestScenario + private sealed class GitTestScenario { public Mock RepoMock { get; } = new();