Skip to content

Commit 3b72aad

Browse files
authored
Merge pull request #17 from Keyfactor/release-2.1
Release 2.1 to main
2 parents ce8e0f7 + 2bf956f commit 3b72aad

File tree

12 files changed

+106
-56
lines changed

12 files changed

+106
-56
lines changed

AzureAppGatewayOrchestrator.Tests/AzureAppGatewayOrchestrator_AzureAppGW.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414

1515
using System.Security.Cryptography.X509Certificates;
1616
using Azure.ResourceManager.Network.Models;
17-
using AzureApplicationGatewayOrchestratorExtension;
1817
using AzureApplicationGatewayOrchestratorExtension.AppGatewayCertificateJobs;
1918
using AzureApplicationGatewayOrchestratorExtension.Client;
2019
using Keyfactor.Logging;

AzureAppGatewayOrchestrator.Tests/AzureAppGatewayOrchestrator_Client.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -87,12 +87,12 @@ public void AzureClientIntegrationTest()
8787
// Step 3 - Get the certificates that exist on the app gateway
8888

8989
// Act
90-
IEnumerable<Keyfactor.Orchestrators.Extensions.CurrentInventoryItem> certs = client.GetAppGatewaySslCertificates();
90+
OperationResult<IEnumerable<Keyfactor.Orchestrators.Extensions.CurrentInventoryItem>> certs = client.GetAppGatewaySslCertificates();
9191

9292
// Assert
93-
Assert.NotNull(certs);
94-
Assert.NotEmpty(certs);
95-
Assert.Contains(certs, c => c.Alias == certName);
93+
Assert.NotNull(certs.Result);
94+
Assert.NotEmpty(certs.Result);
95+
Assert.Contains(certs.Result, c => c.Alias == certName);
9696

9797
// Step 4 - Try to remove the certificate from the app gateway, which should fail
9898
// since it's bound to an HTTPS listener
@@ -119,7 +119,7 @@ public void AzureClientIntegrationTest()
119119
// Rebind the HTTPS listener with the original certificate, if there was only 1 certificate
120120
// previously, otherwise bind it with the first certificate in the list.
121121

122-
ApplicationGatewaySslCertificate replacement = client.GetAppGatewayCertificateByName(certs.First(c => c.Alias != certName).Alias);
122+
ApplicationGatewaySslCertificate replacement = client.GetAppGatewayCertificateByName(certs.Result.First(c => c.Alias != certName).Alias);
123123

124124
client.UpdateHttpsListenerCertificate(replacement, httpsListenerName);
125125

AzureAppGatewayOrchestrator.Tests/AzureAppGatewayOrchestrator_FakeClient.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ public IAzureAppGatewayClient Build()
7474
public IEnumerable<string>? AppGatewaysAvailableOnFakeTenant { get; set; }
7575
public Dictionary<string, ApplicationGatewaySslCertificate>? CertificatesAvailableOnFakeAppGateway { get; set; }
7676

77-
public IEnumerable<CurrentInventoryItem> GetAppGatewaySslCertificates()
77+
public OperationResult<IEnumerable<CurrentInventoryItem>> GetAppGatewaySslCertificates()
7878
{
7979
_logger.LogDebug("Getting App Gateway SSL Certificates from fake app gateway");
8080

@@ -84,6 +84,8 @@ public IEnumerable<CurrentInventoryItem> GetAppGatewaySslCertificates()
8484
}
8585

8686
List<CurrentInventoryItem> inventoryItems = new List<CurrentInventoryItem>();
87+
OperationResult<IEnumerable<CurrentInventoryItem>> result = new(inventoryItems);
88+
8789
foreach (ApplicationGatewaySslCertificate cert in CertificatesAvailableOnFakeAppGateway.Values)
8890
{
8991
inventoryItems.Add(new CurrentInventoryItem
@@ -98,7 +100,7 @@ public IEnumerable<CurrentInventoryItem> GetAppGatewaySslCertificates()
98100

99101
_logger.LogDebug($"Fake client has {inventoryItems.Count} certificates in inventory");
100102

101-
return inventoryItems;
103+
return result;
102104
}
103105

104106
public ApplicationGatewaySslCertificate AddCertificate(string certificateName, string certificateData, string certificatePassword)

AzureAppGatewayOrchestrator/AppGatewayCertificateJobs/Inventory.cs

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,32 @@ public JobResult ProcessJob(InventoryJobConfiguration config, SubmitInventoryUpd
5151

5252
try
5353
{
54-
inventoryItems = Client.GetAppGatewaySslCertificates().ToList();
54+
OperationResult<IEnumerable<CurrentInventoryItem>> inventoryResult = Client.GetAppGatewaySslCertificates();
55+
if (!inventoryResult.Success)
56+
{
57+
// Aggregate the messages into the failure message. Since an exception wasn't thrown,
58+
// we still have a partial success. We want to return a warning.
59+
result.FailureMessage += inventoryResult.ErrorMessage;
60+
result.Result = OrchestratorJobStatusJobResult.Warning;
61+
_logger.LogWarning(result.FailureMessage);
62+
}
63+
else
64+
{
65+
result.Result = OrchestratorJobStatusJobResult.Success;
66+
}
67+
68+
// At least partial success is guaranteed, so we can continue with the inventory items
69+
// that we were able to pull down.
70+
inventoryItems = inventoryResult.Result.ToList();
71+
5572
} catch (Exception ex)
5673
{
74+
// Exception is triggered if we weren't able to pull down the list of certificates
75+
// from Azure. This could be due to a number of reasons, including network issues,
76+
// or the user not having the correct permissions. An exception won't be triggered
77+
// if there are no certificates in the App Gateway, or if we weren't able to assemble
78+
// the list of certificates into a CurrentInventoryItem.
79+
5780
_logger.LogError(ex, "Error getting App Gateway SSL Certificates:\n" + ex.Message);
5881
result.FailureMessage = "Error getting App Gateway SSL Certificates:\n" + ex.Message;
5982
return result;
@@ -64,7 +87,6 @@ public JobResult ProcessJob(InventoryJobConfiguration config, SubmitInventoryUpd
6487
//cb.DynamicInvoke(inventoryItems);
6588
cb(inventoryItems);
6689

67-
result.Result = OrchestratorJobStatusJobResult.Success;
6890
return result;
6991
}
7092
}

AzureAppGatewayOrchestrator/Client/GatewayClient.cs

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -273,15 +273,16 @@ public ApplicationGatewaySslCertificate GetAppGatewayCertificateByName(string ce
273273
return gatewaySslCertificate;
274274
}
275275

276-
public IEnumerable<CurrentInventoryItem> GetAppGatewaySslCertificates()
276+
public OperationResult<IEnumerable<CurrentInventoryItem>> GetAppGatewaySslCertificates()
277277
{
278-
279278
ApplicationGatewayResource appGatewayResource =
280279
_armClient.GetApplicationGatewayResource(AppGatewayResourceId).Get();
281280
_logger.LogDebug($"Getting SSL certificates from App Gateway called \"{appGatewayResource.Data.Name}\"");
282281
_logger.LogDebug($"There are {appGatewayResource.Data.SslCertificates.Count()} certificates in the response.");
283282
List<CurrentInventoryItem> inventoryItems = new List<CurrentInventoryItem>();
284283

284+
OperationResult<IEnumerable<CurrentInventoryItem>> result = new(inventoryItems);
285+
285286
foreach (ApplicationGatewaySslCertificate certObject in appGatewayResource.Data.SslCertificates)
286287
{
287288
List<string> b64EncodedDerCertificateList = new List<string>();
@@ -316,14 +317,19 @@ public IEnumerable<CurrentInventoryItem> GetAppGatewaySslCertificates()
316317
}
317318
catch (Exception e)
318319
{
319-
_logger.LogError($"Error retrieving certificate from Azure Key Vault with ID {certObject.KeyVaultSecretId}: {e.Message}");
320+
string error = $"Failed to download certificate from Azure Key Vault with ID {certObject.KeyVaultSecretId}";
321+
_logger.LogError(error + $": {e.Message}");
322+
323+
result.AddRuntimeErrorMessage(error);
320324
continue;
321325
}
322326
}
323327
else
324328
{
325-
_logger.LogError($"Certificate called \"{certObject.Name}\" ({certObject.Id}) does not have any public certificate data or Key Vault secret ID.");
329+
string error = $"Certificate called \"{certObject.Name}\" ({certObject.Id}) does not have any public certificate data or Key Vault secret ID.";
330+
_logger.LogError(error);
326331

332+
result.AddRuntimeErrorMessage(error);
327333
continue;
328334
}
329335

@@ -340,8 +346,13 @@ public IEnumerable<CurrentInventoryItem> GetAppGatewaySslCertificates()
340346
inventoryItems.Add(inventoryItem);
341347
}
342348

349+
if (!result.Success)
350+
{
351+
result.ErrorSummary = $"Application Gateway Certificate inventory may be incomplete. Successfully read {inventoryItems.Count()}/{appGatewayResource.Data.SslCertificates.Count()} certificates present in the Application Gateway called {AppGatewayResourceId.Name} ({AppGatewayResourceId})\nPlease see Orchestrator logs for more details. Error summary:";
352+
}
353+
343354
_logger.LogDebug($"Found {inventoryItems.Count()} certificates in app gateway");
344-
return inventoryItems;
355+
return result;
345356
}
346357

347358
public void UpdateHttpsListenerCertificate(ApplicationGatewaySslCertificate certificate, string listenerName)

AzureAppGatewayOrchestrator/Client/IAzureAppGatewayClient.cs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,31 @@ public interface IAzureAppGatewayClientBuilder
2828
public IAzureAppGatewayClient Build();
2929
}
3030

31+
public class OperationResult<T>
32+
{
33+
public T Result { get; set; }
34+
public string ErrorSummary { get; set; }
35+
public List<string> Messages { get; set; } = new List<string>();
36+
public bool Success => Messages.Count == 0;
37+
38+
public OperationResult(T result)
39+
{
40+
Result = result;
41+
}
42+
43+
public void AddRuntimeErrorMessage(string message)
44+
{
45+
Messages.Add(" - " + message);
46+
}
47+
48+
public string ErrorMessage => $"{ErrorSummary}\n{string.Join("\n", Messages)}";
49+
}
50+
3151
public interface IAzureAppGatewayClient
3252
{
3353
public ApplicationGatewaySslCertificate AddCertificate(string certificateName, string certificateData, string certificatePassword);
3454
public void RemoveCertificate(string certificateName);
35-
public IEnumerable<CurrentInventoryItem> GetAppGatewaySslCertificates();
55+
public OperationResult<IEnumerable<CurrentInventoryItem>> GetAppGatewaySslCertificates();
3656
public ApplicationGatewaySslCertificate GetAppGatewayCertificateByName(string certificateName);
3757
public bool CertificateExists(string certificateName);
3858
public IEnumerable<string> DiscoverApplicationGateways();

AzureAppGatewayOrchestrator/ListenerBindingJobs/Inventory.cs

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ public JobResult ProcessJob(InventoryJobConfiguration config, SubmitInventoryUpd
4444
JobResult result = new JobResult
4545
{
4646
Result = OrchestratorJobStatusJobResult.Failure,
47-
JobHistoryId = config.JobHistoryId
47+
JobHistoryId = config.JobHistoryId,
48+
FailureMessage = ""
4849
};
4950

5051
List<CurrentInventoryItem> appGatewayCertificateInventory;
@@ -62,9 +63,32 @@ public JobResult ProcessJob(InventoryJobConfiguration config, SubmitInventoryUpd
6263

6364
try
6465
{
65-
appGatewayCertificateInventory = Client.GetAppGatewaySslCertificates().ToList();
66+
OperationResult<IEnumerable<CurrentInventoryItem>> inventoryResult = Client.GetAppGatewaySslCertificates();
67+
if (!inventoryResult.Success)
68+
{
69+
// Aggregate the messages into the failure message. Since an exception wasn't thrown,
70+
// we still have a partial success. We want to return a warning.
71+
result.FailureMessage += inventoryResult.ErrorMessage;
72+
result.Result = OrchestratorJobStatusJobResult.Warning;
73+
_logger.LogWarning(result.FailureMessage);
74+
}
75+
else
76+
{
77+
result.Result = OrchestratorJobStatusJobResult.Success;
78+
}
79+
80+
// At least partial success is guaranteed, so we can continue with the inventory items
81+
// that we were able to pull down.
82+
appGatewayCertificateInventory = inventoryResult.Result.ToList();
83+
6684
} catch (Exception ex)
6785
{
86+
// Exception is triggered if we weren't able to pull down the list of certificates
87+
// from Azure. This could be due to a number of reasons, including network issues,
88+
// or the user not having the correct permissions. An exception won't be triggered
89+
// if there are no certificates in the App Gateway, or if we weren't able to assemble
90+
// the list of certificates into a CurrentInventoryItem.
91+
6892
_logger.LogError(ex, "Error getting App Gateway SSL Certificates:\n" + ex.Message);
6993
result.FailureMessage = "Error getting App Gateway SSL Certificates:\n" + ex.Message;
7094
return result;
@@ -81,7 +105,7 @@ public JobResult ProcessJob(InventoryJobConfiguration config, SubmitInventoryUpd
81105
} catch (Exception ex)
82106
{
83107
_logger.LogError(ex, "Error getting bound App Gateway HTTPS Listener Certificates:\n" + ex.Message);
84-
result.FailureMessage = "Error getting bound App Gateway HTTPS Listener Certificates:\n" + ex.Message;
108+
result.FailureMessage += "Error getting bound App Gateway HTTPS Listener Certificates:\n" + ex.Message;
85109
return result;
86110
}
87111

@@ -129,7 +153,7 @@ public JobResult ProcessJob(InventoryJobConfiguration config, SubmitInventoryUpd
129153

130154
cb.DynamicInvoke(certificateBindingInventory);
131155

132-
result.Result = OrchestratorJobStatusJobResult.Success;
156+
// Result is already set correctly by this point.
133157
return result;
134158
}
135159
}

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,6 @@
1717
- chore(jobs): Refactored Orchestrator job implementations to prefer dependency injection pattern.
1818
- chore(tests): Implemented unit testing framework with a fake client interface.
1919
- chore(tests): Implemented integration tests for both Orchestrator jobs and App Gateway client.
20+
21+
- 2.1.0
22+
- chore(client): Pass error back to Command if certificate download from AKV fails

Makefile

Lines changed: 1 addition & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -62,33 +62,6 @@ setup: ## Setup the environment for development
6262
echo "PROJECT_NAME=$$(basename $$(dirname $$(grep PROJECT_FILE .env | cut -d '=' -f 2)))" >> .env; \
6363
fi
6464

65-
.PHONY: newtest
66-
newtest: setup ## Create a new test project
67-
@source .env; \
68-
testProjectName="$$PROJECT_NAME".Tests; \
69-
echo "Creating new xUnit project called $$testProjectName"; \
70-
dotnet new xunit -o $$testProjectName; \
71-
dotnet sln add $$testProjectName/$$testProjectName.csproj; \
72-
dotnet add $$testProjectName reference $$PROJECT_FILE;
73-
74-
.PHONY: installpackage
75-
installpackage: ## Install a package to the project
76-
@source .env; \
77-
echo "Select a project to install the package into"; \
78-
PS3="Selection: "; \
79-
select opt in $$(ls */*.csproj); do \
80-
if [ -n "$$opt" ]; then \
81-
echo "You have selected $$opt"; \
82-
break; \
83-
else \
84-
echo "Invalid selection. Please try again."; \
85-
fi; \
86-
done; \
87-
echo "Enter the package name to install: "; \
88-
read packageName; \
89-
echo "Installing $$packageName to $$opt"; \
90-
dotnet add $$opt package $$packageName;
91-
9265
.PHONY: testall
9366
testall: ## Run all tests.
9467
@source .env; \
@@ -105,7 +78,7 @@ test: ## Run a single test.
10578
cut -d ' ' -f 5- | \
10679
sed 's/(.*//i' | \
10780
sort | uniq | \
108-
fzf | \
81+
fzf | \
10982
xargs -I {} dotnet test --filter {} --logger "console;verbosity=detailed"
11083

11184
##@ Build

README.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ The Universal Orchestrator is the successor to the Windows Orchestrator. This Or
1515

1616
## Support for Azure Application Gateway Orchestrator
1717

18-
Azure Application Gateway Orchestrator is open source and supported on best effort level for this tool/library/client. This means customers can report Bugs, Feature Requests, Documentation amendment or questions as well as requests for customer information required for setup that needs Keyfactor access to obtain. Such requests do not follow normal SLA commitments for response or resolution. If you have a support issue, please open a support ticket via the Keyfactor Support Portal at https://support.keyfactor.com/
18+
Azure Application Gateway Orchestrator is supported by Keyfactor for Keyfactor customers. If you have a support issue, please open a support ticket via the Keyfactor Support Portal at https://support.keyfactor.com
1919

2020
###### To report a problem or suggest a new feature, use the **[Issues](../../issues)** tab. If you want to contribute actual bug fixes or proposed enhancements, use the **[Pull requests](../../pulls)** tab.
2121

@@ -101,9 +101,7 @@ Natively, Azure Application Gateways support integration with Azure Key Vault fo
101101

102102
#### Mechanics of the Azure Key Vault Download Operation for Inventory Jobs that report certificates imported from AKV
103103

104-
If an AzureApplicationSslCertificate references a secret in AKV (was imported to the App Gateway from AKV), the inventory job will create and use a `SecretClient` from the [`Azure.Security.KeyVault.Secrets.SecretClient` dotnet package](https://learn.microsoft.com/en-us/dotnet/api/azure.security.keyvault.secrets.secretclient?view=azure-dotnet). Authentication to AKV via this client is configured using the exact same `TokenCredential` provided by the [Azure Identity client library for .NET](https://learn.microsoft.com/en-us/dotnet/api/overview/azure/identity-readme?view=azure-dotnet). This means that the Service Principal described in the [Azure Configuration](#azure-configuration) section must also have appropriate permissions to read secrets from the AKV that the App Gateway is integrated with.
105-
106-
The secret referenced in the AzureApplicationSslCertificate will be accessed exactly as reported by Azure, regardless of whether it exists in AKV. Since the App Gateway orchestrator extension doesn't manage AKV secrets in any way, the client will _only log an error_ if the client is unsuccessful in downloading the secret for any reason. IE, if the request to AKV fails after five tries, the imported certificate will not be reported to Keyfactor Command in Inventory operations. This design choice was made based on the logical existance or non-existance of the link between AKV and the App Gateway.
104+
If an AzureApplicationSslCertificate references a secret in AKV (was imported to the App Gateway from AKV), the inventory job will create and use a `SecretClient` from the [`Azure.Security.KeyVault.Secrets.SecretClient` dotnet package](https://learn.microsoft.com/en-us/dotnet/api/azure.security.keyvault.secrets.secretclient?view=azure-dotnet). Authentication to AKV via this client is configured using the exact same `TokenCredential` provided by the [Azure Identity client library for .NET](https://learn.microsoft.com/en-us/dotnet/api/overview/azure/identity-readme?view=azure-dotnet). This means that the Service Principal described in the [Azure Configuration](#azure-configuration) section must also have appropriate permissions to read secrets from the AKV that the App Gateway is integrated with. The secret referenced in the AzureApplicationSslCertificate will be accessed exactly as reported by Azure, regardless of whether it exists in AKV.
107105

108106
## Azure Configuration and Permissions
109107

integration-manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"integration_type": "orchestrator",
55
"status": "production",
66
"description": "The Azure Application Gateway Orchestrator Extension is an extension to the Keyfactor Universal Orchestrator that allows for the management of certificates on Azure Application Gateways, including the ability to add and bind certificates to HTTPS listeners.",
7-
"support_level": "kf-community",
7+
"support_level": "kf-supported",
88
"release_dir": "AzureAppGatewayOrchestrator\\bin\\Release",
99
"link_github": true,
1010
"update_catalog": true,

0 commit comments

Comments
 (0)