Skip to content

Commit

Permalink
feat: set custom connector base url (#69)
Browse files Browse the repository at this point in the history
  • Loading branch information
tdashworth authored Aug 9, 2021
1 parent 302c849 commit 4e34b97
Show file tree
Hide file tree
Showing 16 changed files with 394 additions and 1 deletion.
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ This project's aim is to build a powerful base Package Deployer template that si
- [Set process states](#Set-process-states)
- [SDK Steps](#SDK-stpes)
- [Set SDK step states](#Set-sdk-step-states)
- [Connectors](#Connectors)
- [Set base URLs][#Set-base-URLs]
- [Connection references](#Connection-references)
- [Set connection references](#Set-connection-references)
- [Environment variables](#Environment-variables)
Expand Down Expand Up @@ -108,6 +110,34 @@ All SDK steps within the deployed solution(s) are activated by default after the

> 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.
### Connectors

#### Set base URLs

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).

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:

**Environment variable**

```powershell
$env:PACKAGEDEPLOYER_SETTINGS_CONNBASEURL_new_testconnector = "https://new-url.com/api"
Import-CrmPackage [...]
```

**Runtime setting**

```powershell
$runtimeSettings = "ConnBaseUrl:new_testconnector=https://new-url.com/api"
Import-CrmPackage [...] –RuntimePackageSettings $runtimeSettings
```

The runtime setting takes precedence if both an environment variable and runtime setting are found for the same connection reference.

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.

### Connection references

#### Set connection references
Expand Down
37 changes: 37 additions & 0 deletions src/Capgemini.PowerApps.PackageDeployerTemplate/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ public static class Settings
/// </summary>
public const string PowerAppsEnvironmentVariablePrefix = "EnvVar";

/// <summary>
/// The prefix for all connector base urls.
/// </summary>
public const string CustomConnectorBaseUrlPrefix = "ConnBaseUrl";

/// <summary>
/// The prefix for all environment variables.
/// </summary>
Expand Down Expand Up @@ -323,6 +328,38 @@ public static class Fields
}
}

/// <summary>
/// Constants related to the connector entity.
/// </summary>
public static class Connector
{
/// <summary>
/// The logical name.
/// </summary>
public const string LogicalName = "connector";

/// <summary>
/// Field logical names.
/// </summary>
public static class Fields
{
/// <summary>
/// The connector ID.
/// </summary>
public const string ConnectorId = "connectorid";

/// <summary>
/// The logical name of the name.
/// </summary>
public const string Name = "name";

/// <summary>
/// The logical name of the openapidefinition.
/// </summary>
public const string OpenApiDefinition = "openapidefinition";
}
}

/// <summary>
/// Constants related to the connection reference entity.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public abstract class PackageTemplateBase : ImportExtension
private DocumentTemplateDeploymentService documentTemplateSvc;
private SdkStepDeploymentService sdkStepsSvc;
private ConnectionReferenceDeploymentService connectionReferenceSvc;
private ConnectorDeploymentService connectorSvc;
private TableColumnProcessingService autonumberSeedSettingSvc;
private MailboxDeploymentService mailboxSvc;

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

/// <summary>
/// Gets the custom connector base url mappings.
/// </summary>
/// <returns>The Power App environment variables.</returns>
protected IDictionary<string, string> CustomConnectorBaseUrls => this.GetSettings(Constants.Settings.CustomConnectorBaseUrlPrefix);

/// <summary>
/// Gets a list of solutions that have been processed (i.e. <see cref="OverrideSolutionImportDecision"/> has been ran for that solution.)
/// </summary>
Expand Down Expand Up @@ -238,6 +245,22 @@ protected ConnectionReferenceDeploymentService ConnectionReferenceSvc
}
}

/// <summary>
/// Gets provides deployment functionality relating to custom connectors.
/// </summary>
protected ConnectorDeploymentService ConnectorSvc
{
get
{
if (this.connectorSvc == null)
{
this.connectorSvc = new ConnectorDeploymentService(this.TraceLoggerAdapter, this.CrmServiceAdapter);
}

return this.connectorSvc;
}
}

/// <summary>
/// Gets a service that provides functionality relating to setting autonumber seeds.
/// </summary>
Expand Down Expand Up @@ -378,6 +401,8 @@ public override bool AfterPrimaryImport()
this.TemplateConfig.SdkStepsToDeactivate.Where(s => s.External).Select(s => s.Name));
}

this.ConnectorSvc.SetBaseUrls(this.CustomConnectorBaseUrls);

this.ConnectionReferenceSvc.ConnectConnectionReferences(this.ConnectionReferenceMappings, this.LicensedUsername);

this.ProcessDeploymentService.SetStatesBySolution(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
namespace Capgemini.PowerApps.PackageDeployerTemplate.Services
{
using System;
using System.Collections.Generic;
using System.Dynamic;
using System.Linq;
using Capgemini.PowerApps.PackageDeployerTemplate.Adapters;
using Microsoft.Extensions.Logging;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;

/// <summary>
/// Functionality related to deploying custom connectors.
/// </summary>
public class ConnectorDeploymentService
{
private readonly ILogger logger;
private readonly ICrmServiceAdapter crmSvc;

/// <summary>
/// Initializes a new instance of the <see cref="ConnectorDeploymentService"/> class.
/// </summary>
/// <param name="logger">The <see cref="ILogger"/>.</param>
/// <param name="crmSvc">The <see cref="ICrmServiceAdapter"/>.</param>
public ConnectorDeploymentService(ILogger logger, ICrmServiceAdapter crmSvc)
{
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
this.crmSvc = crmSvc ?? throw new ArgumentNullException(nameof(crmSvc));
}

/// <summary>
/// Sets the scheme, basePath and host of custom connectors on the target Power Apps environment.
/// </summary>
/// <param name="baseUrls">A dictionary of names and baseUrls to set.</param>
public void SetBaseUrls(IDictionary<string, string> baseUrls)
{
if (baseUrls is null || !baseUrls.Any())
{
this.logger.LogInformation("No custom connector base URLs have been configured.");
return;
}

foreach (KeyValuePair<string, string> entry in baseUrls)
{
this.SetBaseUrl(entry.Key, entry.Value);
}
}

/// <summary>
/// Sets the scheme, basePath and host of a custom connector on the target Power Apps environment.
/// </summary>
/// <param name="name">Custom Connector name (NOT display name).</param>
/// <param name="baseUrl">New base URL.</param>
public void SetBaseUrl(string name, string baseUrl)
{
this.logger.LogInformation($"Setting {name} custom connector base URL to {baseUrl}.");

if (!Uri.TryCreate(baseUrl, UriKind.Absolute, out var validatedUrl))
{
this.logger.LogError($"The base URL '{baseUrl}' is not valid and the connector '{name}' won't be updated.");
return;
}

var customConnector = this.GetCustomConnectorByName(name, new ColumnSet(Constants.Connector.Fields.OpenApiDefinition));
if (customConnector is null)
{
this.logger.LogError($"Custom connector {name} not found on target instance.");
return;
}

var existingOpenAPiDefinition = customConnector.GetAttributeValue<string>(Constants.Connector.Fields.OpenApiDefinition);
var updatedOpenApiDefinition = UpdateApiDefinition(existingOpenAPiDefinition, validatedUrl);

customConnector[Constants.Connector.Fields.OpenApiDefinition] = updatedOpenApiDefinition;
this.crmSvc.Update(customConnector);
}

private static string UpdateApiDefinition(string currentDefinition, Uri baseUrl)
{
dynamic openapidefinition = JsonConvert.DeserializeObject<ExpandoObject>(currentDefinition, new ExpandoObjectConverter());

openapidefinition.host = baseUrl.Host;
openapidefinition.basePath = baseUrl.AbsolutePath;
openapidefinition.schemes = new string[] { baseUrl.Scheme };

return JsonConvert.SerializeObject(openapidefinition);
}

private Entity GetCustomConnectorByName(string name, ColumnSet columnSet)
{
return this.crmSvc.RetrieveMultipleByAttribute(
Constants.Connector.LogicalName,
Constants.Connector.Fields.Name,
new string[] { name },
columnSet).Entities.FirstOrDefault();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public PackageDeployerFixture(IMessageSink diagnosticMessageSink)
// Check values are set.
_ = GetApprovalsConnection();
_ = GetTestEnvironmentVariable();
_ = GetExampleConnectorBaseUrl();

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

protected static string GetExampleConnectorBaseUrl() =>
GetRequiredEnvironmentVariable("PACKAGEDEPLOYER_SETTINGS_CONNBASEURL_pdt_5Fexample-20api", "No environment variable configured to set custom connector base url.");

private static string GetRequiredEnvironmentVariable(string name, string exceptionMessage)
{
var url = Environment.GetEnvironmentVariable(name);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,5 +170,22 @@ public void PackageTemplateBase_TableColumnProcessing_AutonumberSeedIsSet(string

response.AutoNumberSeedValue.Should().Be(expectedValue);
}

[Fact]
public void PackageTemplateBase_ConnectorBaseUrlPassed_BaseUrlIsSet()
{
var connectorDefinitionQuery = new QueryByAttribute(Constants.Connector.LogicalName);
connectorDefinitionQuery.AddAttributeValue(Constants.Connector.Fields.Name, "pdt_5Fexample-20api");
connectorDefinitionQuery.ColumnSet = new ColumnSet(Constants.Connector.Fields.OpenApiDefinition);

var connectorDefinition = this.fixture.ServiceClient.RetrieveMultiple(connectorDefinitionQuery).Entities.First();
var openApiDefinition = connectorDefinition.GetAttributeValue<string>(Constants.Connector.Fields.OpenApiDefinition);

var newBaseUrl = new Uri(Environment.GetEnvironmentVariable("PACKAGEDEPLOYER_SETTINGS_CONNBASEURL_pdt_5Fexample-20api"));

openApiDefinition.Should().Contain($"\"host\":\"{newBaseUrl.Host}\"", $"Host was not set to '{newBaseUrl.Host}'.");
openApiDefinition.Should().Contain($"\"basePath\":\"{newBaseUrl.AbsolutePath}\"", $"Base URL was not set to '{newBaseUrl.AbsolutePath}'.");
openApiDefinition.Should().Contain($"\"schemes\":[\"{newBaseUrl.Scheme}\"]", $"Schemes was not set to include '{newBaseUrl.Scheme}'.");
}
}
}
Loading

0 comments on commit 4e34b97

Please sign in to comment.