Idempotent setup: blueprint/infrastructure discovery#144
Conversation
Implement complete idempotency for a365 setup commands to handle
repeated executions gracefully and provide accurate user feedback.
Changes:
- Add BlueprintLookupService for dual-path blueprint discovery (objectId
primary, displayName fallback for migration scenarios)
- Add FederatedCredentialService for FIC existence checks and creation
- Persist blueprint objectIds (app + service principal) to config for
authoritative future lookups
- Track idempotency flags: InfrastructureAlreadyExisted,
BlueprintAlreadyExisted, EndpointAlreadyExisted
- Update setup summary to display "configured (already exists)" vs "created"
Fixes:
- Move endpoint error log after idempotency check (no more false ERR)
- Convert verbose internal logs to LogDebug ("Saved dynamic state", auth checks)
- Fix SaveStateAsync usage to prevent service principal ID overwrites
Testing:
- Add BlueprintLookupServiceTests (8 tests)
- Add FederatedCredentialServiceTests (11 tests)
- All 777/777 tests passing
Wire new services through DI container and all setup subcommands.
There was a problem hiding this comment.
Pull request overview
This PR implements comprehensive idempotency for the a365 setup commands to handle repeated executions gracefully, addressing issue #87 where setup would fail if run multiple times due to resource conflicts.
Key Changes:
- Introduces BlueprintLookupService for dual-path blueprint discovery (objectId primary, displayName fallback)
- Adds FederatedCredentialService for managing federated identity credentials with idempotency checks
- Persists blueprint objectIds (application + service principal) to config for authoritative future lookups
- Adds idempotency tracking flags and improves user feedback to distinguish between "created" vs "configured (already exists)"
Reviewed changes
Copilot reviewed 21 out of 21 changed files in this pull request and generated 13 comments.
Show a summary per file
| File | Description |
|---|---|
| BlueprintLookupService.cs | New service implementing dual-path blueprint discovery (objectId primary, displayName fallback) for migration scenarios |
| FederatedCredentialService.cs | New service for checking existing FICs and creating new ones with HTTP 409 handling and fallback endpoint support |
| BlueprintLookupServiceTests.cs | 8 unit tests covering blueprint and service principal lookup operations |
| FederatedCredentialServiceTests.cs | 11 unit tests covering FIC retrieval, existence checks, and creation with conflict handling |
| SetupCommandTests.cs | Updated to wire new BlueprintLookupService and FederatedCredentialService through DI |
| BlueprintSubcommandTests.cs | Updated test command instantiation with new service dependencies |
| Agent365Config.cs | Added AgentBlueprintObjectId and AgentBlueprintServicePrincipalObjectId properties for authoritative identification |
| SetupResults.cs | Added idempotency tracking flags (InfrastructureAlreadyExisted, BlueprintAlreadyExisted, EndpointAlreadyExisted, etc.) |
| SetupHelpers.cs | Updated summary display to show "configured (already exists)" vs "created" based on idempotency flags |
| BlueprintSubcommand.cs | Implements idempotency checks using dual-path discovery, persists objectIds, and handles existing blueprints |
| InfrastructureSubcommand.cs | Returns tuple with anyAlreadyExisted flag to track infrastructure idempotency |
| AllSubcommand.cs | Orchestrates setup steps with idempotency tracking and reduced verbose logging |
| SetupCommand.cs | Wires new services (BlueprintLookupService, FederatedCredentialService) through DI container |
| BotConfigurator.cs | Improved error handling to move error log after idempotency check and handle HTTP 500 with "already exists" |
| ConfigService.cs | Converted verbose operational logs to LogDebug for cleaner output |
| AgentBlueprintService.cs | Added GetPasswordCredentialsAsync method for querying existing client secrets |
| GraphApiService.cs | Made GraphPostAsync and GraphPostWithResponseAsync virtual to support test mocking |
| AzureValidator.cs, AzureAuthValidator.cs, AzureEnvironmentValidator.cs | Reduced logging verbosity by converting informational logs to LogDebug |
| Program.cs | Registered BlueprintLookupService and FederatedCredentialService in DI container |
Comments suppressed due to low confidence (1)
src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupSubcommands/BlueprintSubcommand.cs:1205
- The CreateBlueprintClientSecretAsync method lacks idempotency checks. When a blueprint already exists and setup is run again, this method will always attempt to create a new client secret, potentially creating multiple secrets for the same application. This contradicts the PR's goal of making setup commands idempotent.
Consider checking if a valid client secret already exists in the config before creating a new one, or use the GetPasswordCredentialsAsync method from AgentBlueprintService to check for existing secrets. For truly idempotent behavior, the method should reuse existing secrets when available or provide clear user feedback when creating additional ones.
public static async Task CreateBlueprintClientSecretAsync(
string blueprintObjectId,
string blueprintAppId,
JsonObject generatedConfig,
string generatedConfigPath,
GraphApiService graphService,
Models.Agent365Config setupConfig,
IConfigService configService,
ILogger logger,
CancellationToken ct = default)
{
try
{
logger.LogInformation("Creating client secret for Agent Blueprint using Graph API...");
var graphToken = await graphService.GetGraphAccessTokenAsync(
generatedConfig["tenantId"]?.GetValue<string>() ?? string.Empty, ct);
if (string.IsNullOrWhiteSpace(graphToken))
{
logger.LogError("Failed to acquire Graph API access token");
throw new InvalidOperationException("Cannot create client secret without Graph API token");
}
using var httpClient = HttpClientFactory.CreateAuthenticatedClient(graphToken);
var secretBody = new JsonObject
{
["passwordCredential"] = new JsonObject
{
["displayName"] = "Agent 365 CLI Generated Secret",
["endDateTime"] = DateTime.UtcNow.AddYears(2).ToString("yyyy-MM-ddTHH:mm:ss.fffZ")
}
};
var addPasswordUrl = $"https://graph.microsoft.com/v1.0/applications/{blueprintObjectId}/addPassword";
var passwordResponse = await httpClient.PostAsync(
addPasswordUrl,
new StringContent(secretBody.ToJsonString(), System.Text.Encoding.UTF8, "application/json"),
ct);
if (!passwordResponse.IsSuccessStatusCode)
{
var errorContent = await passwordResponse.Content.ReadAsStringAsync(ct);
logger.LogError("Failed to create client secret: {Status} - {Error}", passwordResponse.StatusCode, errorContent);
throw new InvalidOperationException($"Failed to create client secret: {errorContent}");
}
var passwordJson = await passwordResponse.Content.ReadAsStringAsync(ct);
var passwordResult = JsonNode.Parse(passwordJson)!.AsObject();
var secretTextNode = passwordResult["secretText"];
if (secretTextNode == null || string.IsNullOrWhiteSpace(secretTextNode.GetValue<string>()))
{
logger.LogError("Client secret text is empty in response");
throw new InvalidOperationException("Client secret creation returned empty secret");
}
var protectedSecret = Microsoft.Agents.A365.DevTools.Cli.Helpers.SecretProtectionHelper.ProtectSecret(secretTextNode.GetValue<string>(), logger);
var isProtected = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
generatedConfig["agentBlueprintClientSecret"] = protectedSecret;
generatedConfig["agentBlueprintClientSecretProtected"] = isProtected;
setupConfig.AgentBlueprintClientSecret = protectedSecret;
setupConfig.AgentBlueprintClientSecretProtected = isProtected;
// Use SaveStateAsync to preserve all existing dynamic properties (especially agentBlueprintServicePrincipalObjectId)
await configService.SaveStateAsync(setupConfig);
logger.LogInformation("Client secret created successfully!");
logger.LogInformation($" - Secret stored in generated config (encrypted: {isProtected})");
logger.LogWarning("IMPORTANT: The client secret has been stored in {Path}", generatedConfigPath);
logger.LogWarning("Keep this file secure and do not commit it to source control!");
if (!isProtected)
{
logger.LogWarning("WARNING: Secret encryption is only available on Windows. The secret is stored in plaintext.");
logger.LogWarning("Consider using environment variables or Azure Key Vault for production deployments.");
}
}
catch (Exception ex)
{
logger.LogWarning(ex, "Failed to create client secret: {Message}", ex.Message);
logger.LogInformation("You can create a client secret manually:");
logger.LogInformation(" 1. Go to Azure Portal > App Registrations");
logger.LogInformation(" 2. Find your Agent Blueprint: {AppId}", blueprintAppId);
logger.LogInformation(" 3. Navigate to Certificates & secrets > Client secrets");
logger.LogInformation(" 4. Click 'New client secret' and save the value");
logger.LogInformation(" 5. Add it to {Path} as 'agentBlueprintClientSecret'", generatedConfigPath);
}
}
src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupSubcommands/BlueprintSubcommand.cs
Outdated
Show resolved
Hide resolved
src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupSubcommands/BlueprintSubcommand.cs
Show resolved
Hide resolved
src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupSubcommands/BlueprintSubcommand.cs
Show resolved
Hide resolved
src/Microsoft.Agents.A365.DevTools.Cli/Services/BotConfigurator.cs
Outdated
Show resolved
Hide resolved
src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupSubcommands/BlueprintSubcommand.cs
Outdated
Show resolved
Hide resolved
src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Services/FederatedCredentialServiceTests.cs
Outdated
Show resolved
Hide resolved
src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Services/BlueprintLookupServiceTests.cs
Show resolved
Hide resolved
src/Microsoft.Agents.A365.DevTools.Cli/Services/FederatedCredentialService.cs
Outdated
Show resolved
Hide resolved
src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupSubcommands/SetupResults.cs
Outdated
Show resolved
Hide resolved
src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Services/BlueprintLookupServiceTests.cs
Outdated
Show resolved
Hide resolved
- Cleanup now deletes federated credentials before blueprint removal, using new FederatedCredentialService methods with endpoint fallback. - FederatedCredentialService enhanced for robust listing, creation, deletion, and error handling; skips malformed entries and supports propagation retries. - Blueprint setup refactored to consistently handle federated credential creation/validation and admin consent, with consent checked before prompting. - Added AdminConsentHelper.CheckConsentExistsAsync to verify existing grants. - Improved idempotency tracking and summary logging for permissions. - Updated CleanupCommand and tests to require/use FederatedCredentialService. - Expanded tests for federated credential and consent logic. - Refined bot endpoint deletion to avoid false positives. - Improved logging and user feedback throughout blueprint and cleanup flows.
src/Microsoft.Agents.A365.DevTools.Cli/Services/AzureEnvironmentValidator.cs
Outdated
Show resolved
Hide resolved
src/Microsoft.Agents.A365.DevTools.Cli/Services/BlueprintLookupService.cs
Outdated
Show resolved
Hide resolved
src/Microsoft.Agents.A365.DevTools.Cli/Services/AgentBlueprintService.cs
Outdated
Show resolved
Hide resolved
src/Microsoft.Agents.A365.DevTools.Cli/Services/FederatedCredentialService.cs
Outdated
Show resolved
Hide resolved
src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupSubcommands/SetupHelpers.cs
Show resolved
Hide resolved
Moved blueprint, federated credential, and password credential model classes from service classes to new files in the Models namespace for better organization. Updated services to use the new models. Improved logging for messaging endpoint setup and Azure CLI Python bitness detection.
src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupSubcommands/BlueprintSubcommand.cs
Show resolved
Hide resolved
src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupSubcommands/BlueprintSubcommand.cs
Outdated
Show resolved
Hide resolved
src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupSubcommands/BlueprintSubcommand.cs
Show resolved
Hide resolved
src/Microsoft.Agents.A365.DevTools.Cli/Services/FederatedCredentialService.cs
Show resolved
Hide resolved
src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupSubcommands/BlueprintSubcommand.cs
Outdated
Show resolved
Hide resolved
src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupSubcommands/BlueprintSubcommand.cs
Outdated
Show resolved
Hide resolved
src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupSubcommands/BlueprintSubcommand.cs
Outdated
Show resolved
Hide resolved
- Update FIC naming to use display name only (no objectId), as uniqueness is per app, not tenant-wide. - Improve error logging for missing blueprint identifiers by removing null-coalescing fallback. - Remove unused variable assignment from federated credential retry logic.
* Idempotent setup: blueprint/infrastructure discovery
Implement complete idempotency for a365 setup commands to handle
repeated executions gracefully and provide accurate user feedback.
Changes:
- Add BlueprintLookupService for dual-path blueprint discovery (objectId
primary, displayName fallback for migration scenarios)
- Add FederatedCredentialService for FIC existence checks and creation
- Persist blueprint objectIds (app + service principal) to config for
authoritative future lookups
- Track idempotency flags: InfrastructureAlreadyExisted,
BlueprintAlreadyExisted, EndpointAlreadyExisted
- Update setup summary to display "configured (already exists)" vs "created"
Fixes:
- Move endpoint error log after idempotency check (no more false ERR)
- Convert verbose internal logs to LogDebug ("Saved dynamic state", auth checks)
- Fix SaveStateAsync usage to prevent service principal ID overwrites
Testing:
- Add BlueprintLookupServiceTests (8 tests)
- Add FederatedCredentialServiceTests (11 tests)
- All 777/777 tests passing
Wire new services through DI container and all setup subcommands.
* Improve federated credential and consent handling in CLI
- Cleanup now deletes federated credentials before blueprint removal, using new FederatedCredentialService methods with endpoint fallback.
- FederatedCredentialService enhanced for robust listing, creation, deletion, and error handling; skips malformed entries and supports propagation retries.
- Blueprint setup refactored to consistently handle federated credential creation/validation and admin consent, with consent checked before prompting.
- Added AdminConsentHelper.CheckConsentExistsAsync to verify existing grants.
- Improved idempotency tracking and summary logging for permissions.
- Updated CleanupCommand and tests to require/use FederatedCredentialService.
- Expanded tests for federated credential and consent logic.
- Refined bot endpoint deletion to avoid false positives.
- Improved logging and user feedback throughout blueprint and cleanup flows.
* Refactor: move model classes to dedicated files
Moved blueprint, federated credential, and password credential model classes from service classes to new files in the Models namespace for better organization. Updated services to use the new models. Improved logging for messaging endpoint setup and Azure CLI Python bitness detection.
* Refactor FIC naming and logging; clean up retry logic
- Update FIC naming to use display name only (no objectId), as uniqueness is per app, not tenant-wide.
- Improve error logging for missing blueprint identifiers by removing null-coalescing fallback.
- Remove unused variable assignment from federated credential retry logic.
Implement complete idempotency for a365 setup commands to handle repeated executions gracefully and provide accurate user feedback.
Changes:
Fixes:
Testing:
Wire new services through DI container and all setup subcommands.
Fixes issue #87