-
Notifications
You must be signed in to change notification settings - Fork 668
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
Conversation
…estability interfaces Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com>
Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com>
@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>
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 We don't need AddAzureProvisioner and BicepProvisioner does not need to drive from Also write more tests and actually verify the behavior in the BicepProvisioner works |
Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com>
Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com>
I've made the requested changes in commit f13af21:
All tests pass and the architecture is now simplified while preserving the requested separation between the orchestrator and the Bicep-specific provisioner. |
@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 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. |
…ler and implement BicepCliCompiler
… update related implementations
…ubscription and tenant operations Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com>
…/IResourceGroupData, ITenantResource/ITenantData interfaces Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com>
src/Aspire.Hosting.Azure/Provisioning/Internal/IProvisioningServices.cs
Outdated
Show resolved
Hide resolved
Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com>
internal static class BicepUtilities | ||
{ | ||
// Known values since they will be filled in by the provisioner | ||
private static readonly string[] s_knownParameterNames = |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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>
There was a problem hiding this 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 |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
Assert.NotNull(principal); | ||
Assert.NotEqual(Guid.Empty, principal.Id); | ||
Assert.NotNull(principal.Name); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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>
There was a problem hiding this 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.
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. |
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
IAzureResourceProvisioner
interface - No longer needed since only one implementation existedAzureResourceProvisioner<T>
base class - Unnecessary generic abstractionBicepProvisioner
class - Integrated its functionality directly intoAzureProvisioner
SelectProvisioner
method that always returnedBicepProvisioner
AddAzureProvisioner<TResource, TProvisioner>
extension method - No longer neededIntegrated BicepProvisioner into AzureProvisioner
AzureProvisioner.ProcessResourceAsync
Added Internal Interfaces for Testability
Created clean abstractions for external dependencies:
IArmClientProvider
- Abstracts Azure ARM client creationISecretClientProvider
- Abstracts Key Vault secret client creationIBicepCliExecutor
- Abstracts bicep CLI executionIUserSecretsManager
- Abstracts user secrets file operationsIProvisioningContextProvider
- Abstracts Azure provisioning context creationPreserved Test Compatibility
BicepUtilities
class - Contains shared static methods (SetParametersAsync
,GetChecksum
, etc.) for existing testsBicepProvisioner
tests continue to workCode Quality Improvements
Before:
After:
Impact Summary
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.