# -*- coding: utf-8 -*- # # Copyright 2014 Google LLC. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """deployments create command.""" from __future__ import absolute_import from __future__ import division from __future__ import unicode_literals from apitools.base.py import exceptions as apitools_exceptions from googlecloudsdk.api_lib.deployment_manager import dm_api_util from googlecloudsdk.api_lib.deployment_manager import dm_base from googlecloudsdk.api_lib.deployment_manager import exceptions as dm_exceptions from googlecloudsdk.api_lib.util import apis from googlecloudsdk.calliope import base from googlecloudsdk.calliope import exceptions from googlecloudsdk.command_lib.deployment_manager import alpha_flags from googlecloudsdk.command_lib.deployment_manager import dm_util from googlecloudsdk.command_lib.deployment_manager import dm_write from googlecloudsdk.command_lib.deployment_manager import flags from googlecloudsdk.command_lib.deployment_manager import importer from googlecloudsdk.command_lib.util.apis import arg_utils from googlecloudsdk.command_lib.util.args import labels_util from googlecloudsdk.core import log from googlecloudsdk.core import properties import six # Number of seconds (approximately) to wait for create operation to complete. OPERATION_TIMEOUT = 20 * 60 # 20 mins @base.UnicodeIsSupported @base.ReleaseTracks(base.ReleaseTrack.GA) @dm_base.UseDmApi(dm_base.DmApiVersion.V2) class Create(base.CreateCommand, dm_base.DmCommand): """Create a deployment. This command inserts (creates) a new deployment based on a provided config file. """ detailed_help = { 'EXAMPLES': """ To create a new deployment from a top-level YAML file, run: $ {command} my-deployment --config=config.yaml --description="My deployment" To create a new deployment from a top-level template file, run: $ gcloud deployment-manager deployments create my-deployment \ --template=template.{jinja|py} \ --properties="string-key:'string-value',integer-key:12345" To create a new deployment directly from a composite type, run: $ gcloud deployment-manager deployments create my-deployment \ --composite-type=/composite: \ --properties="string-key:'string-value',integer-key:12345" To preview a deployment without actually creating resources, run: $ {command} my-new-deployment --config=config.yaml --preview To instantiate a deployment that has been previewed, issue an update command for that deployment without specifying a config file. More information is available at https://cloud.google.com/deployment-manager/docs/configuration/. """, } _create_policy_flag_map = arg_utils.ChoiceEnumMapper( '--create-policy', (apis.GetMessagesModule('deploymentmanager', 'v2beta') .DeploymentmanagerDeploymentsUpdateRequest.CreatePolicyValueValuesEnum), help_str='Create policy for resources that have changed in the update', default='create-or-acquire') @staticmethod def Args(parser, version=base.ReleaseTrack.GA): """Args is called by calliope to gather arguments for this command. Args: parser: An argparse parser that you can use to add arguments that go on the command line after this command. Positional arguments are allowed. version: The version this tool is running as. base.ReleaseTrack.GA is the default. """ group = parser.add_mutually_exclusive_group() config_group = parser.add_mutually_exclusive_group(required=True) flags.AddConfigFlags(config_group) flags.AddAsyncFlag(group) flags.AddDeploymentNameFlag(parser) flags.AddPropertiesFlag(parser) labels_util.AddCreateLabelsFlags(parser) group.add_argument( '--automatic-rollback-on-error', help='If the create request results in a deployment with resource ' 'errors, delete that deployment immediately after creation. ' '(default=False)', dest='automatic_rollback', default=False, action='store_true') parser.add_argument( '--description', help='Optional description of the deployment to insert.', dest='description') parser.add_argument( '--preview', help='Preview the requested create without actually instantiating the ' 'underlying resources. (default=False)', dest='preview', default=False, action='store_true') parser.display_info.AddFormat(flags.RESOURCES_AND_OUTPUTS_FORMAT) def Epilog(self, resources_were_displayed): """Called after resources are displayed if the default format was used. Args: resources_were_displayed: True if resources were displayed. """ if not resources_were_displayed: log.status.Print('No resources or outputs found in your deployment.') def Run(self, args): """Run 'deployments create'. Args: args: argparse.Namespace, The arguments that this command was invoked with. Returns: If --async=true, returns Operation to poll. Else, returns a struct containing the list of resources and list of outputs in the deployment. Raises: HttpException: An http error response was received while executing api request. ConfigError: Config file could not be read or parsed, or the deployment creation operation encountered an error. """ deployment_ref = self.resources.Parse( args.deployment_name, params={'project': properties.VALUES.core.project.GetOrFail}, collection='deploymentmanager.deployments') if (not args.IsSpecified('format')) and (args.async_): args.format = flags.OPERATION_FORMAT deployment = self.messages.Deployment( name=deployment_ref.deployment, target=importer.BuildTargetConfig(self.messages, config=args.config, template=args.template, composite_type=args.composite_type, properties=args.properties) ) self._SetMetadata(args, deployment) try: operation = self.client.deployments.Insert( self._BuildRequest( args=args, project=dm_base.GetProject(), deployment=deployment)) # Fetch and print the latest fingerprint of the deployment. fingerprint = dm_api_util.FetchDeploymentFingerprint( self.client, self.messages, dm_base.GetProject(), deployment_ref.deployment) dm_util.PrintFingerprint(fingerprint) except apitools_exceptions.HttpError as error: raise exceptions.HttpException(error, dm_api_util.HTTP_ERROR_FORMAT) if args.async_: return operation else: op_name = operation.name try: operation = dm_write.WaitForOperation( self.client, self.messages, op_name, operation_description='create', project=dm_base.GetProject(), timeout=OPERATION_TIMEOUT) dm_util.LogOperationStatus(operation, 'Create') except apitools_exceptions.HttpError as error: # TODO(b/37911296): Use gcloud default error handling. raise exceptions.HttpException(error, dm_api_util.HTTP_ERROR_FORMAT) except dm_exceptions.OperationError as error: response = self._HandleOperationError(error, args, operation, dm_base.GetProject(), deployment_ref) if getattr(args, 'automatic_rollback', False): args.format = flags.OPERATION_FORMAT return response return dm_api_util.FetchResourcesAndOutputs(self.client, self.messages, dm_base.GetProject(), deployment_ref.deployment) def _BuildRequest(self, args, project, deployment, supports_create_policy=False): request = self.messages.DeploymentmanagerDeploymentsInsertRequest( project=project, deployment=deployment, preview=args.preview) if supports_create_policy and args.create_policy: parsed_create_flag = Create._create_policy_flag_map.GetEnumForChoice( args.create_policy).name request.createPolicy = ( self.messages.DeploymentmanagerDeploymentsInsertRequest. CreatePolicyValueValuesEnum(parsed_create_flag)) return request def _HandleOperationError( self, error, args, operation, project, deployment_ref): if args.automatic_rollback: delete_operation = self._PerformRollback(deployment_ref.deployment, six.text_type(error)) create_operation = dm_api_util.GetOperation(self.client, self.messages, operation, project) return [create_operation, delete_operation] raise error def _SetMetadata(self, args, deployment): if args.description: deployment.description = args.description label_dict = labels_util.GetUpdateLabelsDictFromArgs(args) if label_dict: deployment.labels = [ self.messages.DeploymentLabelEntry(key=k, value=v) for k, v in sorted(six.iteritems(label_dict)) ] def _PerformRollback(self, deployment_name, error_message): # Print information about the failure. log.warning('There was an error deploying ' + deployment_name + ':\n' + error_message) log.status.Print('`--automatic-rollback-on-error` flag was supplied; ' 'deleting failed deployment...') # Delete the deployment. try: delete_operation = self.client.deployments.Delete( self.messages.DeploymentmanagerDeploymentsDeleteRequest( project=dm_base.GetProject(), deployment=deployment_name, ) ) except apitools_exceptions.HttpError as error: raise exceptions.HttpException(error, dm_api_util.HTTP_ERROR_FORMAT) # TODO(b/37481635): Use gcloud default operation polling. dm_write.WaitForOperation(self.client, self.messages, delete_operation.name, 'delete', dm_base.GetProject(), timeout=OPERATION_TIMEOUT) completed_operation = dm_api_util.GetOperation(self.client, self.messages, delete_operation, dm_base.GetProject()) return completed_operation @base.UnicodeIsSupported @base.ReleaseTracks(base.ReleaseTrack.ALPHA) @dm_base.UseDmApi(dm_base.DmApiVersion.ALPHA) class CreateAlpha(Create): """Create a deployment. This command inserts (creates) a new deployment based on a provided config file. """ @staticmethod def Args(parser): Create.Args(parser, version=base.ReleaseTrack.ALPHA) alpha_flags.AddCredentialFlag(parser) parser.display_info.AddFormat(alpha_flags.RESOURCES_AND_OUTPUTS_FORMAT) Create._create_policy_flag_map.choice_arg.AddToParser(parser) def _SetMetadata(self, args, deployment): if args.credential: deployment.credential = dm_util.CredentialFrom(self.messages, args.credential) super(CreateAlpha, self)._SetMetadata(args, deployment) def _BuildRequest(self, args, project, deployment): return super(CreateAlpha, self)._BuildRequest( args=args, project=project, deployment=deployment, supports_create_policy=True) @base.UnicodeIsSupported @base.ReleaseTracks(base.ReleaseTrack.BETA) @dm_base.UseDmApi(dm_base.DmApiVersion.V2BETA) class CreateBeta(Create): """Create a deployment. This command inserts (creates) a new deployment based on a provided config file. """ @staticmethod def Args(parser): Create.Args(parser, version=base.ReleaseTrack.BETA) Create._create_policy_flag_map.choice_arg.AddToParser(parser) def _BuildRequest(self, args, project, deployment): return super(CreateBeta, self)._BuildRequest( args=args, project=project, deployment=deployment, supports_create_policy=True)