From 372b3f2c4c0a015986b91c0e489cda91998e0265 Mon Sep 17 00:00:00 2001 From: Neil H <46364153+08nholloway@users.noreply.github.com> Date: Tue, 20 Apr 2021 06:15:05 -0700 Subject: [PATCH] [Blueprint] Close #2723: add export feature for blueprint and artifacts (#3259) * added test and input files * style checks * changed param name to match import params, added example * fix linter issues * removed unnecessary AE variable * update history and setup with latest version and release note * change minor version * removed --force flag and replaced with skip_confirmation. updated params other files * Update src/blueprint/setup.py Co-authored-by: Feng Zhou <55177366+fengzhou-msft@users.noreply.github.com> Co-authored-by: Feng Zhou <55177366+fengzhou-msft@users.noreply.github.com> --- src/blueprint/HISTORY.rst | 4 + src/blueprint/azext_blueprint/_help.py | 10 + src/blueprint/azext_blueprint/_params.py | 5 + src/blueprint/azext_blueprint/commands.py | 1 + src/blueprint/azext_blueprint/custom.py | 39 +- .../input/artifacts/policyStorageTags.json | 17 + .../input/artifacts/policyTags.json | 17 + .../input/artifacts/roleContributor.json | 8 + .../input/artifacts/roleOwner.json | 9 + .../input/blueprint.json | 58 ++ .../recordings/test_blueprint_export.yaml | 621 ++++++++++++++++++ .../tests/latest/test_blueprint_scenario.py | 91 +++ src/blueprint/setup.py | 3 +- 13 files changed, 879 insertions(+), 4 deletions(-) create mode 100644 src/blueprint/azext_blueprint/tests/latest/input/export_with_artifacts/input/artifacts/policyStorageTags.json create mode 100644 src/blueprint/azext_blueprint/tests/latest/input/export_with_artifacts/input/artifacts/policyTags.json create mode 100644 src/blueprint/azext_blueprint/tests/latest/input/export_with_artifacts/input/artifacts/roleContributor.json create mode 100644 src/blueprint/azext_blueprint/tests/latest/input/export_with_artifacts/input/artifacts/roleOwner.json create mode 100644 src/blueprint/azext_blueprint/tests/latest/input/export_with_artifacts/input/blueprint.json create mode 100644 src/blueprint/azext_blueprint/tests/latest/recordings/test_blueprint_export.yaml diff --git a/src/blueprint/HISTORY.rst b/src/blueprint/HISTORY.rst index 3b6bbe4ad68..ccfba9ce898 100644 --- a/src/blueprint/HISTORY.rst +++ b/src/blueprint/HISTORY.rst @@ -3,6 +3,10 @@ Release History =============== +0.3.0 ++++++ +* Allow user to export blueprint and artifacts to local directory + 0.2.1 +++++ * Support removing depends_on relationships for artifacts in update command diff --git a/src/blueprint/azext_blueprint/_help.py b/src/blueprint/azext_blueprint/_help.py index 95cc937ebb2..1ba5ab36e58 100644 --- a/src/blueprint/azext_blueprint/_help.py +++ b/src/blueprint/azext_blueprint/_help.py @@ -87,6 +87,16 @@ --input-path "path/to/blueprint/directory" """ +helps['blueprint export'] = """ + type: command + short-summary: Export a blueprint definition and artifacts to json file(s). + examples: + - name: Export a blueprint definition and artifacts + text: |- + az blueprint export --name MyBlueprint \\ + --output-path "path/to/blueprint/directory" +""" + helps['blueprint resource-group'] = """ type: group short-summary: Commands to manage blueprint resource group artifact. diff --git a/src/blueprint/azext_blueprint/_params.py b/src/blueprint/azext_blueprint/_params.py index 405b743b055..fd72f55d68f 100644 --- a/src/blueprint/azext_blueprint/_params.py +++ b/src/blueprint/azext_blueprint/_params.py @@ -85,6 +85,11 @@ def load_arguments(self, _): with self.argument_context('blueprint list') as c: pass + with self.argument_context('blueprint export') as c: + c.argument('blueprint_name', options_list=['--name', '-n'], help='Name of the blueprint definition to export.') + c.argument('output_path', type=file_type, help='The directory path for json definitions of the blueprint and artifacts. The blueprint definition file will be named blueprint.json. Artifacts json files will be in a subdirectory named artifacts.', completer=FilesCompleter()) + c.argument('skip_confirmation', action='store_true', options_list=['--yes', '-y'], help='Skip user confirmation. When set, if directory does not exist, it will be created. If the directory exists and has contents, they will be overwritten. If not set, user will be prompted for permission to proceed') + with self.argument_context('blueprint artifact delete') as c: c.argument('blueprint_name', help='Name of the blueprint definition.') c.argument('artifact_name', options_list=['--name', '-n'], help='Name of the blueprint artifact.') diff --git a/src/blueprint/azext_blueprint/commands.py b/src/blueprint/azext_blueprint/commands.py index 295427b991c..8afe6a8b96c 100644 --- a/src/blueprint/azext_blueprint/commands.py +++ b/src/blueprint/azext_blueprint/commands.py @@ -26,6 +26,7 @@ def load_command_table(self, _): g.custom_show_command('show', 'get_blueprint') g.custom_command('list', 'list_blueprint') g.custom_command('import', 'import_blueprint_with_artifacts', confirmation="This operation will overwrite any unpublished changes if the blueprint already exists.") + g.custom_command('export', 'export_blueprint_with_artifacts') with self.command_group('blueprint resource-group', blueprint_blueprints, client_factory=cf_blueprints) as g: g.custom_command('add', 'add_blueprint_resource_group') diff --git a/src/blueprint/azext_blueprint/custom.py b/src/blueprint/azext_blueprint/custom.py index bdf6c14b6ef..c475dbe1881 100644 --- a/src/blueprint/azext_blueprint/custom.py +++ b/src/blueprint/azext_blueprint/custom.py @@ -11,6 +11,9 @@ import json import os from knack.util import CLIError +from azure.cli.core.util import user_confirmation +from msrestazure.azure_exceptions import CloudError +from ._client_factory import cf_artifacts def import_blueprint_with_artifacts(cmd, @@ -20,8 +23,6 @@ def import_blueprint_with_artifacts(cmd, management_group=None, subscription=None, scope=None): - from ._client_factory import cf_artifacts - artifact_client = cf_artifacts(cmd.cli_ctx) body = {} blueprint_path = os.path.join(input_path, 'blueprint.json') @@ -125,6 +126,39 @@ def list_blueprint(cmd, client, management_group=None, subscription=None, scope= return client.list(scope=scope) +def export_blueprint_with_artifacts(cmd, client, blueprint_name, output_path, skip_confirmation=False, management_group=None, subscription=None, scope=None, **kwargs): + # match folder structure required for import_blueprint_with_artifact + blueprint_parent_folder = os.path.join(os.path.abspath(output_path), blueprint_name) + blueprint_file_location = os.path.join(blueprint_parent_folder, 'blueprint.json') + artifacts_location = os.path.join(blueprint_parent_folder, 'artifacts') + + if os.path.exists(blueprint_parent_folder) and os.listdir(blueprint_parent_folder) and not skip_confirmation: + user_prompt = f"That directory already contains a folder with the name {blueprint_name}. Would you like to continue?" + user_confirmation(user_prompt) + + try: + blueprint = client.get(scope=scope, blueprint_name=blueprint_name) + serialized_blueprint = blueprint.serialize() + except CloudError as error: + raise CLIError('Unable to export blueprint: {}'.format(str(error.message))) + + os.makedirs(artifacts_location, exist_ok=True) + + with open(blueprint_file_location, 'w') as f: + json.dump(serialized_blueprint, f, indent=4) + + artifact_client = cf_artifacts(cmd.cli_ctx) + available_artifacts = artifact_client.list(scope=scope, blueprint_name=blueprint_name) + + for artifact in available_artifacts: + artifact_file_location = os.path.join(artifacts_location, artifact.name + '.json') + serialized_artifact = artifact.serialize() + with open(artifact_file_location, 'w') as f: + json.dump(serialized_artifact, f, indent=4) + + return blueprint + + def delete_blueprint_artifact(cmd, client, blueprint_name, artifact_name, management_group=None, subscription=None, scope=None): return client.delete(scope=scope, @@ -506,7 +540,6 @@ def create_blueprint_assignment(cmd, locks_mode=None, locks_excluded_principals=None, parameters=None): - from msrestazure.azure_exceptions import CloudError from .vendored_sdks.blueprint.models._blueprint_management_client_enums import ManagedServiceIdentityType try: result = client.get(scope=scope, assignment_name=assignment_name) diff --git a/src/blueprint/azext_blueprint/tests/latest/input/export_with_artifacts/input/artifacts/policyStorageTags.json b/src/blueprint/azext_blueprint/tests/latest/input/export_with_artifacts/input/artifacts/policyStorageTags.json new file mode 100644 index 00000000000..e137ff002dd --- /dev/null +++ b/src/blueprint/azext_blueprint/tests/latest/input/export_with_artifacts/input/artifacts/policyStorageTags.json @@ -0,0 +1,17 @@ +{ + "kind": "policyAssignment", + "properties": { + "displayName": "Apply storage tag to resource group", + "description": "Apply storage tag and the parameter also used by the template to resource groups", + "dependsOn": [], + "policyDefinitionId": "/providers/Microsoft.Authorization/policyDefinitions/49c88fc8-6fd1-46fd-a676-f12d1d3a4c71", + "parameters": { + "tagName": { + "value": "StorageType" + }, + "tagValue": { + "value": "[parameters('storageAccountType')]" + } + } + } +} \ No newline at end of file diff --git a/src/blueprint/azext_blueprint/tests/latest/input/export_with_artifacts/input/artifacts/policyTags.json b/src/blueprint/azext_blueprint/tests/latest/input/export_with_artifacts/input/artifacts/policyTags.json new file mode 100644 index 00000000000..73f48e912ea --- /dev/null +++ b/src/blueprint/azext_blueprint/tests/latest/input/export_with_artifacts/input/artifacts/policyTags.json @@ -0,0 +1,17 @@ +{ + "kind": "policyAssignment", + "properties": { + "displayName": "Apply tag and its default value to resource groups", + "description": "Apply tag and its default value to resource groups", + "dependsOn": [], + "policyDefinitionId": "/providers/Microsoft.Authorization/policyDefinitions/49c88fc8-6fd1-46fd-a676-f12d1d3a4c71", + "parameters": { + "tagName": { + "value": "[parameters('tagName')]" + }, + "tagValue": { + "value": "[parameters('tagValue')]" + } + } + } +} \ No newline at end of file diff --git a/src/blueprint/azext_blueprint/tests/latest/input/export_with_artifacts/input/artifacts/roleContributor.json b/src/blueprint/azext_blueprint/tests/latest/input/export_with_artifacts/input/artifacts/roleContributor.json new file mode 100644 index 00000000000..f48e4689792 --- /dev/null +++ b/src/blueprint/azext_blueprint/tests/latest/input/export_with_artifacts/input/artifacts/roleContributor.json @@ -0,0 +1,8 @@ +{ + "kind": "roleAssignment", + "properties": { + "dependsOn": [], + "roleDefinitionId": "/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c", + "principalIds": "[parameters('contributors')]" + } +} \ No newline at end of file diff --git a/src/blueprint/azext_blueprint/tests/latest/input/export_with_artifacts/input/artifacts/roleOwner.json b/src/blueprint/azext_blueprint/tests/latest/input/export_with_artifacts/input/artifacts/roleOwner.json new file mode 100644 index 00000000000..cd957caf908 --- /dev/null +++ b/src/blueprint/azext_blueprint/tests/latest/input/export_with_artifacts/input/artifacts/roleOwner.json @@ -0,0 +1,9 @@ +{ + "kind": "roleAssignment", + "properties": { + "dependsOn": [], + "roleDefinitionId": "/providers/Microsoft.Authorization/roleDefinitions/8e3af657-a8ff-443c-a75c-2fe8c4bcb635", + "principalIds": "[parameters('owners')]", + "resourceGroup": "storageRG" + } +} \ No newline at end of file diff --git a/src/blueprint/azext_blueprint/tests/latest/input/export_with_artifacts/input/blueprint.json b/src/blueprint/azext_blueprint/tests/latest/input/export_with_artifacts/input/blueprint.json new file mode 100644 index 00000000000..c0539179127 --- /dev/null +++ b/src/blueprint/azext_blueprint/tests/latest/input/export_with_artifacts/input/blueprint.json @@ -0,0 +1,58 @@ +{ + "properties": { + "description": "This blueprint sets tag policy and role assignment on the subscription, creates a ResourceGroup, and deploys a resource template and role assignment to that ResourceGroup.", + "targetScope": "subscription", + "parameters": { + "storageAccountType": { + "type": "string", + "metadata": { + "displayName": "storage account type.", + "description": "storage account type." + }, + "defaultValue": "Standard_LRS", + "allowedValues": [ + "Standard_LRS", + "Standard_GRS", + "Standard_ZRS", + "Premium_LRS" + ] + }, + "tagName": { + "type": "string", + "metadata": { + "displayName": "The name of the tag to provide the policy assignment.", + "description": "The name of the tag to provide the policy assignment." + } + }, + "tagValue": { + "type": "string", + "metadata": { + "displayName": "The value of the tag to provide the policy assignment.", + "description": "The value of the tag to provide the policy assignment." + } + }, + "contributors": { + "type": "array", + "metadata": { + "description": "List of AAD object IDs that is assigned Contributor role at the subscription", + "strongType": "PrincipalId" + } + }, + "owners": { + "type": "array", + "metadata": { + "description": "List of AAD object IDs that is assigned Owner role at the resource group", + "strongType": "PrincipalId" + } + } + }, + "resourceGroups": { + "storageRG": { + "metadata": { + "description": "Contains the resource template deployment and a role assignment." + }, + "dependsOn": [] + } + } + } +} \ No newline at end of file diff --git a/src/blueprint/azext_blueprint/tests/latest/recordings/test_blueprint_export.yaml b/src/blueprint/azext_blueprint/tests/latest/recordings/test_blueprint_export.yaml new file mode 100644 index 00000000000..51e867392b5 --- /dev/null +++ b/src/blueprint/azext_blueprint/tests/latest/recordings/test_blueprint_export.yaml @@ -0,0 +1,621 @@ +interactions: +- request: + body: '{"properties": {"description": "This blueprint sets tag policy and role + assignment on the subscription, creates a ResourceGroup, and deploys a resource + template and role assignment to that ResourceGroup.", "targetScope": "subscription", + "parameters": {"storageAccountType": {"type": "string", "metadata": {"displayName": + "storage account type.", "description": "storage account type."}, "defaultValue": + "Standard_LRS", "allowedValues": ["Standard_LRS", "Standard_GRS", "Standard_ZRS", + "Premium_LRS"]}, "tagName": {"type": "string", "metadata": {"displayName": "The + name of the tag to provide the policy assignment.", "description": "The name + of the tag to provide the policy assignment."}}, "tagValue": {"type": "string", + "metadata": {"displayName": "The value of the tag to provide the policy assignment.", + "description": "The value of the tag to provide the policy assignment."}}, "contributors": + {"type": "array", "metadata": {"description": "List of AAD object IDs that is + assigned Contributor role at the subscription", "strongType": "PrincipalId"}}, + "owners": {"type": "array", "metadata": {"description": "List of AAD object + IDs that is assigned Owner role at the resource group", "strongType": "PrincipalId"}}}, + "resourceGroups": {"storageRG": {"metadata": {"description": "Contains the resource + template deployment and a role assignment."}, "dependsOn": []}}}}' + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + CommandName: + - blueprint import + Connection: + - keep-alive + Content-Length: + - '1369' + Content-Type: + - application/json; charset=utf-8 + ParameterSetName: + - --name --input-path -y + User-Agent: + - python/3.8.8 (Windows-10-10.0.19041-SP0) msrest/0.6.21 msrest_azure/0.6.3 + azure-mgmt-blueprint/2018-11-01-preview Azure-SDK-For-Python AZURECLI/2.21.0 + accept-language: + - en-US + method: PUT + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Blueprint/blueprints/test-imported-bp000002?api-version=2018-11-01-preview + response: + body: + string: "{\r\n \"properties\": {\r\n \"parameters\": {\r\n \"storageAccountType\": + {\r\n \"type\": \"string\",\r\n \"metadata\": {\r\n \"displayName\": + \"storage account type.\",\r\n \"description\": \"storage account + type.\"\r\n },\r\n \"defaultValue\": \"Standard_LRS\",\r\n \"allowedValues\": + [\r\n \"Standard_LRS\",\r\n \"Standard_GRS\",\r\n \"Standard_ZRS\",\r\n + \ \"Premium_LRS\"\r\n ]\r\n },\r\n \"tagName\": {\r\n + \ \"type\": \"string\",\r\n \"metadata\": {\r\n \"displayName\": + \"The name of the tag to provide the policy assignment.\",\r\n \"description\": + \"The name of the tag to provide the policy assignment.\"\r\n }\r\n + \ },\r\n \"tagValue\": {\r\n \"type\": \"string\",\r\n \"metadata\": + {\r\n \"displayName\": \"The value of the tag to provide the policy + assignment.\",\r\n \"description\": \"The value of the tag to provide + the policy assignment.\"\r\n }\r\n },\r\n \"contributors\": + {\r\n \"type\": \"array\",\r\n \"metadata\": {\r\n \"description\": + \"List of AAD object IDs that is assigned Contributor role at the subscription\",\r\n + \ \"strongType\": \"PrincipalId\"\r\n }\r\n },\r\n \"owners\": + {\r\n \"type\": \"array\",\r\n \"metadata\": {\r\n \"description\": + \"List of AAD object IDs that is assigned Owner role at the resource group\",\r\n + \ \"strongType\": \"PrincipalId\"\r\n }\r\n }\r\n },\r\n + \ \"resourceGroups\": {\r\n \"storageRG\": {\r\n \"metadata\": + {\r\n \"description\": \"Contains the resource template deployment + and a role assignment.\"\r\n },\r\n \"dependsOn\": []\r\n }\r\n + \ },\r\n \"targetScope\": \"subscription\",\r\n \"status\": {\r\n + \ \"timeCreated\": \"2021-04-16T15:47:34+00:00\",\r\n \"lastModified\": + \"2021-04-16T15:47:34.6783608+00:00\"\r\n },\r\n \"description\": \"This + blueprint sets tag policy and role assignment on the subscription, creates + a ResourceGroup, and deploys a resource template and role assignment to that + ResourceGroup.\"\r\n },\r\n \"id\": \"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Blueprint/blueprints/test-imported-bp000002\",\r\n + \ \"type\": \"Microsoft.Blueprint/blueprints\",\r\n \"name\": \"test-imported-bp000002\"\r\n}" + headers: + cache-control: + - no-cache + content-length: + - '2227' + content-type: + - application/json; charset=utf-8 + date: + - Fri, 16 Apr 2021 15:47:34 GMT + expires: + - '-1' + pragma: + - no-cache + server: + - Microsoft-HTTPAPI/2.0 + strict-transport-security: + - max-age=31536000; includeSubDomains + x-content-type-options: + - nosniff + x-ms-ratelimit-remaining-subscription-writes: + - '1199' + status: + code: 201 + message: Created +- request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + CommandName: + - blueprint import + Connection: + - keep-alive + ParameterSetName: + - --name --input-path -y + User-Agent: + - python/3.8.8 (Windows-10-10.0.19041-SP0) msrest/0.6.21 msrest_azure/0.6.3 + azure-mgmt-blueprint/2018-11-01-preview Azure-SDK-For-Python AZURECLI/2.21.0 + accept-language: + - en-US + method: GET + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Blueprint/blueprints/test-imported-bp000002/artifacts?api-version=2018-11-01-preview + response: + body: + string: "{\r\n \"value\": []\r\n}" + headers: + cache-control: + - no-cache + content-length: + - '19' + content-type: + - application/json; charset=utf-8 + date: + - Fri, 16 Apr 2021 15:47:34 GMT + expires: + - '-1' + pragma: + - no-cache + server: + - Microsoft-HTTPAPI/2.0 + strict-transport-security: + - max-age=31536000; includeSubDomains + transfer-encoding: + - chunked + vary: + - Accept-Encoding + x-content-type-options: + - nosniff + status: + code: 200 + message: OK +- request: + body: '{"kind": "policyAssignment", "properties": {"displayName": "Apply storage + tag to resource group", "description": "Apply storage tag and the parameter + also used by the template to resource groups", "dependsOn": [], "policyDefinitionId": + "/providers/Microsoft.Authorization/policyDefinitions/49c88fc8-6fd1-46fd-a676-f12d1d3a4c71", + "parameters": {"tagName": {"value": "StorageType"}, "tagValue": {"value": "[parameters(''storageAccountType'')]"}}}}' + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + CommandName: + - blueprint import + Connection: + - keep-alive + Content-Length: + - '443' + Content-Type: + - application/json; charset=utf-8 + ParameterSetName: + - --name --input-path -y + User-Agent: + - python/3.8.8 (Windows-10-10.0.19041-SP0) msrest/0.6.21 msrest_azure/0.6.3 + azure-mgmt-blueprint/2018-11-01-preview Azure-SDK-For-Python AZURECLI/2.21.0 + accept-language: + - en-US + method: PUT + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Blueprint/blueprints/test-imported-bp000002/artifacts/policyStorageTags?api-version=2018-11-01-preview + response: + body: + string: "{\r\n \"properties\": {\r\n \"policyDefinitionId\": \"/providers/Microsoft.Authorization/policyDefinitions/49c88fc8-6fd1-46fd-a676-f12d1d3a4c71\",\r\n + \ \"parameters\": {\r\n \"tagName\": {\r\n \"value\": \"StorageType\"\r\n + \ },\r\n \"tagValue\": {\r\n \"value\": \"[parameters('storageAccountType')]\"\r\n + \ }\r\n },\r\n \"dependsOn\": [],\r\n \"displayName\": \"Apply + storage tag to resource group\",\r\n \"description\": \"Apply storage tag + and the parameter also used by the template to resource groups\"\r\n },\r\n + \ \"kind\": \"policyAssignment\",\r\n \"id\": \"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Blueprint/blueprints/test-imported-bp000002/artifacts/policyStorageTags\",\r\n + \ \"type\": \"Microsoft.Blueprint/blueprints/artifacts\",\r\n \"name\": \"policyStorageTags\"\r\n}" + headers: + cache-control: + - no-cache + content-length: + - '784' + content-type: + - application/json; charset=utf-8 + date: + - Fri, 16 Apr 2021 15:47:34 GMT + expires: + - '-1' + pragma: + - no-cache + server: + - Microsoft-HTTPAPI/2.0 + strict-transport-security: + - max-age=31536000; includeSubDomains + x-content-type-options: + - nosniff + x-ms-ratelimit-remaining-subscription-writes: + - '1199' + status: + code: 201 + message: Created +- request: + body: '{"kind": "policyAssignment", "properties": {"displayName": "Apply tag and + its default value to resource groups", "description": "Apply tag and its default + value to resource groups", "dependsOn": [], "policyDefinitionId": "/providers/Microsoft.Authorization/policyDefinitions/49c88fc8-6fd1-46fd-a676-f12d1d3a4c71", + "parameters": {"tagName": {"value": "[parameters(''tagName'')]"}, "tagValue": + {"value": "[parameters(''tagValue'')]"}}}}' + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + CommandName: + - blueprint import + Connection: + - keep-alive + Content-Length: + - '430' + Content-Type: + - application/json; charset=utf-8 + ParameterSetName: + - --name --input-path -y + User-Agent: + - python/3.8.8 (Windows-10-10.0.19041-SP0) msrest/0.6.21 msrest_azure/0.6.3 + azure-mgmt-blueprint/2018-11-01-preview Azure-SDK-For-Python AZURECLI/2.21.0 + accept-language: + - en-US + method: PUT + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Blueprint/blueprints/test-imported-bp000002/artifacts/policyTags?api-version=2018-11-01-preview + response: + body: + string: "{\r\n \"properties\": {\r\n \"policyDefinitionId\": \"/providers/Microsoft.Authorization/policyDefinitions/49c88fc8-6fd1-46fd-a676-f12d1d3a4c71\",\r\n + \ \"parameters\": {\r\n \"tagName\": {\r\n \"value\": \"[parameters('tagName')]\"\r\n + \ },\r\n \"tagValue\": {\r\n \"value\": \"[parameters('tagValue')]\"\r\n + \ }\r\n },\r\n \"dependsOn\": [],\r\n \"displayName\": \"Apply + tag and its default value to resource groups\",\r\n \"description\": \"Apply + tag and its default value to resource groups\"\r\n },\r\n \"kind\": \"policyAssignment\",\r\n + \ \"id\": \"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Blueprint/blueprints/test-imported-bp000002/artifacts/policyTags\",\r\n + \ \"type\": \"Microsoft.Blueprint/blueprints/artifacts\",\r\n \"name\": \"policyTags\"\r\n}" + headers: + cache-control: + - no-cache + content-length: + - '757' + content-type: + - application/json; charset=utf-8 + date: + - Fri, 16 Apr 2021 15:47:34 GMT + expires: + - '-1' + pragma: + - no-cache + server: + - Microsoft-HTTPAPI/2.0 + strict-transport-security: + - max-age=31536000; includeSubDomains + x-content-type-options: + - nosniff + x-ms-ratelimit-remaining-subscription-writes: + - '1198' + status: + code: 201 + message: Created +- request: + body: '{"kind": "roleAssignment", "properties": {"dependsOn": [], "roleDefinitionId": + "/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c", + "principalIds": "[parameters(''contributors'')]"}}' + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + CommandName: + - blueprint import + Connection: + - keep-alive + Content-Length: + - '218' + Content-Type: + - application/json; charset=utf-8 + ParameterSetName: + - --name --input-path -y + User-Agent: + - python/3.8.8 (Windows-10-10.0.19041-SP0) msrest/0.6.21 msrest_azure/0.6.3 + azure-mgmt-blueprint/2018-11-01-preview Azure-SDK-For-Python AZURECLI/2.21.0 + accept-language: + - en-US + method: PUT + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Blueprint/blueprints/test-imported-bp000002/artifacts/roleContributor?api-version=2018-11-01-preview + response: + body: + string: "{\r\n \"properties\": {\r\n \"roleDefinitionId\": \"/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c\",\r\n + \ \"principalIds\": \"[parameters('contributors')]\",\r\n \"dependsOn\": + []\r\n },\r\n \"kind\": \"roleAssignment\",\r\n \"id\": \"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Blueprint/blueprints/test-imported-bp000002/artifacts/roleContributor\",\r\n + \ \"type\": \"Microsoft.Blueprint/blueprints/artifacts\",\r\n \"name\": \"roleContributor\"\r\n}" + headers: + cache-control: + - no-cache + content-length: + - '488' + content-type: + - application/json; charset=utf-8 + date: + - Fri, 16 Apr 2021 15:47:34 GMT + expires: + - '-1' + pragma: + - no-cache + server: + - Microsoft-HTTPAPI/2.0 + strict-transport-security: + - max-age=31536000; includeSubDomains + x-content-type-options: + - nosniff + x-ms-ratelimit-remaining-subscription-writes: + - '1197' + status: + code: 201 + message: Created +- request: + body: '{"kind": "roleAssignment", "properties": {"dependsOn": [], "roleDefinitionId": + "/providers/Microsoft.Authorization/roleDefinitions/8e3af657-a8ff-443c-a75c-2fe8c4bcb635", + "principalIds": "[parameters(''owners'')]", "resourceGroup": "storageRG"}}' + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + CommandName: + - blueprint import + Connection: + - keep-alive + Content-Length: + - '242' + Content-Type: + - application/json; charset=utf-8 + ParameterSetName: + - --name --input-path -y + User-Agent: + - python/3.8.8 (Windows-10-10.0.19041-SP0) msrest/0.6.21 msrest_azure/0.6.3 + azure-mgmt-blueprint/2018-11-01-preview Azure-SDK-For-Python AZURECLI/2.21.0 + accept-language: + - en-US + method: PUT + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Blueprint/blueprints/test-imported-bp000002/artifacts/roleOwner?api-version=2018-11-01-preview + response: + body: + string: "{\r\n \"properties\": {\r\n \"roleDefinitionId\": \"/providers/Microsoft.Authorization/roleDefinitions/8e3af657-a8ff-443c-a75c-2fe8c4bcb635\",\r\n + \ \"principalIds\": \"[parameters('owners')]\",\r\n \"dependsOn\": [],\r\n + \ \"resourceGroup\": \"storageRG\"\r\n },\r\n \"kind\": \"roleAssignment\",\r\n + \ \"id\": \"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Blueprint/blueprints/test-imported-bp000002/artifacts/roleOwner\",\r\n + \ \"type\": \"Microsoft.Blueprint/blueprints/artifacts\",\r\n \"name\": \"roleOwner\"\r\n}" + headers: + cache-control: + - no-cache + content-length: + - '505' + content-type: + - application/json; charset=utf-8 + date: + - Fri, 16 Apr 2021 15:47:34 GMT + expires: + - '-1' + pragma: + - no-cache + server: + - Microsoft-HTTPAPI/2.0 + strict-transport-security: + - max-age=31536000; includeSubDomains + x-content-type-options: + - nosniff + x-ms-ratelimit-remaining-subscription-writes: + - '1196' + status: + code: 201 + message: Created +- request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + CommandName: + - blueprint export + Connection: + - keep-alive + ParameterSetName: + - --output-path --name --yes + User-Agent: + - python/3.8.8 (Windows-10-10.0.19041-SP0) msrest/0.6.21 msrest_azure/0.6.3 + azure-mgmt-blueprint/2018-11-01-preview Azure-SDK-For-Python AZURECLI/2.21.0 + accept-language: + - en-US + method: GET + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Blueprint/blueprints/test-imported-bp000002?api-version=2018-11-01-preview + response: + body: + string: "{\r\n \"properties\": {\r\n \"parameters\": {\r\n \"storageAccountType\": + {\r\n \"type\": \"string\",\r\n \"metadata\": {\r\n \"displayName\": + \"storage account type.\",\r\n \"description\": \"storage account + type.\"\r\n },\r\n \"defaultValue\": \"Standard_LRS\",\r\n \"allowedValues\": + [\r\n \"Standard_LRS\",\r\n \"Standard_GRS\",\r\n \"Standard_ZRS\",\r\n + \ \"Premium_LRS\"\r\n ]\r\n },\r\n \"tagName\": {\r\n + \ \"type\": \"string\",\r\n \"metadata\": {\r\n \"displayName\": + \"The name of the tag to provide the policy assignment.\",\r\n \"description\": + \"The name of the tag to provide the policy assignment.\"\r\n }\r\n + \ },\r\n \"tagValue\": {\r\n \"type\": \"string\",\r\n \"metadata\": + {\r\n \"displayName\": \"The value of the tag to provide the policy + assignment.\",\r\n \"description\": \"The value of the tag to provide + the policy assignment.\"\r\n }\r\n },\r\n \"contributors\": + {\r\n \"type\": \"array\",\r\n \"metadata\": {\r\n \"description\": + \"List of AAD object IDs that is assigned Contributor role at the subscription\",\r\n + \ \"strongType\": \"PrincipalId\"\r\n }\r\n },\r\n \"owners\": + {\r\n \"type\": \"array\",\r\n \"metadata\": {\r\n \"description\": + \"List of AAD object IDs that is assigned Owner role at the resource group\",\r\n + \ \"strongType\": \"PrincipalId\"\r\n }\r\n }\r\n },\r\n + \ \"resourceGroups\": {\r\n \"storageRG\": {\r\n \"metadata\": + {\r\n \"description\": \"Contains the resource template deployment + and a role assignment.\"\r\n },\r\n \"dependsOn\": []\r\n }\r\n + \ },\r\n \"targetScope\": \"subscription\",\r\n \"status\": {\r\n + \ \"timeCreated\": \"2021-04-16T15:47:34+00:00\",\r\n \"lastModified\": + \"2021-04-16T15:47:34.6783608+00:00\"\r\n },\r\n \"description\": \"This + blueprint sets tag policy and role assignment on the subscription, creates + a ResourceGroup, and deploys a resource template and role assignment to that + ResourceGroup.\"\r\n },\r\n \"id\": \"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Blueprint/blueprints/test-imported-bp000002\",\r\n + \ \"type\": \"Microsoft.Blueprint/blueprints\",\r\n \"name\": \"test-imported-bp000002\"\r\n}" + headers: + cache-control: + - no-cache + content-length: + - '2227' + content-type: + - application/json; charset=utf-8 + date: + - Fri, 16 Apr 2021 15:47:35 GMT + expires: + - '-1' + pragma: + - no-cache + server: + - Microsoft-HTTPAPI/2.0 + strict-transport-security: + - max-age=31536000; includeSubDomains + transfer-encoding: + - chunked + vary: + - Accept-Encoding + x-content-type-options: + - nosniff + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + CommandName: + - blueprint export + Connection: + - keep-alive + ParameterSetName: + - --output-path --name --yes + User-Agent: + - python/3.8.8 (Windows-10-10.0.19041-SP0) msrest/0.6.21 msrest_azure/0.6.3 + azure-mgmt-blueprint/2018-11-01-preview Azure-SDK-For-Python AZURECLI/2.21.0 + accept-language: + - en-US + method: GET + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Blueprint/blueprints/test-imported-bp000002/artifacts?api-version=2018-11-01-preview + response: + body: + string: "{\r\n \"value\": [\r\n {\r\n \"properties\": {\r\n \"policyDefinitionId\": + \"/providers/Microsoft.Authorization/policyDefinitions/49c88fc8-6fd1-46fd-a676-f12d1d3a4c71\",\r\n + \ \"parameters\": {\r\n \"tagName\": {\r\n \"value\": + \"StorageType\"\r\n },\r\n \"tagValue\": {\r\n \"value\": + \"[parameters('storageAccountType')]\"\r\n }\r\n },\r\n \"dependsOn\": + [],\r\n \"displayName\": \"Apply storage tag to resource group\",\r\n + \ \"description\": \"Apply storage tag and the parameter also used by + the template to resource groups\"\r\n },\r\n \"kind\": \"policyAssignment\",\r\n + \ \"id\": \"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Blueprint/blueprints/test-imported-bp000002/artifacts/policyStorageTags\",\r\n + \ \"type\": \"Microsoft.Blueprint/blueprints/artifacts\",\r\n \"name\": + \"policyStorageTags\"\r\n },\r\n {\r\n \"properties\": {\r\n \"policyDefinitionId\": + \"/providers/Microsoft.Authorization/policyDefinitions/49c88fc8-6fd1-46fd-a676-f12d1d3a4c71\",\r\n + \ \"parameters\": {\r\n \"tagName\": {\r\n \"value\": + \"[parameters('tagName')]\"\r\n },\r\n \"tagValue\": {\r\n + \ \"value\": \"[parameters('tagValue')]\"\r\n }\r\n },\r\n + \ \"dependsOn\": [],\r\n \"displayName\": \"Apply tag and its + default value to resource groups\",\r\n \"description\": \"Apply tag + and its default value to resource groups\"\r\n },\r\n \"kind\": + \"policyAssignment\",\r\n \"id\": \"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Blueprint/blueprints/test-imported-bp000002/artifacts/policyTags\",\r\n + \ \"type\": \"Microsoft.Blueprint/blueprints/artifacts\",\r\n \"name\": + \"policyTags\"\r\n },\r\n {\r\n \"properties\": {\r\n \"roleDefinitionId\": + \"/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c\",\r\n + \ \"principalIds\": \"[parameters('contributors')]\",\r\n \"dependsOn\": + []\r\n },\r\n \"kind\": \"roleAssignment\",\r\n \"id\": \"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Blueprint/blueprints/test-imported-bp000002/artifacts/roleContributor\",\r\n + \ \"type\": \"Microsoft.Blueprint/blueprints/artifacts\",\r\n \"name\": + \"roleContributor\"\r\n },\r\n {\r\n \"properties\": {\r\n \"roleDefinitionId\": + \"/providers/Microsoft.Authorization/roleDefinitions/8e3af657-a8ff-443c-a75c-2fe8c4bcb635\",\r\n + \ \"principalIds\": \"[parameters('owners')]\",\r\n \"dependsOn\": + [],\r\n \"resourceGroup\": \"storageRG\"\r\n },\r\n \"kind\": + \"roleAssignment\",\r\n \"id\": \"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Blueprint/blueprints/test-imported-bp000002/artifacts/roleOwner\",\r\n + \ \"type\": \"Microsoft.Blueprint/blueprints/artifacts\",\r\n \"name\": + \"roleOwner\"\r\n }\r\n ]\r\n}" + headers: + cache-control: + - no-cache + content-length: + - '2820' + content-type: + - application/json; charset=utf-8 + date: + - Fri, 16 Apr 2021 15:47:35 GMT + expires: + - '-1' + pragma: + - no-cache + server: + - Microsoft-HTTPAPI/2.0 + strict-transport-security: + - max-age=31536000; includeSubDomains + transfer-encoding: + - chunked + vary: + - Accept-Encoding + x-content-type-options: + - nosniff + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + CommandName: + - blueprint delete + Connection: + - keep-alive + Content-Length: + - '0' + ParameterSetName: + - --name -y + User-Agent: + - python/3.8.8 (Windows-10-10.0.19041-SP0) msrest/0.6.21 msrest_azure/0.6.3 + azure-mgmt-blueprint/2018-11-01-preview Azure-SDK-For-Python AZURECLI/2.21.0 + accept-language: + - en-US + method: DELETE + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Blueprint/blueprints/test-imported-bp000002?api-version=2018-11-01-preview + response: + body: + string: "{\r\n \"properties\": {\r\n \"parameters\": {\r\n \"storageAccountType\": + {\r\n \"type\": \"string\",\r\n \"metadata\": {\r\n \"displayName\": + \"storage account type.\",\r\n \"description\": \"storage account + type.\"\r\n },\r\n \"defaultValue\": \"Standard_LRS\",\r\n \"allowedValues\": + [\r\n \"Standard_LRS\",\r\n \"Standard_GRS\",\r\n \"Standard_ZRS\",\r\n + \ \"Premium_LRS\"\r\n ]\r\n },\r\n \"tagName\": {\r\n + \ \"type\": \"string\",\r\n \"metadata\": {\r\n \"displayName\": + \"The name of the tag to provide the policy assignment.\",\r\n \"description\": + \"The name of the tag to provide the policy assignment.\"\r\n }\r\n + \ },\r\n \"tagValue\": {\r\n \"type\": \"string\",\r\n \"metadata\": + {\r\n \"displayName\": \"The value of the tag to provide the policy + assignment.\",\r\n \"description\": \"The value of the tag to provide + the policy assignment.\"\r\n }\r\n },\r\n \"contributors\": + {\r\n \"type\": \"array\",\r\n \"metadata\": {\r\n \"description\": + \"List of AAD object IDs that is assigned Contributor role at the subscription\",\r\n + \ \"strongType\": \"PrincipalId\"\r\n }\r\n },\r\n \"owners\": + {\r\n \"type\": \"array\",\r\n \"metadata\": {\r\n \"description\": + \"List of AAD object IDs that is assigned Owner role at the resource group\",\r\n + \ \"strongType\": \"PrincipalId\"\r\n }\r\n }\r\n },\r\n + \ \"resourceGroups\": {\r\n \"storageRG\": {\r\n \"metadata\": + {\r\n \"description\": \"Contains the resource template deployment + and a role assignment.\"\r\n },\r\n \"dependsOn\": []\r\n }\r\n + \ },\r\n \"targetScope\": \"subscription\",\r\n \"status\": {\r\n + \ \"timeCreated\": \"2021-04-16T15:47:34+00:00\",\r\n \"lastModified\": + \"2021-04-16T15:47:34.6783608+00:00\"\r\n },\r\n \"description\": \"This + blueprint sets tag policy and role assignment on the subscription, creates + a ResourceGroup, and deploys a resource template and role assignment to that + ResourceGroup.\"\r\n },\r\n \"id\": \"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Blueprint/blueprints/test-imported-bp000002\",\r\n + \ \"type\": \"Microsoft.Blueprint/blueprints\",\r\n \"name\": \"test-imported-bp000002\"\r\n}" + headers: + cache-control: + - no-cache + content-length: + - '2227' + content-type: + - application/json; charset=utf-8 + date: + - Fri, 16 Apr 2021 15:47:36 GMT + expires: + - '-1' + pragma: + - no-cache + server: + - Microsoft-HTTPAPI/2.0 + strict-transport-security: + - max-age=31536000; includeSubDomains + transfer-encoding: + - chunked + vary: + - Accept-Encoding + x-content-type-options: + - nosniff + x-ms-ratelimit-remaining-subscription-deletes: + - '14999' + status: + code: 200 + message: OK +version: 1 diff --git a/src/blueprint/azext_blueprint/tests/latest/test_blueprint_scenario.py b/src/blueprint/azext_blueprint/tests/latest/test_blueprint_scenario.py index bb3197dcd96..10269313d89 100644 --- a/src/blueprint/azext_blueprint/tests/latest/test_blueprint_scenario.py +++ b/src/blueprint/azext_blueprint/tests/latest/test_blueprint_scenario.py @@ -5,6 +5,10 @@ import os import unittest +import json +import filecmp +from pathlib import Path +import shutil from azure_devtools.scenario_tests import AllowLargeResponse from azure.cli.testsdk import (ScenarioTest, ResourceGroupPreparer, @@ -288,3 +292,90 @@ def test_blueprint_import(self, resource_group): '--subscription "{subscription}" ' '--name "storage-rg" ' '-y') + + @AllowLargeResponse() + @ResourceGroupPreparer(name_prefix='cli_test_blueprint_export') + def test_blueprint_export(self, resource_group): + # same procedure as testing the import, build a blueprint to export + self.kwargs.update({ + 'blueprintName': self.create_random_name(prefix='test-imported-bp', length=24), + 'subscription': self.get_subscription_id(), + }) + + # this will overwrite the previous settings + self.cmd( + 'az blueprint import ' + '--name "{blueprintName}" ' + '--input-path "src/blueprint/azext_blueprint/tests/latest/input/export_with_artifacts/input" ' + '-y', + checks=[]) + + self.cmd( + 'az blueprint export ' + '--output-path "src/blueprint/azext_blueprint/tests/latest/input/export_with_artifacts/exported" ' + '--name "{blueprintName}" ' + '--yes', + checks=[]) + + # check if the import and output artifacts are equal in content + input_blueprint = "src/blueprint/azext_blueprint/tests/latest/input/export_with_artifacts/input/blueprint.json" + input_artifact_directory = "src/blueprint/azext_blueprint/tests/latest/input/export_with_artifacts/input/artifacts" + output_blueprint = f"src/blueprint/azext_blueprint/tests/latest/input/export_with_artifacts/exported/{self.kwargs['blueprintName']}/blueprint.json" + output_artifact_directory = f"src/blueprint/azext_blueprint/tests/latest/input/export_with_artifacts/exported/{self.kwargs['blueprintName']}/artifacts" + output_path = Path(f"src/blueprint/azext_blueprint/tests/latest/input/export_with_artifacts/exported/{self.kwargs['blueprintName']}") + + # recursive function to check for json equality + def ordered(obj): + if isinstance(obj, dict): + return sorted((k, ordered(v)) for k, v in obj.items()) + if isinstance(obj, list): + return sorted(ordered(x) for x in obj) + else: + return obj + + # file comparison + with open(input_blueprint) as input_f: + input_blueprint = json.load(input_f) + ordered_input_blueprint = ordered(input_blueprint) + with open(output_blueprint) as output_f: + output_blueprint = json.load(output_f) + ordered_output_blueprint = ordered(output_blueprint) + + try: + self.assertEqual(ordered_input_blueprint, ordered_output_blueprint) + except AssertionError: + if output_path.exists() and output_path.is_dir(): + shutil.rmtree(output_path) + raise + + # artifact directory comparison + artifacts_cmp = filecmp.dircmp(input_artifact_directory, output_artifact_directory) + try: + assert len(artifacts_cmp.right_only) == 0 and len(artifacts_cmp.left_only) == 0 and len(artifacts_cmp.funny_files) == 0 + except AssertionError: + if output_path.exists() and output_path.is_dir(): + shutil.rmtree(output_path) + raise + + # artifact file comparison + for filename in os.listdir(input_artifact_directory): + with open(os.path.join(input_artifact_directory, filename)) as input_f: + input_artifact = json.load(input_f) + ordered_input_artifact = ordered(input_artifact) + with open(os.path.join(output_artifact_directory, filename)) as output_f: + output_artifact = json.load(output_f) + ordered_output_artifact = ordered(output_artifact) + try: + self.assertEqual(ordered_input_artifact, ordered_output_artifact) + except AssertionError: + if output_path.exists() and output_path.is_dir(): + shutil.rmtree(output_path) + raise + + self.cmd('az blueprint delete ' + '--name "{blueprintName}" ' + '-y', + checks=[JMESPathCheck('name', self.kwargs.get('blueprintName', ''))]) + + if output_path.exists() and output_path.is_dir(): + shutil.rmtree(output_path) diff --git a/src/blueprint/setup.py b/src/blueprint/setup.py index d0a5dcbdb50..eb815aacc10 100644 --- a/src/blueprint/setup.py +++ b/src/blueprint/setup.py @@ -16,7 +16,8 @@ # TODO: Confirm this is the right version number you want and it matches your # HISTORY.rst entry. -VERSION = '0.2.1' +VERSION = '0.3.0' + # The full list of classifiers is available at # https://pypi.python.org/pypi?%3Aaction=list_classifiers