Skip to content

Commit

Permalink
[Blueprint] Close Azure#2723: add export feature for blueprint and ar…
Browse files Browse the repository at this point in the history
…tifacts (Azure#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 <[email protected]>

Co-authored-by: Feng Zhou <[email protected]>
  • Loading branch information
08nholloway and fengzhou-msft authored Apr 20, 2021
1 parent a980ca1 commit 372b3f2
Show file tree
Hide file tree
Showing 13 changed files with 879 additions and 4 deletions.
4 changes: 4 additions & 0 deletions src/blueprint/HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions src/blueprint/azext_blueprint/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
5 changes: 5 additions & 0 deletions src/blueprint/azext_blueprint/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.')
Expand Down
1 change: 1 addition & 0 deletions src/blueprint/azext_blueprint/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
39 changes: 36 additions & 3 deletions src/blueprint/azext_blueprint/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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')
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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')]"
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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')]"
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"kind": "roleAssignment",
"properties": {
"dependsOn": [],
"roleDefinitionId": "/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c",
"principalIds": "[parameters('contributors')]"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"kind": "roleAssignment",
"properties": {
"dependsOn": [],
"roleDefinitionId": "/providers/Microsoft.Authorization/roleDefinitions/8e3af657-a8ff-443c-a75c-2fe8c4bcb635",
"principalIds": "[parameters('owners')]",
"resourceGroup": "storageRG"
}
}
Original file line number Diff line number Diff line change
@@ -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": []
}
}
}
}
Loading

0 comments on commit 372b3f2

Please sign in to comment.