Skip to content

Commit 4e34b97

Browse files
authored
feat: set custom connector base url (#69)
1 parent 302c849 commit 4e34b97

File tree

16 files changed

+394
-1
lines changed

16 files changed

+394
-1
lines changed

README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ This project's aim is to build a powerful base Package Deployer template that si
1717
- [Set process states](#Set-process-states)
1818
- [SDK Steps](#SDK-stpes)
1919
- [Set SDK step states](#Set-sdk-step-states)
20+
- [Connectors](#Connectors)
21+
- [Set base URLs][#Set-base-URLs]
2022
- [Connection references](#Connection-references)
2123
- [Set connection references](#Set-connection-references)
2224
- [Environment variables](#Environment-variables)
@@ -108,6 +110,34 @@ All SDK steps within the deployed solution(s) are activated by default after the
108110

109111
> You can also activate or deactivate SDK steps that are not in your package by setting the `external` attribute to `true` on an `<sdkstep>` element. Be careful when doing this - deploying your package may introduce side-effects to an environment that make it incompatible with other solutions.
110112
113+
### Connectors
114+
115+
#### Set base URLs
116+
117+
You can set the base URL (scheme, host, base path) for custom connector either through environment variables (for example, those [exposed on Azure Pipelines](https://docs.microsoft.com/en-us/azure/devops/pipelines/process/variables?view=azure-devops&tabs=yaml%2Cbatch#access-variables-through-the-environment) from your variables or variable groups) or through Package Deployer [runtime settings](https://docs.microsoft.com/en-us/power-platform/admin/deploy-packages-using-package-deployer-windows-powershell#use-the-cmdlet-to-deploy-packages).
118+
119+
Environment variables must be prefixed with `PACKAGEDEPLOYER_SETTINGS_CONNBASEURL_` and followed by the connector name (not display name). Similarly, runtime settings must be prefixed with `ConnBaseUrl:` and followed by the connector name (not display name). For example, if a custom connector name was `new_testconnector`, this could be set via either of the following:
120+
121+
**Environment variable**
122+
123+
```powershell
124+
$env:PACKAGEDEPLOYER_SETTINGS_CONNBASEURL_new_testconnector = "https://new-url.com/api"
125+
126+
Import-CrmPackage [...]
127+
```
128+
129+
**Runtime setting**
130+
131+
```powershell
132+
$runtimeSettings = "ConnBaseUrl:new_testconnector=https://new-url.com/api"
133+
134+
Import-CrmPackage [...] –RuntimePackageSettings $runtimeSettings
135+
```
136+
137+
The runtime setting takes precedence if both an environment variable and runtime setting are found for the same connection reference.
138+
139+
To get your custom connector name, either query the web API for `https://[your-environment].dynamics.com/api/data/v9.2/connectors` and use the `name` property or if your solution is unpacked, use the `name` property in the `.xml` under the `Connectors/` directory.
140+
111141
### Connection references
112142

113143
#### Set connection references

src/Capgemini.PowerApps.PackageDeployerTemplate/Constants.cs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ public static class Settings
2020
/// </summary>
2121
public const string PowerAppsEnvironmentVariablePrefix = "EnvVar";
2222

23+
/// <summary>
24+
/// The prefix for all connector base urls.
25+
/// </summary>
26+
public const string CustomConnectorBaseUrlPrefix = "ConnBaseUrl";
27+
2328
/// <summary>
2429
/// The prefix for all environment variables.
2530
/// </summary>
@@ -323,6 +328,38 @@ public static class Fields
323328
}
324329
}
325330

331+
/// <summary>
332+
/// Constants related to the connector entity.
333+
/// </summary>
334+
public static class Connector
335+
{
336+
/// <summary>
337+
/// The logical name.
338+
/// </summary>
339+
public const string LogicalName = "connector";
340+
341+
/// <summary>
342+
/// Field logical names.
343+
/// </summary>
344+
public static class Fields
345+
{
346+
/// <summary>
347+
/// The connector ID.
348+
/// </summary>
349+
public const string ConnectorId = "connectorid";
350+
351+
/// <summary>
352+
/// The logical name of the name.
353+
/// </summary>
354+
public const string Name = "name";
355+
356+
/// <summary>
357+
/// The logical name of the openapidefinition.
358+
/// </summary>
359+
public const string OpenApiDefinition = "openapidefinition";
360+
}
361+
}
362+
326363
/// <summary>
327364
/// Constants related to the connection reference entity.
328365
/// </summary>

src/Capgemini.PowerApps.PackageDeployerTemplate/PackageTemplateBase.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ public abstract class PackageTemplateBase : ImportExtension
3131
private DocumentTemplateDeploymentService documentTemplateSvc;
3232
private SdkStepDeploymentService sdkStepsSvc;
3333
private ConnectionReferenceDeploymentService connectionReferenceSvc;
34+
private ConnectorDeploymentService connectorSvc;
3435
private TableColumnProcessingService autonumberSeedSettingSvc;
3536
private MailboxDeploymentService mailboxSvc;
3637

@@ -88,6 +89,12 @@ protected string LicensedUsername
8889
/// <returns>The Power App environment variables.</returns>
8990
protected IDictionary<string, string> PowerAppsEnvironmentVariables => this.GetSettings(Constants.Settings.PowerAppsEnvironmentVariablePrefix);
9091

92+
/// <summary>
93+
/// Gets the custom connector base url mappings.
94+
/// </summary>
95+
/// <returns>The Power App environment variables.</returns>
96+
protected IDictionary<string, string> CustomConnectorBaseUrls => this.GetSettings(Constants.Settings.CustomConnectorBaseUrlPrefix);
97+
9198
/// <summary>
9299
/// Gets a list of solutions that have been processed (i.e. <see cref="OverrideSolutionImportDecision"/> has been ran for that solution.)
93100
/// </summary>
@@ -238,6 +245,22 @@ protected ConnectionReferenceDeploymentService ConnectionReferenceSvc
238245
}
239246
}
240247

248+
/// <summary>
249+
/// Gets provides deployment functionality relating to custom connectors.
250+
/// </summary>
251+
protected ConnectorDeploymentService ConnectorSvc
252+
{
253+
get
254+
{
255+
if (this.connectorSvc == null)
256+
{
257+
this.connectorSvc = new ConnectorDeploymentService(this.TraceLoggerAdapter, this.CrmServiceAdapter);
258+
}
259+
260+
return this.connectorSvc;
261+
}
262+
}
263+
241264
/// <summary>
242265
/// Gets a service that provides functionality relating to setting autonumber seeds.
243266
/// </summary>
@@ -378,6 +401,8 @@ public override bool AfterPrimaryImport()
378401
this.TemplateConfig.SdkStepsToDeactivate.Where(s => s.External).Select(s => s.Name));
379402
}
380403

404+
this.ConnectorSvc.SetBaseUrls(this.CustomConnectorBaseUrls);
405+
381406
this.ConnectionReferenceSvc.ConnectConnectionReferences(this.ConnectionReferenceMappings, this.LicensedUsername);
382407

383408
this.ProcessDeploymentService.SetStatesBySolution(
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
namespace Capgemini.PowerApps.PackageDeployerTemplate.Services
2+
{
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Dynamic;
6+
using System.Linq;
7+
using Capgemini.PowerApps.PackageDeployerTemplate.Adapters;
8+
using Microsoft.Extensions.Logging;
9+
using Microsoft.Xrm.Sdk;
10+
using Microsoft.Xrm.Sdk.Query;
11+
using Newtonsoft.Json;
12+
using Newtonsoft.Json.Converters;
13+
14+
/// <summary>
15+
/// Functionality related to deploying custom connectors.
16+
/// </summary>
17+
public class ConnectorDeploymentService
18+
{
19+
private readonly ILogger logger;
20+
private readonly ICrmServiceAdapter crmSvc;
21+
22+
/// <summary>
23+
/// Initializes a new instance of the <see cref="ConnectorDeploymentService"/> class.
24+
/// </summary>
25+
/// <param name="logger">The <see cref="ILogger"/>.</param>
26+
/// <param name="crmSvc">The <see cref="ICrmServiceAdapter"/>.</param>
27+
public ConnectorDeploymentService(ILogger logger, ICrmServiceAdapter crmSvc)
28+
{
29+
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
30+
this.crmSvc = crmSvc ?? throw new ArgumentNullException(nameof(crmSvc));
31+
}
32+
33+
/// <summary>
34+
/// Sets the scheme, basePath and host of custom connectors on the target Power Apps environment.
35+
/// </summary>
36+
/// <param name="baseUrls">A dictionary of names and baseUrls to set.</param>
37+
public void SetBaseUrls(IDictionary<string, string> baseUrls)
38+
{
39+
if (baseUrls is null || !baseUrls.Any())
40+
{
41+
this.logger.LogInformation("No custom connector base URLs have been configured.");
42+
return;
43+
}
44+
45+
foreach (KeyValuePair<string, string> entry in baseUrls)
46+
{
47+
this.SetBaseUrl(entry.Key, entry.Value);
48+
}
49+
}
50+
51+
/// <summary>
52+
/// Sets the scheme, basePath and host of a custom connector on the target Power Apps environment.
53+
/// </summary>
54+
/// <param name="name">Custom Connector name (NOT display name).</param>
55+
/// <param name="baseUrl">New base URL.</param>
56+
public void SetBaseUrl(string name, string baseUrl)
57+
{
58+
this.logger.LogInformation($"Setting {name} custom connector base URL to {baseUrl}.");
59+
60+
if (!Uri.TryCreate(baseUrl, UriKind.Absolute, out var validatedUrl))
61+
{
62+
this.logger.LogError($"The base URL '{baseUrl}' is not valid and the connector '{name}' won't be updated.");
63+
return;
64+
}
65+
66+
var customConnector = this.GetCustomConnectorByName(name, new ColumnSet(Constants.Connector.Fields.OpenApiDefinition));
67+
if (customConnector is null)
68+
{
69+
this.logger.LogError($"Custom connector {name} not found on target instance.");
70+
return;
71+
}
72+
73+
var existingOpenAPiDefinition = customConnector.GetAttributeValue<string>(Constants.Connector.Fields.OpenApiDefinition);
74+
var updatedOpenApiDefinition = UpdateApiDefinition(existingOpenAPiDefinition, validatedUrl);
75+
76+
customConnector[Constants.Connector.Fields.OpenApiDefinition] = updatedOpenApiDefinition;
77+
this.crmSvc.Update(customConnector);
78+
}
79+
80+
private static string UpdateApiDefinition(string currentDefinition, Uri baseUrl)
81+
{
82+
dynamic openapidefinition = JsonConvert.DeserializeObject<ExpandoObject>(currentDefinition, new ExpandoObjectConverter());
83+
84+
openapidefinition.host = baseUrl.Host;
85+
openapidefinition.basePath = baseUrl.AbsolutePath;
86+
openapidefinition.schemes = new string[] { baseUrl.Scheme };
87+
88+
return JsonConvert.SerializeObject(openapidefinition);
89+
}
90+
91+
private Entity GetCustomConnectorByName(string name, ColumnSet columnSet)
92+
{
93+
return this.crmSvc.RetrieveMultipleByAttribute(
94+
Constants.Connector.LogicalName,
95+
Constants.Connector.Fields.Name,
96+
new string[] { name },
97+
columnSet).Entities.FirstOrDefault();
98+
}
99+
}
100+
}

tests/Capgemini.PowerApps.PackageDeployerTemplate.IntegrationTests/PackageDeployerFixture.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ public PackageDeployerFixture(IMessageSink diagnosticMessageSink)
2121
// Check values are set.
2222
_ = GetApprovalsConnection();
2323
_ = GetTestEnvironmentVariable();
24+
_ = GetExampleConnectorBaseUrl();
2425

2526
var startInfo = new ProcessStartInfo
2627
{
@@ -89,6 +90,9 @@ protected static string GetPassword() =>
8990
protected static string GetTestEnvironmentVariable() =>
9091
GetRequiredEnvironmentVariable("PACKAGEDEPLOYER_SETTINGS_ENVVAR_PDT_TESTVARIABLE", "No environment variable configured to set power apps test environment variable.");
9192

93+
protected static string GetExampleConnectorBaseUrl() =>
94+
GetRequiredEnvironmentVariable("PACKAGEDEPLOYER_SETTINGS_CONNBASEURL_pdt_5Fexample-20api", "No environment variable configured to set custom connector base url.");
95+
9296
private static string GetRequiredEnvironmentVariable(string name, string exceptionMessage)
9397
{
9498
var url = Environment.GetEnvironmentVariable(name);

tests/Capgemini.PowerApps.PackageDeployerTemplate.IntegrationTests/PackageTemplateBaseTests.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,5 +170,22 @@ public void PackageTemplateBase_TableColumnProcessing_AutonumberSeedIsSet(string
170170

171171
response.AutoNumberSeedValue.Should().Be(expectedValue);
172172
}
173+
174+
[Fact]
175+
public void PackageTemplateBase_ConnectorBaseUrlPassed_BaseUrlIsSet()
176+
{
177+
var connectorDefinitionQuery = new QueryByAttribute(Constants.Connector.LogicalName);
178+
connectorDefinitionQuery.AddAttributeValue(Constants.Connector.Fields.Name, "pdt_5Fexample-20api");
179+
connectorDefinitionQuery.ColumnSet = new ColumnSet(Constants.Connector.Fields.OpenApiDefinition);
180+
181+
var connectorDefinition = this.fixture.ServiceClient.RetrieveMultiple(connectorDefinitionQuery).Entities.First();
182+
var openApiDefinition = connectorDefinition.GetAttributeValue<string>(Constants.Connector.Fields.OpenApiDefinition);
183+
184+
var newBaseUrl = new Uri(Environment.GetEnvironmentVariable("PACKAGEDEPLOYER_SETTINGS_CONNBASEURL_pdt_5Fexample-20api"));
185+
186+
openApiDefinition.Should().Contain($"\"host\":\"{newBaseUrl.Host}\"", $"Host was not set to '{newBaseUrl.Host}'.");
187+
openApiDefinition.Should().Contain($"\"basePath\":\"{newBaseUrl.AbsolutePath}\"", $"Base URL was not set to '{newBaseUrl.AbsolutePath}'.");
188+
openApiDefinition.Should().Contain($"\"schemes\":[\"{newBaseUrl.Scheme}\"]", $"Schemes was not set to include '{newBaseUrl.Scheme}'.");
189+
}
173190
}
174191
}

0 commit comments

Comments
 (0)