Skip to content

Simplify AzureProvisioner and make it testable by removing unnecessary abstraction layers #9737

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 33 commits into from
Jun 11, 2025

Conversation

Copilot
Copy link
Contributor

@Copilot Copilot AI commented Jun 6, 2025

This PR simplifies the AzureProvisioner by removing unnecessary abstraction layers that were built for multiple provisioners but only had one implementation (BicepProvisioner). The changes make the code more maintainable and testable while preserving all existing functionality.

Changes Made

Removed Unnecessary Abstractions

  • Deleted IAzureResourceProvisioner interface - No longer needed since only one implementation existed
  • Deleted AzureResourceProvisioner<T> base class - Unnecessary generic abstraction
  • Deleted BicepProvisioner class - Integrated its functionality directly into AzureProvisioner
  • Removed provisioner selection logic - The SelectProvisioner method that always returned BicepProvisioner
  • Removed AddAzureProvisioner<TResource, TProvisioner> extension method - No longer needed

Integrated BicepProvisioner into AzureProvisioner

  • Moved all Bicep provisioning logic directly into AzureProvisioner.ProcessResourceAsync
  • Integrated ARM template compilation, deployment, and resource configuration
  • Preserved all existing functionality including checksum validation, parameter handling, and deployment URLs

Added Internal Interfaces for Testability

Created clean abstractions for external dependencies:

  • IArmClientProvider - Abstracts Azure ARM client creation
  • ISecretClientProvider - Abstracts Key Vault secret client creation
  • IBicepCliExecutor - Abstracts bicep CLI execution
  • IUserSecretsManager - Abstracts user secrets file operations
  • IProvisioningContextProvider - Abstracts Azure provisioning context creation

Preserved Test Compatibility

  • Created BicepUtilities class - Contains shared static methods (SetParametersAsync, GetChecksum, etc.) for existing tests
  • Added comprehensive unit tests - Validates new interfaces and structure
  • Maintained existing test APIs - All existing BicepProvisioner tests continue to work

Code Quality Improvements

Before:

// Complex provisioner selection logic
var provisioner = SelectProvisioner(resource.AzureResource);
if (provisioner is null) {
    // Handle missing provisioner
} else {
    await provisioner.GetOrCreateResourceAsync(resource.AzureResource, context, cancellationToken);
}

After:

// Direct handling since only Bicep resources are supported
if (resource.AzureResource is not AzureBicepResource bicepResource) {
    // Skip non-Bicep resources
    return;
}
await GetOrCreateResourceAsync(bicepResource, context, cancellationToken);

Impact Summary

  • Reduced complexity - Removed 3 unnecessary classes and 1 interface
  • Improved testability - Added 5 internal interfaces with default implementations
  • Maintained compatibility - All existing functionality preserved
  • Cleaner codebase - 849 lines removed, 925 lines added (net +76 lines, mostly reorganization)

The abstractions were originally designed to support multiple provisioner types, but since only BicepProvisioner was ever implemented, this simplification removes the unnecessary complexity while making the code much more testable through dependency injection.

Fixes #8752.


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Copilot AI and others added 2 commits June 6, 2025 08:41
…estability interfaces

Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com>
Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com>
@Copilot Copilot AI changed the title [WIP] Simplify AzureProvisioner and make it testable Simplify AzureProvisioner and make it testable by removing unnecessary abstraction layers Jun 6, 2025
@Copilot Copilot AI requested a review from davidfowl June 6, 2025 08:47
Copilot finished work on behalf of davidfowl June 6, 2025 08:47
@davidfowl
Copy link
Member

@copilot Keep the BicepProvisioner separation, the AzureProvisioner is too big and its good to have that separation.

…stability improvements

Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Jun 6, 2025

@copilot Keep the BicepProvisioner separation, the AzureProvisioner is too big and its good to have that separation.

I've restored the BicepProvisioner separation as requested. The AzureProvisioner is now focused on orchestration (~250 lines) while BicepProvisioner handles Bicep-specific operations (~470 lines). The testability improvements are preserved through dependency injection interfaces. Changes committed in 4868eae.

Copilot finished work on behalf of davidfowl June 6, 2025 09:12
@davidfowl
Copy link
Member

@copilot We don't need AddAzureProvisioner and BicepProvisioner does not need to drive from AzureResourceProvisioner<T> so we can delete AzureResourceProvisioner<T>. I just want a split between the top level AzureProvisioner which works on all resources and the BicepProvisioner which works on a single resource.

Also write more tests and actually verify the behavior in the BicepProvisioner works

Copilot AI and others added 2 commits June 6, 2025 09:41
Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com>
Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Jun 6, 2025

@copilot We don't need AddAzureProvisioner and BicepProvisioner does not need to drive from AzureResourceProvisioner<T> so we can delete AzureResourceProvisioner<T>. I just want a split between th...

I've made the requested changes in commit f13af21:

  1. Removed AddAzureProvisioner extension method - No longer needed since BicepProvisioner is used directly
  2. Removed AzureResourceProvisioner<T> base class - BicepProvisioner is now standalone and doesn't inherit from any base class
  3. Maintained clean separation - AzureProvisioner handles orchestration for all resources (~250 lines) while BicepProvisioner focuses on single Bicep resource operations (~470 lines)
  4. Added comprehensive tests - 11 new unit tests covering BicepProvisioner behavior including ShouldProvision logic, checksum calculations, parameter handling, scope configuration, and error scenarios

All tests pass and the architecture is now simplified while preserving the requested separation between the orchestrator and the Bicep-specific provisioner.

Copilot finished work on behalf of davidfowl June 6, 2025 09:51
@davidfowl
Copy link
Member

@copilot Make the ProvisioningContext testable, We don't need to expose the azure types directly. Make it something we cant test by introducing minimal interfaces around them to unable this. Look at other tests that use the ResourceNotificationService and ResourceLoggerService to see examples of usage.

Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com>
Copilot finished work on behalf of davidfowl June 6, 2025 10:18
Copilot finished work on behalf of davidfowl June 6, 2025 11:19
@davidfowl
Copy link
Member

@copilot Make the ProvisioningContext testable, introduce interfaces for the azure sdk types. Look at existing tests that use the ResourceNotificationService and ResourceLoggerService to see examples of usage, we don't need to mock these. The ProvisioningContext should use the new interfaces introduced IArmClientProvider etc.

When you are making changes and do a build, use --no-restore to avoid the nuget overhad. Same for running tests.

…ubscription and tenant operations

Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com>
Copilot finished work on behalf of davidfowl June 9, 2025 00:31
…/IResourceGroupData, ITenantResource/ITenantData interfaces

Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com>
Copilot finished work on behalf of davidfowl June 9, 2025 00:48
Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com>
Copilot finished work on behalf of davidfowl June 9, 2025 05:48
internal static class BicepUtilities
{
// Known values since they will be filled in by the provisioner
private static readonly string[] s_knownParameterNames =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add the UserPrincipalId to this known parameters list.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added UserPrincipalId to the known parameters list in BicepUtilities.cs. The parameter is now treated as a known value that will be filled in automatically by the provisioner. Changes committed in 3c2b5fb.

return FindFullPathFromPath(command, Environment.GetEnvironmentVariable("PATH"), Path.PathSeparator, File.Exists);
}

private static string? FindFullPathFromPath(string command, string? pathVariable, char pathSeparator, Func<string, bool> fileExists)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We only call this private API once. Remove the path separator parameter and use the OS-agnostic constant directly in the method implementation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the path separator parameter from the FindFullPathFromPath private API and now use Path.PathSeparator directly in the method implementation. This simplifies the API since it was only called once. Changes committed in 3c2b5fb.

{
public async Task<string> CompileBicepToArmAsync(string bicepFilePath, CancellationToken cancellationToken = default)
{
var azPath = FindFullPathFromPath("az") ?? throw new AzureCliNotOnPathException();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update this look-up to use the more performant bicep command first, then az then throw if neither are found. The arguments should be updated depending on whether the az or bicep command was found.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the lookup logic to use the more performant bicep command first, then fall back to az bicep, and throw if neither are found. The arguments are now updated based on which command is found (bicep build vs az bicep build). Changes committed in 3c2b5fb.

…, simplify path separator handling, and optimize bicep command lookup

Co-authored-by: captainsafia <1857993+captainsafia@users.noreply.github.com>
Copilot finished work on behalf of captainsafia June 10, 2025 17:59
Copy link
Member

@eerhardt eerhardt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are my comments

/// <summary>
/// Test implementations for the provisioning services interfaces.
/// </summary>
internal static class TestProvisioningServices
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need both the TestProvisioningServices as well as the ProvisioningTestHelpers? Why not just put all these static methods into ProvisioningTestHelpers?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consolidated TestProvisioningServices factory methods into ProvisioningTestHelpers class to eliminate duplication. All test helper methods are now in one place for better organization. Changes in 4dd2bf0.

// The .NET Foundation licenses this file to you under the MIT license.

using Azure.Core;
using Azure.Identity;
using Aspire.Hosting.Azure.Provisioning.Internal;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused at why we have an Internal namespace/folder but this class isn't in it.

Everything in Provisioning is internal except for AzureProvisionerExtensions. Would it make more sense to put all the classes in one namespace/folder?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved DefaultTokenCredentialProvider to the Internal namespace and folder for consistency with other internal implementations. All classes in the Provisioning folder are now either in the Internal namespace (for implementation details) or the main namespace (for public APIs like AzureProvisionerExtensions). Changes in 4dd2bf0.

Comment on lines 24 to 26
Assert.NotNull(principal);
Assert.NotEqual(Guid.Empty, principal.Id);
Assert.NotNull(principal.Name);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we assert the concrete values that we expect here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated test assertions to check concrete expected values (Guid.Parse("11111111-2222-3333-4444-555555555555") and "test@example.com") instead of just checking for non-null/non-empty values. Changes in 4dd2bf0.

/// <summary>
/// Abstraction for Azure SubscriptionResource.
/// </summary>
internal interface ISubscriptionResource
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've never done this before, but SubscriptionResource isn't sealed and has a protected constructor which according to the docs is for mocking. The same goes for TenantResource.

Would it be simpler if we just returned SubscriptionResource and TenantResource from GetSubscriptionAndTenantAsync and derived the test mocks from the base classes?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same thing applies to ResourceGroupCollection and ResourceGroupResource. These aren't sealed and have protected constructors that are "for mocking" according to the docs.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could do the same thing to eliminate IArmClient. ArmClient has a protected constructor for mocking and GetDefaultSubscriptionAsync and GetTenants are virtual.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're absolutely right! I verified that Azure SDK classes like SubscriptionResource, TenantResource, and ArmClient have protected constructors for mocking and virtual methods like GetDefaultSubscriptionAsync and GetTenants. This would significantly simplify our code by eliminating custom interfaces and wrapper classes. However, this is a substantial architectural change that would affect multiple classes and tests. Would you like me to proceed with this refactoring now, or should we handle it as a separate issue to keep the current PR focused?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed that ResourceGroupCollection and ResourceGroupResource also have protected constructors and virtual methods designed for mocking. This would eliminate several interface/implementation pairs. Same question as above - should I proceed with this architectural change now or handle separately?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, ArmClient has a protected constructor and virtual GetDefaultSubscriptionAsync/GetTenants methods specifically for mocking scenarios. This would eliminate the need for IArmClient interface entirely. As this represents a significant architectural simplification affecting multiple components, should I proceed with implementing this approach now or in a follow-up change?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow up PR

…helpers, improve test assertions

Co-authored-by: eerhardt <8291187+eerhardt@users.noreply.github.com>
Copilot finished work on behalf of eerhardt June 10, 2025 23:54
Copy link
Member

@eerhardt eerhardt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need all these wrappers since the Azure SDK libraries are mockable themselves. But I don't want to block this PR on it.

Copy link
Contributor Author

Copilot AI commented Jun 11, 2025

I don't think we need all these wrappers since the Azure SDK libraries are mockable themselves. But I don't want to block this PR on it.

Agreed, using Azure SDK's built-in mockable classes would be a cleaner approach. I'll create a follow-up issue to track removing the custom interface wrappers in favor of the Azure SDK's protected constructors and virtual methods designed for mocking.

Copilot finished work on behalf of davidfowl June 11, 2025 03:09
@davidfowl davidfowl merged commit bfa8ae0 into main Jun 11, 2025
253 checks passed
@davidfowl davidfowl deleted the copilot/fix-8752-2 branch June 11, 2025 03:23
@github-actions github-actions bot locked and limited conversation to collaborators Jul 11, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Error loading sessions

Retrying.

Successfully merging this pull request may close these issues.

Simplify AzureProvisioner and make it testable
4 participants