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/5] 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/5] 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/5] 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."); + } + } } } From 163c8936bea9f7ecf5f9d3204c52fbdcbad242c4 Mon Sep 17 00:00:00 2001 From: Nishkalank Bezawada Date: Fri, 3 Oct 2025 10:22:59 +0200 Subject: [PATCH 4/5] Refactoring ImportFlow --- .../PowerPlatform/PowerAutomate/ImportFlow.cs | 300 +----------------- src/Commands/Utilities/ImportFlowUtility.cs | 258 +++++++++++++++ 2 files changed, 274 insertions(+), 284 deletions(-) create mode 100644 src/Commands/Utilities/ImportFlowUtility.cs diff --git a/src/Commands/PowerPlatform/PowerAutomate/ImportFlow.cs b/src/Commands/PowerPlatform/PowerAutomate/ImportFlow.cs index e0b7ad0e5..ae1d46347 100644 --- a/src/Commands/PowerPlatform/PowerAutomate/ImportFlow.cs +++ b/src/Commands/PowerPlatform/PowerAutomate/ImportFlow.cs @@ -2,13 +2,8 @@ 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 { @@ -35,32 +30,21 @@ protected override void ExecuteCmdlet() { var environmentName = GetEnvironmentName(); string baseUrl = PowerPlatformUtility.GetBapEndpoint(Connection.AzureEnvironment); - - //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 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()); + var sasUrl = ImportFlowUtility.GenerateSasUrl(Connection.HttpClient, AccessToken, baseUrl, environmentName); + var blobUri = ImportFlowUtility.BuildBlobUri(sasUrl, PackagePath); + ImportFlowUtility.UploadPackageToBlob(blobUri, PackagePath); + var importParametersResponse = ImportFlowUtility.GetImportParameters(Connection.HttpClient, AccessToken, baseUrl, environmentName, blobUri); + var importOperationsData = ImportFlowUtility.GetImportOperations(Connection.HttpClient, AccessToken, importParametersResponse.Location.ToString()); + var propertiesElement = ImportFlowUtility.GetPropertiesElement(importOperationsData); + + ImportFlowUtility.ValidateProperties(propertiesElement); + var resourcesObject = ImportFlowUtility.ParseResources(propertiesElement); + var resource = ImportFlowUtility.TransformResources(resourcesObject, Name); + var validatePackagePayload = ImportFlowUtility.CreateImportObject(propertiesElement, resourcesObject); + var validateResponseData = ImportFlowUtility.ValidateImportPackage(Connection.HttpClient, AccessToken, baseUrl, environmentName, validatePackagePayload); + var importPackagePayload = ImportFlowUtility.CreateImportObject(validateResponseData); + var importResult = ImportFlowUtility.ImportPackage(Connection.HttpClient, AccessToken, baseUrl, environmentName, importPackagePayload); + var importStatus = ImportFlowUtility.WaitForImportCompletion(Connection.HttpClient, AccessToken, importResult.Location.ToString()); WriteObject($"Import {importStatus}"); } @@ -71,257 +55,5 @@ private string GetEnvironmentName() ? 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; - } - - 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 System.Net.Http.Headers.HttpResponseHeaders GetImportParameters(string baseUrl, string environmentName, UriBuilder blobUri) - { - var importPayload = new - { - packageLink = new - { - value = blobUri.Uri.ToString() - } - }; - var response = 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"); - System.Threading.Thread.Sleep(2500); - return response; - } - - private JsonElement GetImportOperations(string importOperationsUrl) - { - var listImportOperations = RestHelper.Get( - Connection.HttpClient, - importOperationsUrl, - AccessToken, - accept: "application/json" - ); - WriteVerbose("Import operations retrieved"); - return JsonSerializer.Deserialize(listImportOperations); - } - - private JsonElement GetPropertiesElement(JsonElement importOperationsData) - { - if (!importOperationsData.TryGetProperty("properties", out JsonElement propertiesElement)) - { - WriteObject("Import failed: 'properties' section missing."); - 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 _); - bool hasResources = propertiesElement.TryGetProperty("resources", out _); - - if (!(hasStatus && hasPackageLink && hasDetails && hasResources)) - { - WriteObject("Import failed: One or more required fields are missing in 'properties'."); - 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; - } - } - - private JsonObject ParseResources(JsonElement propertiesElement) - { - if (!propertiesElement.TryGetProperty("resources", out JsonElement resourcesElement)) - { - 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) - { - 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") - { - resource["selectedCreationType"] = "New"; - if (ParameterSpecified(nameof(Name))) - { - if (resource.TryGetPropertyValue("details", out JsonNode detailsNode) && detailsNode is JsonObject detailsObject) - { - detailsObject["displayName"] = Name; - } - } - } - 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; - } - - 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; - int retryCount = 0; - - do - { - System.Threading.Thread.Sleep(2500); - 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..."); - retryCount++; - } - else if (status == "Failed") - { - ThrowImportError(importResultData); - } - } while (status == "Running" && retryCount < 5); - - if (status == "Running") - { - throw new Exception("Import failed to complete after 5 attempts."); - } - - 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."); - } - } } } diff --git a/src/Commands/Utilities/ImportFlowUtility.cs b/src/Commands/Utilities/ImportFlowUtility.cs new file mode 100644 index 000000000..26b760ba4 --- /dev/null +++ b/src/Commands/Utilities/ImportFlowUtility.cs @@ -0,0 +1,258 @@ +using PnP.PowerShell.Commands.Utilities.REST; +using PnP.PowerShell.Commands.Base; +using System; +using System.IO; +using System.Text.Json; +using PnP.Framework.Diagnostics; +using System.Net.Http; +using System.Text.Json.Nodes; + +namespace PnP.PowerShell.Commands.Utilities +{ + internal static class ImportFlowUtility + { + public static string GenerateSasUrl(HttpClient httpClient, string accessToken, string baseUrl, string environmentName) + { + var response = RestHelper.Post(PnPConnection.Current.HttpClient, $"{baseUrl}/providers/Microsoft.BusinessAppPlatform/environments/{environmentName}/generateResourceStorage?api-version=2016-11-01", accessToken); + var data = JsonSerializer.Deserialize(response); + return data.GetProperty("sharedAccessSignature").GetString(); + } + + public static UriBuilder BuildBlobUri(string sasUrl, string packagePath) + { + var fileName = Path.GetFileName(packagePath); + var blobUri = new UriBuilder(sasUrl); + blobUri.Path += $"/{fileName}"; + return blobUri; + } + + public static void UploadPackageToBlob(UriBuilder blobUri, string PackagePath) + { + 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}"); + } + } + } + + public static System.Net.Http.Headers.HttpResponseHeaders GetImportParameters(HttpClient httpClient, string accessToken, string baseUrl, string environmentName, UriBuilder blobUri) + { + var importPayload = new + { + packageLink = new + { + value = blobUri.Uri.ToString() + } + }; + var response = RestHelper.PostGetResponseHeader( + httpClient, + $"{baseUrl}/providers/Microsoft.BusinessAppPlatform/environments/{environmentName}/listImportParameters?api-version=2016-11-01", + accessToken, + payload: importPayload, + accept: "application/json" + ); + Log.Debug("ImportFlowUtility", "Import parameters retrieved"); + System.Threading.Thread.Sleep(2500); + return response; + } + + public static JsonElement GetImportOperations(HttpClient httpClient, string accessToken, string importOperationsUrl) + { + var listImportOperations = RestHelper.Get( + httpClient, + importOperationsUrl, + accessToken, + accept: "application/json" + ); + Log.Debug("ImportFlowUtility", "Import operations retrieved"); + return JsonSerializer.Deserialize(listImportOperations); + } + + public static JsonElement GetPropertiesElement(JsonElement importOperationsData) + { + if (!importOperationsData.TryGetProperty("properties", out JsonElement propertiesElement)) + { + throw new Exception("Import failed: 'properties' section missing."); + } + return propertiesElement; + } + + public static void ValidateProperties(JsonElement propertiesElement) + { + 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)) + { + throw new Exception("Import failed: One or more required fields are missing in 'properties'."); + } + if (!propertiesElement.TryGetProperty("resources", out JsonElement resourcesElement)) + { + throw new Exception("Import failed: 'resources' section missing in 'properties'."); + } + } + + public static JsonObject ParseResources(JsonElement propertiesElement) + { + if (!propertiesElement.TryGetProperty("resources", out JsonElement resourcesElement)) + { + throw new Exception("Import failed: 'resources' section missing in 'properties'."); + } + return JsonNode.Parse(resourcesElement.GetRawText()) as JsonObject; + } + + public static JsonElement ValidateImportPackage(HttpClient httpClient, string accessToken, string baseUrl, string environmentName, JsonObject validatePackagePayload) + { + var validateResponse = RestHelper.Post(httpClient, $"{baseUrl}/providers/Microsoft.BusinessAppPlatform/environments/{environmentName}/validateImportPackage?api-version=2016-11-01", accessToken, payload: validatePackagePayload); + return JsonSerializer.Deserialize(validateResponse); + } + + public static JsonObject TransformResources(JsonObject resourcesObject, string Name) + { + 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") + { + resource["selectedCreationType"] = "New"; + if (Name != null) + { + if (resource.TryGetPropertyValue("details", out JsonNode detailsNode) && detailsNode is JsonObject detailsObject) + { + detailsObject["displayName"] = Name; + } + } + } + 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; + } + + public static 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; + } + + public static System.Net.Http.Headers.HttpResponseHeaders ImportPackage(HttpClient httpClient, string accessToken, string baseUrl, string environmentName, JsonObject importPackagePayload) + { + var importResult = RestHelper.PostGetResponseHeader( + httpClient, + $"{baseUrl}/providers/Microsoft.BusinessAppPlatform/environments/{environmentName}/importPackage?api-version=2016-11-01", + accessToken, + payload: importPackagePayload, + accept: "application/json" + ); + Log.Debug("ImportFlowUtility", "Import package initiated"); + return importResult; + } + + public static string WaitForImportCompletion(HttpClient httpClient, string accessToken, string importPackageResponseUrl) + { + string status; + int retryCount = 0; + + do + { + System.Threading.Thread.Sleep(2500); + var importResultData = RestHelper.Get(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 + { + Log.Warning("ImportFlowUtility", "Failed to retrieve the status from the response."); + throw new Exception("Import status could not be determined."); + } + + + if (status == "Running") + { + Log.Debug("ImportFlowUtility", "Import is still running. Waiting for completion..."); + retryCount++; + } + else if (status == "Failed") + { + ThrowImportError(importResultData); + } + } while (status == "Running" && retryCount < 5); + + if (status == "Running") + { + throw new Exception("Import failed to complete after 5 attempts."); + } + + return status; + } + + public static 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."); + } + } + } +} From 3a9850bf70525ee730c3c27a04e299b386dc396e Mon Sep 17 00:00:00 2001 From: Nishkalank Bezawada Date: Fri, 3 Oct 2025 11:40:25 +0200 Subject: [PATCH 5/5] Final commit after refactoring and updating documentation --- documentation/Import-PnPFlow.md | 2 +- .../PowerAutomate/ImportFlowResult.cs | 22 +++++ .../PowerPlatform/PowerAutomate/ImportFlow.cs | 19 +---- src/Commands/Utilities/ImportFlowUtility.cs | 83 +++++++++++++++---- 4 files changed, 91 insertions(+), 35 deletions(-) create mode 100644 src/Commands/Model/PowerPlatform/PowerAutomate/ImportFlowResult.cs diff --git a/documentation/Import-PnPFlow.md b/documentation/Import-PnPFlow.md index dc1bdf173..c645ece58 100644 --- a/documentation/Import-PnPFlow.md +++ b/documentation/Import-PnPFlow.md @@ -26,7 +26,7 @@ Import-PnPFlow [-Environment ] [-PackagePath < ``` ## DESCRIPTION -This cmdlet Imports a Microsoft Power Automate Flow from a zip package. +This cmdlet imports a Microsoft Power Automate Flow from a ZIP package. At present, only flows originating from the same tenant are supported. 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. diff --git a/src/Commands/Model/PowerPlatform/PowerAutomate/ImportFlowResult.cs b/src/Commands/Model/PowerPlatform/PowerAutomate/ImportFlowResult.cs new file mode 100644 index 000000000..1805584d9 --- /dev/null +++ b/src/Commands/Model/PowerPlatform/PowerAutomate/ImportFlowResult.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace PnP.PowerShell.Commands.Model.PowerPlatform.PowerAutomate +{ + public class ImportFlowResult + { + public string Name { get; set; } + public string Status { get; set; } + public ImportFlowDetails Details { get; set; } + } + + public class ImportFlowDetails + { + public string DisplayName { get; set; } + public string Description { get; set; } + public DateTime CreatedTime { get; set; } + } +} diff --git a/src/Commands/PowerPlatform/PowerAutomate/ImportFlow.cs b/src/Commands/PowerPlatform/PowerAutomate/ImportFlow.cs index ae1d46347..a8486a12e 100644 --- a/src/Commands/PowerPlatform/PowerAutomate/ImportFlow.cs +++ b/src/Commands/PowerPlatform/PowerAutomate/ImportFlow.cs @@ -30,23 +30,8 @@ protected override void ExecuteCmdlet() { var environmentName = GetEnvironmentName(); string baseUrl = PowerPlatformUtility.GetBapEndpoint(Connection.AzureEnvironment); - var sasUrl = ImportFlowUtility.GenerateSasUrl(Connection.HttpClient, AccessToken, baseUrl, environmentName); - var blobUri = ImportFlowUtility.BuildBlobUri(sasUrl, PackagePath); - ImportFlowUtility.UploadPackageToBlob(blobUri, PackagePath); - var importParametersResponse = ImportFlowUtility.GetImportParameters(Connection.HttpClient, AccessToken, baseUrl, environmentName, blobUri); - var importOperationsData = ImportFlowUtility.GetImportOperations(Connection.HttpClient, AccessToken, importParametersResponse.Location.ToString()); - var propertiesElement = ImportFlowUtility.GetPropertiesElement(importOperationsData); - - ImportFlowUtility.ValidateProperties(propertiesElement); - var resourcesObject = ImportFlowUtility.ParseResources(propertiesElement); - var resource = ImportFlowUtility.TransformResources(resourcesObject, Name); - var validatePackagePayload = ImportFlowUtility.CreateImportObject(propertiesElement, resourcesObject); - var validateResponseData = ImportFlowUtility.ValidateImportPackage(Connection.HttpClient, AccessToken, baseUrl, environmentName, validatePackagePayload); - var importPackagePayload = ImportFlowUtility.CreateImportObject(validateResponseData); - var importResult = ImportFlowUtility.ImportPackage(Connection.HttpClient, AccessToken, baseUrl, environmentName, importPackagePayload); - var importStatus = ImportFlowUtility.WaitForImportCompletion(Connection.HttpClient, AccessToken, importResult.Location.ToString()); - - WriteObject($"Import {importStatus}"); + var importStatus = ImportFlowUtility.ExecuteImportFlow(Connection.HttpClient,AccessToken,baseUrl,environmentName,PackagePath,Name); + WriteObject(importStatus); } private string GetEnvironmentName() diff --git a/src/Commands/Utilities/ImportFlowUtility.cs b/src/Commands/Utilities/ImportFlowUtility.cs index 26b760ba4..7769cd316 100644 --- a/src/Commands/Utilities/ImportFlowUtility.cs +++ b/src/Commands/Utilities/ImportFlowUtility.cs @@ -6,14 +6,33 @@ using PnP.Framework.Diagnostics; using System.Net.Http; using System.Text.Json.Nodes; +using PnP.PowerShell.Commands.Model.PowerPlatform.PowerAutomate; +using System.Threading; namespace PnP.PowerShell.Commands.Utilities { internal static class ImportFlowUtility { + public static ImportFlowResult ExecuteImportFlow(HttpClient httpClient, string accessToken, string baseUrl, string environmentName, string packagePath, string name) + { + var sasUrl = GenerateSasUrl(httpClient, accessToken, baseUrl, environmentName); + var blobUri = BuildBlobUri(sasUrl, packagePath); + UploadPackageToBlob(blobUri, packagePath); + var importParametersResponse = GetImportParameters(httpClient, accessToken, baseUrl, environmentName, blobUri); + var importOperationsData = GetImportOperations(httpClient, accessToken, importParametersResponse.Location.ToString()); + var propertiesElement = GetPropertiesElement(importOperationsData); + ValidateProperties(propertiesElement); + var resourcesObject = ParseResources(propertiesElement); + var resource = TransformResources(resourcesObject, name); + var validatePackagePayload = CreateImportObject(propertiesElement, resourcesObject); + var validateResponseData = ValidateImportPackage(httpClient, accessToken, baseUrl, environmentName, validatePackagePayload); + var importPackagePayload = CreateImportObject(validateResponseData); + var importResult = ImportPackage(httpClient, accessToken, baseUrl, environmentName, importPackagePayload); + return WaitForImportCompletion(httpClient, accessToken, importResult.Location.ToString()); + } public static string GenerateSasUrl(HttpClient httpClient, string accessToken, string baseUrl, string environmentName) { - var response = RestHelper.Post(PnPConnection.Current.HttpClient, $"{baseUrl}/providers/Microsoft.BusinessAppPlatform/environments/{environmentName}/generateResourceStorage?api-version=2016-11-01", accessToken); + var response = RestHelper.Post(httpClient, $"{baseUrl}/providers/Microsoft.BusinessAppPlatform/environments/{environmentName}/generateResourceStorage?api-version=2016-11-01", accessToken); var data = JsonSerializer.Deserialize(response); return data.GetProperty("sharedAccessSignature").GetString(); } @@ -187,19 +206,20 @@ public static System.Net.Http.Headers.HttpResponseHeaders ImportPackage(HttpClie return importResult; } - public static string WaitForImportCompletion(HttpClient httpClient, string accessToken, string importPackageResponseUrl) + public static ImportFlowResult WaitForImportCompletion(HttpClient httpClient,string accessToken,string importPackageResponseUrl) { - string status; + string status = null; int retryCount = 0; + JsonElement importResultDataElement = default; do { - System.Threading.Thread.Sleep(2500); + Thread.Sleep(2500); var importResultData = RestHelper.Get(httpClient, importPackageResponseUrl, accessToken, accept: "application/json"); - var importResultDataElement = JsonSerializer.Deserialize(importResultData); + importResultDataElement = JsonSerializer.Deserialize(importResultData); - if (importResultDataElement.TryGetProperty("properties", out JsonElement importResultPropertiesElement) && - importResultPropertiesElement.TryGetProperty("status", out JsonElement statusElement)) + if (importResultDataElement.TryGetProperty("properties", out JsonElement propertiesElement) && + propertiesElement.TryGetProperty("status", out JsonElement statusElement)) { status = statusElement.GetString(); } @@ -209,7 +229,6 @@ public static string WaitForImportCompletion(HttpClient httpClient, string acces throw new Exception("Import status could not be determined."); } - if (status == "Running") { Log.Debug("ImportFlowUtility", "Import is still running. Waiting for completion..."); @@ -217,23 +236,23 @@ public static string WaitForImportCompletion(HttpClient httpClient, string acces } else if (status == "Failed") { - ThrowImportError(importResultData); + ThrowImportError(importResultDataElement); } + } while (status == "Running" && retryCount < 5); if (status == "Running") { throw new Exception("Import failed to complete after 5 attempts."); } - - return status; + return MapToImportFlowResult(importResultDataElement); } - public static void ThrowImportError(string importResultData) + + public static void ThrowImportError(JsonElement importErrorResultData) { - var importErrorResultData = JsonSerializer.Deserialize(importResultData); - if (importErrorResultData.TryGetProperty("properties", out JsonElement importErrorResultPropertiesElement) && - importErrorResultPropertiesElement.TryGetProperty("resources", out JsonElement resourcesElement)) + if (importErrorResultData.TryGetProperty("properties", out JsonElement propertiesElement) && + propertiesElement.TryGetProperty("resources", out JsonElement resourcesElement)) { foreach (var resource in resourcesElement.EnumerateObject()) { @@ -244,15 +263,45 @@ public static void ThrowImportError(string importResultData) : 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."); + } + + + private static ImportFlowResult MapToImportFlowResult(JsonElement importResultDataElement) + { + var result = new ImportFlowResult(); + + if (importResultDataElement.TryGetProperty("name", out var nameElement)) { - throw new Exception("Import failed: Unknown error."); + result.Name = nameElement.GetString(); } + + if (importResultDataElement.TryGetProperty("properties", out var propertiesElement)) + { + if (propertiesElement.TryGetProperty("status", out var statusElement)) + { + result.Status = statusElement.GetString(); + } + + var details = new ImportFlowDetails(); + if (propertiesElement.TryGetProperty("details", out var detailsElement)) + { + details.DisplayName = detailsElement.GetProperty("displayName").GetString(); + details.Description = detailsElement.GetProperty("description").GetString(); + details.CreatedTime = detailsElement.GetProperty("createdTime").GetDateTime(); + } + result.Details = details; + } + + return result; } + } }