Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Blueprint] Close #2723: add export feature for blueprint and artifacts #3259

Merged
merged 10 commits into from
Apr 20, 2021
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