-
Notifications
You must be signed in to change notification settings - Fork 10
fix: cleanup agent instances in a365 cleanup blueprint #284
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
base: main
Are you sure you want to change the base?
Changes from all commits
13dc5f0
674a8ac
77f8a67
7120cdf
5813cb6
9b57f08
22fc347
44ae0ca
ff0ddc9
d16e107
6a54122
5f95f84
6de710d
9adf6dc
9a446e4
8800b54
60ef584
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -13,6 +13,9 @@ namespace Microsoft.Agents.A365.DevTools.Cli.Commands; | |||||
|
|
||||||
| public class CleanupCommand | ||||||
| { | ||||||
| private const string AgenticUsersKey = "agentic users"; | ||||||
| private const string IdentitySpsKey = "identity SPs"; | ||||||
|
|
||||||
| public static Command CreateCommand( | ||||||
| ILogger<CleanupCommand> logger, | ||||||
| IConfigService configService, | ||||||
|
|
@@ -49,7 +52,7 @@ public static Command CreateCommand( | |||||
| }, configOption, verboseOption); | ||||||
|
|
||||||
| // Add subcommands for granular control | ||||||
| cleanupCommand.AddCommand(CreateBlueprintCleanupCommand(logger, configService, botConfigurator, executor, agentBlueprintService, federatedCredentialService, correlationId: correlationId)); | ||||||
| cleanupCommand.AddCommand(CreateBlueprintCleanupCommand(logger, configService, botConfigurator, executor, agentBlueprintService, confirmationProvider, federatedCredentialService, correlationId: correlationId)); | ||||||
gwharris7 marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
| cleanupCommand.AddCommand(CreateAzureCleanupCommand(logger, configService, executor)); | ||||||
| cleanupCommand.AddCommand(CreateInstanceCleanupCommand(logger, configService, executor)); | ||||||
|
|
||||||
|
|
@@ -62,6 +65,7 @@ private static Command CreateBlueprintCleanupCommand( | |||||
| IBotConfigurator botConfigurator, | ||||||
| CommandExecutor executor, | ||||||
| AgentBlueprintService agentBlueprintService, | ||||||
| IConfirmationProvider confirmationProvider, | ||||||
| FederatedCredentialService federatedCredentialService, | ||||||
| string? correlationId = null) | ||||||
| { | ||||||
|
|
@@ -106,41 +110,114 @@ private static Command CreateBlueprintCleanupCommand( | |||||
| return; | ||||||
| } | ||||||
|
|
||||||
| // Full blueprint cleanup (original behavior) | ||||||
| // Full blueprint cleanup with cascade instance deletion | ||||||
| logger.LogInformation("Starting blueprint cleanup..."); | ||||||
|
|
||||||
| // Check if there's actually a blueprint to clean up | ||||||
| if (string.IsNullOrWhiteSpace(config.AgentBlueprintId)) | ||||||
| { | ||||||
| logger.LogInformation("No blueprint application found to clean up"); | ||||||
| return; | ||||||
| } | ||||||
|
|
||||||
| // Query for agent instances linked to this blueprint before showing preview | ||||||
| logger.LogInformation("Querying for agent instances linked to blueprint..."); | ||||||
| List<AgentInstanceInfo> instances; | ||||||
| try | ||||||
| { | ||||||
| instances = (await agentBlueprintService.GetAgentInstancesForBlueprintAsync( | ||||||
| config.TenantId, | ||||||
| config.AgentBlueprintId))?.ToList() ?? new List<AgentInstanceInfo>(); | ||||||
| } | ||||||
| catch (Exception ex) | ||||||
| { | ||||||
| logger.LogError(ex, "Failed to query agent instances for blueprint {BlueprintId}. Aborting cleanup.", config.AgentBlueprintId); | ||||||
| return; | ||||||
gwharris7 marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
| } | ||||||
|
|
||||||
| // Show preview | ||||||
| logger.LogInformation(""); | ||||||
| logger.LogInformation("Blueprint Cleanup Preview:"); | ||||||
| logger.LogInformation("============================="); | ||||||
| logger.LogInformation("Will delete Entra ID application: {BlueprintId}", config.AgentBlueprintId); | ||||||
| logger.LogInformation(" Name: {DisplayName}", config.AgentBlueprintDisplayName); | ||||||
|
|
||||||
| if (instances.Count > 0) | ||||||
| { | ||||||
| logger.LogInformation(""); | ||||||
| logger.LogInformation("Will also delete {Count} agent instance(s) linked to this blueprint:", instances.Count); | ||||||
| foreach (var instance in instances) | ||||||
| { | ||||||
| logger.LogInformation(" Instance: {DisplayName} (SP: {SpId})", instance.DisplayName ?? "(unnamed)", instance.IdentitySpId); | ||||||
| if (!string.IsNullOrWhiteSpace(instance.AgentUserId)) | ||||||
| logger.LogInformation(" Agentic user: {UserId}", instance.AgentUserId); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| logger.LogInformation(""); | ||||||
|
|
||||||
| Console.Write("Continue with blueprint cleanup? (y/N): "); | ||||||
| var response = Console.ReadLine()?.Trim().ToLowerInvariant(); | ||||||
| if (response != "y" && response != "yes") | ||||||
| if (!await confirmationProvider.ConfirmAsync("Continue with blueprint cleanup? (y/N): ")) | ||||||
| { | ||||||
| logger.LogInformation("Cleanup cancelled by user"); | ||||||
| return; | ||||||
| } | ||||||
gwharris7 marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
|
|
||||||
| // Delete instances first (warn and continue on failure) | ||||||
| var failedResources = new Dictionary<string, List<string>> | ||||||
| { | ||||||
| [AgenticUsersKey] = new List<string>(), | ||||||
| [IdentitySpsKey] = new List<string>() | ||||||
| }; | ||||||
|
|
||||||
| foreach (var instance in instances) | ||||||
| { | ||||||
| // Delete agentic user before identity SP | ||||||
| if (!string.IsNullOrWhiteSpace(instance.AgentUserId)) | ||||||
| { | ||||||
| logger.LogInformation("Deleting agentic user {UserId} for instance {DisplayName}...", | ||||||
| instance.AgentUserId, instance.DisplayName ?? instance.IdentitySpId); | ||||||
|
|
||||||
| var userDeleted = await agentBlueprintService.DeleteAgentUserAsync( | ||||||
| config.TenantId, | ||||||
| instance.AgentUserId); | ||||||
|
|
||||||
| if (!userDeleted) | ||||||
| { | ||||||
| logger.LogWarning("Failed to delete agentic user {UserId} -- will continue", instance.AgentUserId); | ||||||
| failedResources[AgenticUsersKey].Add(instance.AgentUserId!); | ||||||
| } | ||||||
| else | ||||||
| { | ||||||
| logger.LogInformation("Agentic user deleted"); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| // Delete identity SP | ||||||
| logger.LogInformation("Deleting agent identity SP {SpId} for instance {DisplayName}...", | ||||||
| instance.IdentitySpId, instance.DisplayName ?? instance.IdentitySpId); | ||||||
|
|
||||||
| var spDeleted = await agentBlueprintService.DeleteAgentIdentityAsync( | ||||||
| config.TenantId, | ||||||
| instance.IdentitySpId); | ||||||
|
|
||||||
| if (!spDeleted) | ||||||
| { | ||||||
| logger.LogWarning("Failed to delete agent identity SP {SpId} -- will continue", instance.IdentitySpId); | ||||||
| failedResources[IdentitySpsKey].Add(instance.IdentitySpId); | ||||||
| } | ||||||
| else | ||||||
| { | ||||||
| logger.LogInformation("Agent identity SP deleted"); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| // Delete federated credentials first before deleting the blueprint | ||||||
| logger.LogInformation(""); | ||||||
| logger.LogInformation("Deleting federated credentials from blueprint..."); | ||||||
|
|
||||||
| // Configure FederatedCredentialService with custom client app ID if available | ||||||
| if (!string.IsNullOrWhiteSpace(config.ClientAppId)) | ||||||
| { | ||||||
| federatedCredentialService.CustomClientAppId = config.ClientAppId; | ||||||
| } | ||||||
|
|
||||||
|
|
||||||
| var ficsDeleted = await federatedCredentialService.DeleteAllFederatedCredentialsAsync( | ||||||
| config.TenantId, | ||||||
| config.AgentBlueprintId); | ||||||
|
|
@@ -155,28 +232,30 @@ private static Command CreateBlueprintCleanupCommand( | |||||
| logger.LogInformation("Federated credentials deleted successfully"); | ||||||
| } | ||||||
|
|
||||||
| // Delete the agent blueprint using the special Graph API endpoint | ||||||
| // Delete the agent blueprint | ||||||
| logger.LogInformation(""); | ||||||
| logger.LogInformation("Deleting agent blueprint application..."); | ||||||
| var deleted = await agentBlueprintService.DeleteAgentBlueprintAsync( | ||||||
| config.TenantId, | ||||||
| config.AgentBlueprintId); | ||||||
|
|
||||||
| if (!deleted) | ||||||
| { | ||||||
| logger.LogWarning(""); | ||||||
| logger.LogWarning("Blueprint deletion failed."); | ||||||
| logger.LogWarning("Blueprint deletion failed. The blueprint still exists in Entra ID."); | ||||||
| PrintOrphanSummary(logger, failedResources); | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 1ES PT Branch Validation blocks official NuGet publish for GitHub‑hosted pipeline. Scenario: one instance existed, its SP and agentic user were deleted successfully, but |
||||||
| if (!HasOrphanedResources(failedResources)) | ||||||
| { | ||||||
| logger.LogWarning("All agent instances were deleted. Retry 'a365 cleanup blueprint' or delete the blueprint manually via the Entra portal or Graph API."); | ||||||
| } | ||||||
| return; | ||||||
| } | ||||||
|
|
||||||
| // Blueprint deleted successfully | ||||||
| logger.LogInformation("Agent blueprint application deleted successfully"); | ||||||
|
|
||||||
| // Handle endpoint deletion if needed using shared helper | ||||||
| if (!await DeleteMessagingEndpointAsync(logger, config, botConfigurator, correlationId: correlationId)) | ||||||
| { | ||||||
| return; | ||||||
|
Comment on lines
256
to
258
|
||||||
| } | ||||||
|
|
||||||
|
Comment on lines
256
to
259
|
||||||
| // Clear configuration after successful blueprint deletion | ||||||
| logger.LogInformation(""); | ||||||
|
|
@@ -188,8 +267,14 @@ private static Command CreateBlueprintCleanupCommand( | |||||
|
|
||||||
| await configService.SaveStateAsync(config); | ||||||
| logger.LogInformation("Local configuration cleared"); | ||||||
| logger.LogInformation(""); | ||||||
| logger.LogInformation("Blueprint cleanup completed successfully!"); | ||||||
|
|
||||||
| // Emit orphan summary if any instance deletions failed (PrintOrphanSummary returns early if none) | ||||||
| PrintOrphanSummary(logger, failedResources); | ||||||
| if (!HasOrphanedResources(failedResources)) | ||||||
| { | ||||||
| logger.LogInformation(""); | ||||||
| logger.LogInformation("Blueprint cleanup completed successfully!"); | ||||||
| } | ||||||
| } | ||||||
| catch (Exception ex) | ||||||
| { | ||||||
|
|
@@ -808,6 +893,37 @@ private static async Task ExecuteEndpointOnlyCleanupAsync( | |||||
| logger.LogInformation(""); | ||||||
| } | ||||||
|
|
||||||
| /// <summary> | ||||||
| /// Checks whether any instance deletions were recorded as failures. | ||||||
| /// </summary> | ||||||
| private static bool HasOrphanedResources(Dictionary<string, List<string>> failedResources) | ||||||
| { | ||||||
| return failedResources[AgenticUsersKey].Count + failedResources[IdentitySpsKey].Count > 0; | ||||||
| } | ||||||
|
|
||||||
| /// <summary> | ||||||
| /// Prints a summary of orphaned Entra ID resources that could not be deleted. | ||||||
| /// This should be called whenever instance deletions have failed, regardless of | ||||||
| /// whether the blueprint deletion itself succeeded or failed. | ||||||
| /// </summary> | ||||||
| private static void PrintOrphanSummary( | ||||||
| ILogger<CleanupCommand> logger, | ||||||
| Dictionary<string, List<string>> failedResources) | ||||||
| { | ||||||
| if (!HasOrphanedResources(failedResources)) | ||||||
| { | ||||||
| return; | ||||||
| } | ||||||
|
|
||||||
| logger.LogWarning("Blueprint cleanup completed with warnings."); | ||||||
|
||||||
| logger.LogWarning("Blueprint cleanup completed with warnings."); | |
| logger.LogWarning("Blueprint cleanup encountered warnings."); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT License. | ||
|
|
||
| namespace Microsoft.Agents.A365.DevTools.Cli.Models; | ||
|
|
||
| /// <summary> | ||
| /// Represents an agent instance linked to a blueprint, consisting of an agent identity | ||
| /// service principal and an optional agentic user. | ||
| /// </summary> | ||
| public sealed record AgentInstanceInfo | ||
| { | ||
| /// <summary>Graph object ID of the agent identity service principal.</summary> | ||
| public required string IdentitySpId { get; init; } | ||
|
|
||
| /// <summary>Display name of the identity service principal, shown in cleanup preview.</summary> | ||
| public string? DisplayName { get; init; } | ||
|
|
||
| /// <summary>Graph object ID of the linked agentic user, if one exists.</summary> | ||
| public string? AgentUserId { get; init; } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.