From ff0d29b0de8a9f52ed3b234f2b40f81cee861ea6 Mon Sep 17 00:00:00 2001 From: Nishkalank Bezawada <47456098+NishkalankBezawada@users.noreply.github.com> Date: Tue, 15 Apr 2025 16:52:41 +0000 Subject: [PATCH 1/3] Initial commit for New command - Import-PnPFlow --- .../PowerPlatform/PowerAutomate/ImportFlow.cs | 233 ++++++++++++++++++ 1 file changed, 233 insertions(+) create mode 100644 src/Commands/PowerPlatform/PowerAutomate/ImportFlow.cs diff --git a/src/Commands/PowerPlatform/PowerAutomate/ImportFlow.cs b/src/Commands/PowerPlatform/PowerAutomate/ImportFlow.cs new file mode 100644 index 000000000..a0a9c8f10 --- /dev/null +++ b/src/Commands/PowerPlatform/PowerAutomate/ImportFlow.cs @@ -0,0 +1,233 @@ +using PnP.PowerShell.Commands.Attributes; +using PnP.PowerShell.Commands.Base; +using PnP.PowerShell.Commands.Base.PipeBinds; +using PnP.PowerShell.Commands.Utilities; +using PnP.PowerShell.Commands.Utilities.REST; +using System; +using System.IO; +using System.Management.Automation; +using System.Net.Http; +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace PnP.PowerShell.Commands.PowerPlatform.PowerAutomate +{ + [Cmdlet(VerbsData.Import, "PnPFlow")] + [ApiNotAvailableUnderApplicationPermissions] + [RequiredApiDelegatedPermissions("azure/user_impersonation")] + public class ImportFlow : PnPAzureManagementApiCmdlet + { + [Parameter(Mandatory = true)] + public PowerAutomateFlowPipeBind Identity; + + [Parameter(Mandatory = false)] + public PowerPlatformEnvironmentPipeBind Environment; + + [Parameter(Mandatory = true)] + public string PackagePath; + + [Parameter(Mandatory = false)] + public SwitchParameter CreateAsNew; + + [Parameter(Mandatory = false)] + public string Name; + + protected override void ExecuteCmdlet() + { + var environmentName = ParameterSpecified(nameof(Environment)) ? Environment.GetName() : PowerPlatformUtility.GetDefaultEnvironment(ArmRequestHelper, Connection.AzureEnvironment)?.Name; + var flowName = Identity.GetName(); + + var postData = new + { + baseResourceIds = new[] { + $"/providers/Microsoft.Flow/flows/{flowName}" + } + }; + + string baseUrl = PowerPlatformUtility.GetBapEndpoint(Connection.AzureEnvironment); + + // Step 1: Generate a storage URL for the package + var generateResourceUrlResponse = RestHelper.Post(Connection.HttpClient, $"{baseUrl}/providers/Microsoft.BusinessAppPlatform/environments/{environmentName}/generateResourceStorage?api-version=2016-11-01", AccessToken); + WriteVerbose($"Storage resource URL generated: {generateResourceUrlResponse}"); + + // Parse the response to get the shared access signature URL + var resourceUrlData = JsonSerializer.Deserialize(generateResourceUrlResponse); + var sasUrl = resourceUrlData.GetProperty("sharedAccessSignature").GetString(); + + + var fileName = Path.GetFileName(PackagePath); + var blobUri = new UriBuilder(sasUrl); + blobUri.Path += $"/{fileName}"; + + UploadPackageToBlob(blobUri); + + + // Step 3: Get import parameters with the package link + var importPayload = new + { + packageLink = new + { + value = blobUri.Uri.ToString() + } + }; + + var importParametersResponse = RestHelper.PostGetResponseHeader( + Connection.HttpClient, + $"{baseUrl}/providers/Microsoft.BusinessAppPlatform/environments/{environmentName}/listImportParameters?api-version=2016-11-01", + AccessToken, + payload: importPayload, + accept: "application/json" + ); + WriteVerbose("Import parameters retrieved"); + + + + var importOperationsUrl = importParametersResponse.Location.ToString(); + + var listImportOperations = RestHelper.Get( + Connection.HttpClient, + importOperationsUrl, + AccessToken, + accept: "application/json" + ); + + WriteVerbose("Import operations retrieved"); + + var importOperationsData = JsonSerializer.Deserialize(listImportOperations); + + if (!importOperationsData.TryGetProperty("properties", out JsonElement propertiesElement)) + { + WriteObject("Import failed: 'properties' section missing."); + return; + } + + bool hasStatus = propertiesElement.TryGetProperty("status", out _); + bool hasPackageLink = propertiesElement.TryGetProperty("packageLink", out _); + bool hasDetails = propertiesElement.TryGetProperty("details", out _); + bool hasResources = propertiesElement.TryGetProperty("resources", out _); + + if (!(hasStatus && hasPackageLink && hasDetails && hasResources)) + { + WriteObject("Import failed: One or more required fields are missing in 'properties'."); + return; + } + if (!propertiesElement.TryGetProperty("resources", out JsonElement resourcesElement)) + { + WriteObject("Import failed: 'resources' section missing in 'properties'."); + return; + } + + var resourcesObject = JsonNode.Parse(resourcesElement.GetRawText()) as JsonObject; + var resource = TransformResources(resourcesObject); + + // Update the "resources" in the propertiesElement + var validatePackagePayload = CreateImportObject(propertiesElement, resourcesObject); + /*var validatePackagePayload = new JsonObject + { + ["details"] = JsonNode.Parse(propertiesElement.GetProperty("details").GetRawText()), + ["packageLink"] = JsonNode.Parse(propertiesElement.GetProperty("packageLink").GetRawText()), + ["status"] = JsonNode.Parse(propertiesElement.GetProperty("status").GetRawText()), + ["resources"] = resource + };*/ + + var validateResponse = RestHelper.Post(Connection.HttpClient, $"{baseUrl}/providers/Microsoft.BusinessAppPlatform/environments/{environmentName}/validateImportPackage?api-version=2016-11-01", AccessToken, payload: validatePackagePayload); + var validateResponseData = JsonSerializer.Deserialize(validateResponse); + + var importPackagePayload = CreateImportObject(validateResponseData); + var importResult = RestHelper.Post(Connection.HttpClient, $"{baseUrl}/providers/Microsoft.BusinessAppPlatform/environments/{environmentName}/importPackage?api-version=2016-11-01", AccessToken, payload: importPackagePayload); + WriteVerbose("Import package initiated"); + + WriteObject(importResult); + + } + + private void UploadPackageToBlob(UriBuilder blobUri) + { + // Step 2: Upload the package to the blob storage using the SAS URL + + // Upload using clean HttpClient + using (var blobClient = new HttpClient()) + using (var packageFileStream = new FileStream(PackagePath, FileMode.Open, FileAccess.Read)) + { + var packageContent = new StreamContent(packageFileStream); + packageContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/octet-stream"); + + var request = new HttpRequestMessage(HttpMethod.Put, blobUri.Uri) + { + Content = packageContent + }; + + request.Headers.Add("x-ms-blob-type", "BlockBlob"); + + var uploadResponse = blobClient.SendAsync(request).GetAwaiter().GetResult(); + + if (!uploadResponse.IsSuccessStatusCode) + { + var errorContent = uploadResponse.Content.ReadAsStringAsync().GetAwaiter().GetResult(); + throw new Exception($"Upload failed: {uploadResponse.StatusCode} - {errorContent}"); + } + } + } + + private JsonObject TransformResources(JsonObject resourcesObject) + { + foreach (var property in resourcesObject) + { + string resourceKey = property.Key; + var resource = property.Value as JsonObject; + + if (resource != null && resource.TryGetPropertyValue("type", out JsonNode typeNode)) + { + string resourceType = typeNode?.ToString(); + + if (resourceType == "Microsoft.Flow/flows") + { + if (CreateAsNew) + { + resource["selectedCreationType"] = "New"; + if (ParameterSpecified(nameof(Name))) + { + if (resource.TryGetPropertyValue("details", out JsonNode detailsNode) && detailsNode is JsonObject detailsObject) + { + detailsObject["displayName"] = Name; + } + } + + } + else + { + resource["selectedCreationType"] = "Existing"; + if (resource.TryGetPropertyValue("suggestedId", out JsonNode suggestedIdNode) && suggestedIdNode != null) + { + resource["id"] = JsonValue.Create(suggestedIdNode.ToString()); + } + } + } + else if (resourceType == "Microsoft.PowerApps/apis/connections") + { + resource["selectedCreationType"] = "Existing"; + + // Only set the id if suggestedId exists + if (resource.TryGetPropertyValue("suggestedId", out JsonNode suggestedIdNode) && suggestedIdNode != null) + { + resource["id"] = JsonValue.Create(suggestedIdNode.ToString()); + } + } + } + } + return resourcesObject; + } + + private JsonObject CreateImportObject(JsonElement importData, JsonObject resourceObject = null) + { + JsonObject resourcesObject = new JsonObject + { + ["details"] = JsonNode.Parse(importData.GetProperty("details").GetRawText()), + ["packageLink"] = JsonNode.Parse(importData.GetProperty("packageLink").GetRawText()), + ["status"] = JsonNode.Parse(importData.GetProperty("status").GetRawText()), + ["resources"] = resourceObject ?? JsonNode.Parse(importData.GetProperty("resources").GetRawText()) + }; + return resourcesObject; + } + } +} From 67494ee405cad383ba350b2a8ef6c20bb9804944 Mon Sep 17 00:00:00 2001 From: Nishkalank Bezawada <47456098+NishkalankBezawada@users.noreply.github.com> Date: Thu, 24 Apr 2025 16:31:28 +0000 Subject: [PATCH 2/3] Test scenarios passed --- .../PowerPlatform/PowerAutomate/ImportFlow.cs | 70 +++++++++++++------ 1 file changed, 47 insertions(+), 23 deletions(-) diff --git a/src/Commands/PowerPlatform/PowerAutomate/ImportFlow.cs b/src/Commands/PowerPlatform/PowerAutomate/ImportFlow.cs index a0a9c8f10..b0ea31f9a 100644 --- a/src/Commands/PowerPlatform/PowerAutomate/ImportFlow.cs +++ b/src/Commands/PowerPlatform/PowerAutomate/ImportFlow.cs @@ -1,4 +1,6 @@ -using PnP.PowerShell.Commands.Attributes; +using Microsoft.Graph; +using Newtonsoft.Json.Serialization; +using PnP.PowerShell.Commands.Attributes; using PnP.PowerShell.Commands.Base; using PnP.PowerShell.Commands.Base.PipeBinds; using PnP.PowerShell.Commands.Utilities; @@ -17,9 +19,6 @@ namespace PnP.PowerShell.Commands.PowerPlatform.PowerAutomate [RequiredApiDelegatedPermissions("azure/user_impersonation")] public class ImportFlow : PnPAzureManagementApiCmdlet { - [Parameter(Mandatory = true)] - public PowerAutomateFlowPipeBind Identity; - [Parameter(Mandatory = false)] public PowerPlatformEnvironmentPipeBind Environment; @@ -35,17 +34,8 @@ public class ImportFlow : PnPAzureManagementApiCmdlet protected override void ExecuteCmdlet() { var environmentName = ParameterSpecified(nameof(Environment)) ? Environment.GetName() : PowerPlatformUtility.GetDefaultEnvironment(ArmRequestHelper, Connection.AzureEnvironment)?.Name; - var flowName = Identity.GetName(); - - var postData = new - { - baseResourceIds = new[] { - $"/providers/Microsoft.Flow/flows/{flowName}" - } - }; string baseUrl = PowerPlatformUtility.GetBapEndpoint(Connection.AzureEnvironment); - // Step 1: Generate a storage URL for the package var generateResourceUrlResponse = RestHelper.Post(Connection.HttpClient, $"{baseUrl}/providers/Microsoft.BusinessAppPlatform/environments/{environmentName}/generateResourceStorage?api-version=2016-11-01", AccessToken); WriteVerbose($"Storage resource URL generated: {generateResourceUrlResponse}"); @@ -80,7 +70,7 @@ protected override void ExecuteCmdlet() ); WriteVerbose("Import parameters retrieved"); - + System.Threading.Thread.Sleep(2500); //Wait 2.5 seconds to get the import parameters var importOperationsUrl = importParametersResponse.Location.ToString(); @@ -122,23 +112,20 @@ protected override void ExecuteCmdlet() // Update the "resources" in the propertiesElement var validatePackagePayload = CreateImportObject(propertiesElement, resourcesObject); - /*var validatePackagePayload = new JsonObject - { - ["details"] = JsonNode.Parse(propertiesElement.GetProperty("details").GetRawText()), - ["packageLink"] = JsonNode.Parse(propertiesElement.GetProperty("packageLink").GetRawText()), - ["status"] = JsonNode.Parse(propertiesElement.GetProperty("status").GetRawText()), - ["resources"] = resource - };*/ var validateResponse = RestHelper.Post(Connection.HttpClient, $"{baseUrl}/providers/Microsoft.BusinessAppPlatform/environments/{environmentName}/validateImportPackage?api-version=2016-11-01", AccessToken, payload: validatePackagePayload); var validateResponseData = JsonSerializer.Deserialize(validateResponse); var importPackagePayload = CreateImportObject(validateResponseData); - var importResult = RestHelper.Post(Connection.HttpClient, $"{baseUrl}/providers/Microsoft.BusinessAppPlatform/environments/{environmentName}/importPackage?api-version=2016-11-01", AccessToken, payload: importPackagePayload); + + var importResult = RestHelper.PostGetResponseHeader(Connection.HttpClient, $"{baseUrl}/providers/Microsoft.BusinessAppPlatform/environments/{environmentName}/importPackage?api-version=2016-11-01", AccessToken, payload: importPackagePayload, accept: "application/json"); WriteVerbose("Import package initiated"); - WriteObject(importResult); + var importPackageResponseUrl = importResult.Location.ToString(); + + var importStatus = WaitForImportCompletion(importPackageResponseUrl); + WriteObject($"Import {importStatus}"); } private void UploadPackageToBlob(UriBuilder blobUri) @@ -229,5 +216,42 @@ private JsonObject CreateImportObject(JsonElement importData, JsonObject resourc }; return resourcesObject; } + + private string WaitForImportCompletion(string importPackageResponseUrl) + { + string status; + int retryCount = 0; + + do + { + var importResultData = RestHelper.Get(Connection.HttpClient, importPackageResponseUrl, AccessToken, accept: "application/json"); + var importResultDataElement = JsonSerializer.Deserialize(importResultData); + + if (importResultDataElement.TryGetProperty("properties", out JsonElement importResultPropertiesElement) && + importResultPropertiesElement.TryGetProperty("status", out JsonElement statusElement)) + { + status = statusElement.GetString(); + } + else + { + WriteWarning("Failed to retrieve the status from the response."); + throw new Exception("Import status could not be determined."); + } + + if (status == "Running") + { + WriteVerbose("Import is still running. Waiting for completion..."); + System.Threading.Thread.Sleep(2500); // Wait for 2.5 seconds before retrying + retryCount++; + } + } while (status == "Running" && retryCount < 5); + + if (status == "Running") + { + throw new Exception("Import failed to complete after 5 attempts."); + } + + return status; + } } } From 37a0b40b9eace7ab0601e0bef6219bd04c50d22c Mon Sep 17 00:00:00 2001 From: Nishkalank Bezawada <47456098+NishkalankBezawada@users.noreply.github.com> Date: Wed, 30 Apr 2025 16:37:46 +0000 Subject: [PATCH 3/3] Code refactored and added documentation --- documentation/Import-PnPFlow.md | 128 +++++++++ .../PowerPlatform/PowerAutomate/ImportFlow.cs | 250 +++++++++++------- 2 files changed, 288 insertions(+), 90 deletions(-) create mode 100644 documentation/Import-PnPFlow.md diff --git a/documentation/Import-PnPFlow.md b/documentation/Import-PnPFlow.md new file mode 100644 index 000000000..dc1bdf173 --- /dev/null +++ b/documentation/Import-PnPFlow.md @@ -0,0 +1,128 @@ +--- +Module Name: PnP.PowerShell +schema: 2.0.0 +applicable: SharePoint Online +online version: https://pnp.github.io/powershell/cmdlets/Import-PnPFlow.html +external help file: PnP.PowerShell.dll-Help.xml +title: Import-PnPFlow +--- + +# Import-PnPFlow + +## SYNOPSIS + +**Required Permissions** + +* Azure: management.azure.com + +Imports a Microsoft Power Automate Flow. + +## SYNTAX + +### With Zip Package +```powershell +Import-PnPFlow [-Environment ] [-PackagePath ] [-Name ] [-Connection ] + +``` + +## DESCRIPTION +This cmdlet Imports a Microsoft Power Automate Flow from a zip package. + +Many times Importing a Microsoft Power Automate Flow will not be possible due to various reasons such as connections having gone stale, SharePoint sites referenced no longer existing or other configuration errors in the Flow. To display these errors when trying to Import a Flow, provide the -Verbose flag with your Import request. If not provided, these errors will silently be ignored. + +## EXAMPLES + +### Example 1 +```powershell +Import-PnPFlow -Environment (Get-PnPPowerPlatformEnvironment -Identity "myenvironment") -PackagePath C:\Temp\Export-ReEnableFlow_20250414140636.zip -Name NewFlowName +``` + +This will Import the specified Microsoft Power Automate Flow from the specified Power Platform environment as an output to the current output of PowerShell + +### Example 2 +```powershell +Import-PnPFlow -Environment (Get-PnPPowerPlatformEnvironment -IsDefault) -PackagePath C:\Temp\Export-ReEnableFlow_20250414140636.zip -Name NewFlowName +``` + +This will Import the specified Microsoft Power Automate Flow from the default Power Platform environment as an output to the current output of PowerShell + +### Example 3 +```powershell +Import-PnPFlow -PackagePath C:\Temp\Export-ReEnableFlow_20250414140636.zip -Name NewFlowName +``` + +This will Import a flow to the default environment. The flow will be imported as a zip package. The name of the flow will be set to NewFlowName. + +### Example 4 +```powershell +Import-PnPFlow -PackagePath C:\Temp\Export-ReEnableFlow_20250414140636.zip -Name NewFlowName -Verbose +``` + +This will Import a flow to the default environment. The flow will be imported as a zip package. The name of the flow will be set to NewFlowName. With the -Verbose flag, any errors that occur during the import process will be displayed in the console. + +## PARAMETERS + +### -Connection +Optional connection to be used by the cmdlet. +Retrieve the value for this parameter by either specifying -ReturnConnection on Connect-PnPOnline or by executing Get-PnPConnection. + +```yaml +Type: PnPConnection +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Environment +The name of the Power Platform environment or an Environment instance. If omitted, the default environment will be used. + +```yaml +Type: PowerPlatformEnvironmentPipeBind +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: The default environment +Accept pipeline input: True +Accept wildcard characters: False +``` + +### -PackagePath +Local path of the .zip package to import. The path must be a valid path on the local file system. + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: true +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Name +The new name of the flow. + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: true +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +## RELATED LINKS + +[Microsoft 365 Patterns and Practices](https://aka.ms/m365pnp) \ No newline at end of file diff --git a/src/Commands/PowerPlatform/PowerAutomate/ImportFlow.cs b/src/Commands/PowerPlatform/PowerAutomate/ImportFlow.cs index b0ea31f9a..e0b7ad0e5 100644 --- a/src/Commands/PowerPlatform/PowerAutomate/ImportFlow.cs +++ b/src/Commands/PowerPlatform/PowerAutomate/ImportFlow.cs @@ -1,6 +1,4 @@ -using Microsoft.Graph; -using Newtonsoft.Json.Serialization; -using PnP.PowerShell.Commands.Attributes; +using PnP.PowerShell.Commands.Attributes; using PnP.PowerShell.Commands.Base; using PnP.PowerShell.Commands.Base.PipeBinds; using PnP.PowerShell.Commands.Utilities; @@ -19,40 +17,107 @@ namespace PnP.PowerShell.Commands.PowerPlatform.PowerAutomate [RequiredApiDelegatedPermissions("azure/user_impersonation")] public class ImportFlow : PnPAzureManagementApiCmdlet { + private const string ParameterSet_BYIDENTITY = "By Identity"; + private const string ParameterSet_ALL = "All"; + + [Parameter(Mandatory = false, ValueFromPipeline = true, ParameterSetName = ParameterSet_BYIDENTITY)] + [Parameter(Mandatory = false, ValueFromPipeline = true, ParameterSetName = ParameterSet_ALL)] [Parameter(Mandatory = false)] public PowerPlatformEnvironmentPipeBind Environment; - [Parameter(Mandatory = true)] + [Parameter(Mandatory = true, ParameterSetName = ParameterSet_ALL)] public string PackagePath; - [Parameter(Mandatory = false)] - public SwitchParameter CreateAsNew; - - [Parameter(Mandatory = false)] + [Parameter(Mandatory = true, ParameterSetName = ParameterSet_ALL)] public string Name; protected override void ExecuteCmdlet() { - var environmentName = ParameterSpecified(nameof(Environment)) ? Environment.GetName() : PowerPlatformUtility.GetDefaultEnvironment(ArmRequestHelper, Connection.AzureEnvironment)?.Name; - + var environmentName = GetEnvironmentName(); string baseUrl = PowerPlatformUtility.GetBapEndpoint(Connection.AzureEnvironment); - // Step 1: Generate a storage URL for the package - var generateResourceUrlResponse = RestHelper.Post(Connection.HttpClient, $"{baseUrl}/providers/Microsoft.BusinessAppPlatform/environments/{environmentName}/generateResourceStorage?api-version=2016-11-01", AccessToken); - WriteVerbose($"Storage resource URL generated: {generateResourceUrlResponse}"); - // Parse the response to get the shared access signature URL - var resourceUrlData = JsonSerializer.Deserialize(generateResourceUrlResponse); - var sasUrl = resourceUrlData.GetProperty("sharedAccessSignature").GetString(); + //Get the SAS URL for the blob storage + var sasUrl = GenerateSasUrl(baseUrl, environmentName); + var blobUri = BuildBlobUri(sasUrl, PackagePath); + // Step 1: Upload the package to the blob storage using the SAS URL + UploadPackageToBlob(blobUri); + //Step 2: this will list the import parameters + var importParametersResponse = GetImportParameters(baseUrl, environmentName, blobUri); + // Step 3: Get the list of import operations data + var importOperationsData = GetImportOperations(importParametersResponse.Location.ToString()); + var propertiesElement = GetPropertiesElement(importOperationsData); + + ValidateProperties(propertiesElement); + var resourcesObject = ParseResources(propertiesElement); + // Step 4: Transform the resources object + var resource = TransformResources(resourcesObject); + var validatePackagePayload = CreateImportObject(propertiesElement, resourcesObject); + //Step 5: Validate the import package + var validateResponseData = ValidateImportPackage(baseUrl, environmentName, validatePackagePayload); - var fileName = Path.GetFileName(PackagePath); + var importPackagePayload = CreateImportObject(validateResponseData); + //Step 6: import package + var importResult = ImportPackage(baseUrl, environmentName, importPackagePayload); + //Step 7: Wait for the import to complete + var importStatus = WaitForImportCompletion(importResult.Location.ToString()); + + WriteObject($"Import {importStatus}"); + } + + private string GetEnvironmentName() + { + return ParameterSpecified(nameof(Environment)) + ? Environment.GetName() + : PowerPlatformUtility.GetDefaultEnvironment(ArmRequestHelper, Connection.AzureEnvironment)?.Name; + } + + private string GenerateSasUrl(string baseUrl, string environmentName) + { + var response = RestHelper.Post(Connection.HttpClient, $"{baseUrl}/providers/Microsoft.BusinessAppPlatform/environments/{environmentName}/generateResourceStorage?api-version=2016-11-01", AccessToken); + WriteVerbose($"Storage resource URL generated: {response}"); + var data = JsonSerializer.Deserialize(response); + return data.GetProperty("sharedAccessSignature").GetString(); + } + + private UriBuilder BuildBlobUri(string sasUrl, string packagePath) + { + var fileName = Path.GetFileName(packagePath); var blobUri = new UriBuilder(sasUrl); blobUri.Path += $"/{fileName}"; + return blobUri; + } - UploadPackageToBlob(blobUri); + private void UploadPackageToBlob(UriBuilder blobUri) + { + // Step 2: Upload the package to the blob storage using the SAS URL + + // Upload using clean HttpClient + using (var blobClient = new HttpClient()) + using (var packageFileStream = new FileStream(PackagePath, FileMode.Open, FileAccess.Read)) + { + var packageContent = new StreamContent(packageFileStream); + packageContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/octet-stream"); + + var request = new HttpRequestMessage(HttpMethod.Put, blobUri.Uri) + { + Content = packageContent + }; + request.Headers.Add("x-ms-blob-type", "BlockBlob"); - // Step 3: Get import parameters with the package link + var uploadResponse = blobClient.SendAsync(request).GetAwaiter().GetResult(); + + if (!uploadResponse.IsSuccessStatusCode) + { + var errorContent = uploadResponse.Content.ReadAsStringAsync().GetAwaiter().GetResult(); + throw new Exception($"Upload failed: {uploadResponse.StatusCode} - {errorContent}"); + } + } + } + + private System.Net.Http.Headers.HttpResponseHeaders GetImportParameters(string baseUrl, string environmentName, UriBuilder blobUri) + { var importPayload = new { packageLink = new @@ -60,8 +125,7 @@ protected override void ExecuteCmdlet() value = blobUri.Uri.ToString() } }; - - var importParametersResponse = RestHelper.PostGetResponseHeader( + var response = RestHelper.PostGetResponseHeader( Connection.HttpClient, $"{baseUrl}/providers/Microsoft.BusinessAppPlatform/environments/{environmentName}/listImportParameters?api-version=2016-11-01", AccessToken, @@ -69,28 +133,34 @@ protected override void ExecuteCmdlet() accept: "application/json" ); WriteVerbose("Import parameters retrieved"); + System.Threading.Thread.Sleep(2500); + return response; + } - System.Threading.Thread.Sleep(2500); //Wait 2.5 seconds to get the import parameters - - var importOperationsUrl = importParametersResponse.Location.ToString(); - + private JsonElement GetImportOperations(string importOperationsUrl) + { var listImportOperations = RestHelper.Get( Connection.HttpClient, importOperationsUrl, AccessToken, accept: "application/json" ); - WriteVerbose("Import operations retrieved"); + return JsonSerializer.Deserialize(listImportOperations); + } - var importOperationsData = JsonSerializer.Deserialize(listImportOperations); - + private JsonElement GetPropertiesElement(JsonElement importOperationsData) + { if (!importOperationsData.TryGetProperty("properties", out JsonElement propertiesElement)) { WriteObject("Import failed: 'properties' section missing."); - return; + throw new Exception("Import failed: 'properties' section missing."); } + return propertiesElement; + } + private void ValidateProperties(JsonElement propertiesElement) + { bool hasStatus = propertiesElement.TryGetProperty("status", out _); bool hasPackageLink = propertiesElement.TryGetProperty("packageLink", out _); bool hasDetails = propertiesElement.TryGetProperty("details", out _); @@ -99,61 +169,29 @@ protected override void ExecuteCmdlet() if (!(hasStatus && hasPackageLink && hasDetails && hasResources)) { WriteObject("Import failed: One or more required fields are missing in 'properties'."); - return; + throw new Exception("Import failed: One or more required fields are missing in 'properties'."); } if (!propertiesElement.TryGetProperty("resources", out JsonElement resourcesElement)) { WriteObject("Import failed: 'resources' section missing in 'properties'."); return; } - - var resourcesObject = JsonNode.Parse(resourcesElement.GetRawText()) as JsonObject; - var resource = TransformResources(resourcesObject); - - // Update the "resources" in the propertiesElement - var validatePackagePayload = CreateImportObject(propertiesElement, resourcesObject); - - var validateResponse = RestHelper.Post(Connection.HttpClient, $"{baseUrl}/providers/Microsoft.BusinessAppPlatform/environments/{environmentName}/validateImportPackage?api-version=2016-11-01", AccessToken, payload: validatePackagePayload); - var validateResponseData = JsonSerializer.Deserialize(validateResponse); - - var importPackagePayload = CreateImportObject(validateResponseData); - - var importResult = RestHelper.PostGetResponseHeader(Connection.HttpClient, $"{baseUrl}/providers/Microsoft.BusinessAppPlatform/environments/{environmentName}/importPackage?api-version=2016-11-01", AccessToken, payload: importPackagePayload, accept: "application/json"); - WriteVerbose("Import package initiated"); - - var importPackageResponseUrl = importResult.Location.ToString(); - - var importStatus = WaitForImportCompletion(importPackageResponseUrl); - - WriteObject($"Import {importStatus}"); } - private void UploadPackageToBlob(UriBuilder blobUri) + private JsonObject ParseResources(JsonElement propertiesElement) { - // Step 2: Upload the package to the blob storage using the SAS URL - - // Upload using clean HttpClient - using (var blobClient = new HttpClient()) - using (var packageFileStream = new FileStream(PackagePath, FileMode.Open, FileAccess.Read)) + if (!propertiesElement.TryGetProperty("resources", out JsonElement resourcesElement)) { - var packageContent = new StreamContent(packageFileStream); - packageContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/octet-stream"); - - var request = new HttpRequestMessage(HttpMethod.Put, blobUri.Uri) - { - Content = packageContent - }; - - request.Headers.Add("x-ms-blob-type", "BlockBlob"); - - var uploadResponse = blobClient.SendAsync(request).GetAwaiter().GetResult(); - - if (!uploadResponse.IsSuccessStatusCode) - { - var errorContent = uploadResponse.Content.ReadAsStringAsync().GetAwaiter().GetResult(); - throw new Exception($"Upload failed: {uploadResponse.StatusCode} - {errorContent}"); - } + WriteObject("Import failed: 'resources' section missing in 'properties'."); + throw new Exception("Import failed: 'resources' section missing in 'properties'."); } + return JsonNode.Parse(resourcesElement.GetRawText()) as JsonObject; + } + + private JsonElement ValidateImportPackage(string baseUrl, string environmentName, JsonObject validatePackagePayload) + { + var validateResponse = RestHelper.Post(Connection.HttpClient, $"{baseUrl}/providers/Microsoft.BusinessAppPlatform/environments/{environmentName}/validateImportPackage?api-version=2016-11-01", AccessToken, payload: validatePackagePayload); + return JsonSerializer.Deserialize(validateResponse); } private JsonObject TransformResources(JsonObject resourcesObject) @@ -169,24 +207,12 @@ private JsonObject TransformResources(JsonObject resourcesObject) if (resourceType == "Microsoft.Flow/flows") { - if (CreateAsNew) + resource["selectedCreationType"] = "New"; + if (ParameterSpecified(nameof(Name))) { - resource["selectedCreationType"] = "New"; - if (ParameterSpecified(nameof(Name))) + if (resource.TryGetPropertyValue("details", out JsonNode detailsNode) && detailsNode is JsonObject detailsObject) { - if (resource.TryGetPropertyValue("details", out JsonNode detailsNode) && detailsNode is JsonObject detailsObject) - { - detailsObject["displayName"] = Name; - } - } - - } - else - { - resource["selectedCreationType"] = "Existing"; - if (resource.TryGetPropertyValue("suggestedId", out JsonNode suggestedIdNode) && suggestedIdNode != null) - { - resource["id"] = JsonValue.Create(suggestedIdNode.ToString()); + detailsObject["displayName"] = Name; } } } @@ -217,6 +243,19 @@ private JsonObject CreateImportObject(JsonElement importData, JsonObject resourc return resourcesObject; } + private System.Net.Http.Headers.HttpResponseHeaders ImportPackage(string baseUrl, string environmentName, JsonObject importPackagePayload) + { + var importResult = RestHelper.PostGetResponseHeader( + Connection.HttpClient, + $"{baseUrl}/providers/Microsoft.BusinessAppPlatform/environments/{environmentName}/importPackage?api-version=2016-11-01", + AccessToken, + payload: importPackagePayload, + accept: "application/json" + ); + WriteVerbose("Import package initiated"); + return importResult; + } + private string WaitForImportCompletion(string importPackageResponseUrl) { string status; @@ -224,6 +263,7 @@ private string WaitForImportCompletion(string importPackageResponseUrl) do { + System.Threading.Thread.Sleep(2500); var importResultData = RestHelper.Get(Connection.HttpClient, importPackageResponseUrl, AccessToken, accept: "application/json"); var importResultDataElement = JsonSerializer.Deserialize(importResultData); @@ -238,12 +278,16 @@ private string WaitForImportCompletion(string importPackageResponseUrl) throw new Exception("Import status could not be determined."); } + if (status == "Running") { WriteVerbose("Import is still running. Waiting for completion..."); - System.Threading.Thread.Sleep(2500); // Wait for 2.5 seconds before retrying retryCount++; } + else if (status == "Failed") + { + ThrowImportError(importResultData); + } } while (status == "Running" && retryCount < 5); if (status == "Running") @@ -253,5 +297,31 @@ private string WaitForImportCompletion(string importPackageResponseUrl) return status; } + + private void ThrowImportError(string importResultData) + { + var importErrorResultData = JsonSerializer.Deserialize(importResultData); + if (importErrorResultData.TryGetProperty("properties", out JsonElement importErrorResultPropertiesElement) && + importErrorResultPropertiesElement.TryGetProperty("resources", out JsonElement resourcesElement)) + { + foreach (var resource in resourcesElement.EnumerateObject()) + { + if (resource.Value.TryGetProperty("error", out JsonElement errorElement)) + { + string errorMessage = errorElement.TryGetProperty("message", out JsonElement messageElement) + ? messageElement.GetString() + : errorElement.TryGetProperty("code", out JsonElement codeElement) + ? codeElement.GetString() + : "Unknown error"; + throw new Exception($"Import failed: {errorMessage}"); + } + } + throw new Exception("Import failed: No error details found in resources."); + } + else + { + throw new Exception("Import failed: Unknown error."); + } + } } }