1# -*- coding: utf-8 -*- # 2# Copyright 2014 Google LLC. All Rights Reserved. 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); 5# you may not use this file except in compliance with the License. 6# You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15 16"""deployments update command.""" 17 18from __future__ import absolute_import 19from __future__ import division 20from __future__ import unicode_literals 21 22from apitools.base.py import exceptions as apitools_exceptions 23 24from googlecloudsdk.api_lib.deployment_manager import dm_api_util 25from googlecloudsdk.api_lib.deployment_manager import dm_base 26from googlecloudsdk.api_lib.deployment_manager import dm_labels 27from googlecloudsdk.api_lib.util import apis 28from googlecloudsdk.calliope import base 29from googlecloudsdk.calliope import exceptions 30from googlecloudsdk.command_lib.deployment_manager import alpha_flags 31from googlecloudsdk.command_lib.deployment_manager import dm_util 32from googlecloudsdk.command_lib.deployment_manager import dm_write 33from googlecloudsdk.command_lib.deployment_manager import flags 34from googlecloudsdk.command_lib.deployment_manager import importer 35from googlecloudsdk.command_lib.util.apis import arg_utils 36from googlecloudsdk.command_lib.util.args import labels_util 37from googlecloudsdk.core import log 38from googlecloudsdk.core import properties 39 40import six 41 42# Number of seconds (approximately) to wait for update operation to complete. 43OPERATION_TIMEOUT = 20 * 60 # 20 mins 44 45 46@base.UnicodeIsSupported 47@base.ReleaseTracks(base.ReleaseTrack.GA) 48@dm_base.UseDmApi(dm_base.DmApiVersion.V2) 49class Update(base.UpdateCommand, dm_base.DmCommand): 50 """Update a deployment based on a provided config file. 51 52 This command will update a deployment with the new config file provided. 53 Different policies for create, update, and delete policies can be specified. 54 """ 55 56 detailed_help = { 57 'EXAMPLES': """ 58To update an existing deployment with a new config YAML file, run: 59 60 $ {command} my-deployment --config=new_config.yaml 61 62To update an existing deployment with a new config template file, run: 63 64 $ {command} my-deployment --template=new_config.{jinja|py} 65 66To update an existing deployment with a composite type as a new config, run: 67 68 $ {command} my-deployment --composite-type=<project-id>/composite:<new-config> 69 70 71To preview an update to an existing deployment without actually modifying the resources, run: 72 73 $ {command} my-deployment --config=new_config.yaml --preview 74 75To apply an update that has been previewed, provide the name of the previewed deployment, and no config file: 76 77 $ {command} my-deployment 78 79To specify different create, update, or delete policies, include any subset of the following flags: 80 81 $ {command} my-deployment --config=new_config.yaml --create-policy=acquire --delete-policy=abandon 82 83To perform an update without waiting for the operation to complete, run: 84 85 $ {command} my-deployment --config=new_config.yaml --async 86 87To update an existing deployment with a new config file and a fingerprint, run: 88 89 $ {command} my-deployment --config=new_config.yaml --fingerprint=deployment-fingerprint 90 91Either the `--config`, `--template`, or `--composite-type` flag is required unless launching an already-previewed update to a deployment. If you want to update a deployment's metadata, such as the labels or description, you must run a separate command with `--update-labels`, `--remove-labels`, or `--description`, as applicable. 92 93More information is available at https://cloud.google.com/deployment-manager/docs/deployments/updating-deployments. 94""", 95 } 96 97 _delete_policy_flag_map = flags.GetDeleteFlagEnumMap( 98 (apis.GetMessagesModule('deploymentmanager', 'v2') 99 .DeploymentmanagerDeploymentsUpdateRequest.DeletePolicyValueValuesEnum)) 100 101 _create_policy_flag_map = arg_utils.ChoiceEnumMapper( 102 '--create-policy', 103 (apis.GetMessagesModule('deploymentmanager', 'v2') 104 .DeploymentmanagerDeploymentsUpdateRequest.CreatePolicyValueValuesEnum), 105 help_str='Create policy for resources that have changed in the update', 106 default='create-or-acquire') 107 108 _create_policy_v2beta_flag_map = arg_utils.ChoiceEnumMapper( 109 '--create-policy', 110 (apis.GetMessagesModule('deploymentmanager', 'v2beta') 111 .DeploymentmanagerDeploymentsUpdateRequest.CreatePolicyValueValuesEnum), 112 help_str='Create policy for resources that have changed in the update', 113 default='create-or-acquire') 114 115 @staticmethod 116 def Args(parser, version=base.ReleaseTrack.GA): 117 """Args is called by calliope to gather arguments for this command. 118 119 Args: 120 parser: An argparse parser that you can use to add arguments that go 121 on the command line after this command. Positional arguments are 122 allowed. 123 version: The version this tool is running as. base.ReleaseTrack.GA 124 is the default. 125 """ 126 flags.AddDeploymentNameFlag(parser) 127 flags.AddPropertiesFlag(parser) 128 flags.AddAsyncFlag(parser) 129 130 parser.add_argument( 131 '--description', 132 help='The new description of the deployment.', 133 dest='description' 134 ) 135 136 group = parser.add_mutually_exclusive_group() 137 flags.AddConfigFlags(group) 138 139 if version in [base.ReleaseTrack.ALPHA, base.ReleaseTrack.BETA]: 140 group.add_argument( 141 '--manifest-id', 142 help='Manifest Id of a previous deployment. ' 143 'This flag cannot be used with --config.', 144 dest='manifest_id') 145 146 labels_util.AddUpdateLabelsFlags(parser, enable_clear=False) 147 148 parser.add_argument( 149 '--preview', 150 help='Preview the requested update without making any changes to the ' 151 'underlying resources. (default=False)', 152 dest='preview', 153 default=False, 154 action='store_true') 155 156 if version in [base.ReleaseTrack.ALPHA, base.ReleaseTrack.BETA]: 157 Update._create_policy_v2beta_flag_map.choice_arg.AddToParser(parser) 158 else: 159 Update._create_policy_flag_map.choice_arg.AddToParser(parser) 160 161 Update._delete_policy_flag_map.choice_arg.AddToParser(parser) 162 flags.AddFingerprintFlag(parser) 163 164 parser.display_info.AddFormat(flags.RESOURCES_AND_OUTPUTS_FORMAT) 165 166 def Epilog(self, resources_were_displayed): 167 """Called after resources are displayed if the default format was used. 168 169 Args: 170 resources_were_displayed: True if resources were displayed. 171 """ 172 if not resources_were_displayed: 173 log.status.Print('No resources or outputs found in your deployment.') 174 175 def Run(self, args): 176 """Run 'deployments update'. 177 178 Args: 179 args: argparse.Namespace, The arguments that this command was invoked 180 with. 181 182 Returns: 183 If --async=true, returns Operation to poll. 184 Else, returns a struct containing the list of resources and list of 185 outputs in the deployment. 186 187 Raises: 188 HttpException: An http error response was received while executing api 189 request. 190 """ 191 deployment_ref = self.resources.Parse( 192 args.deployment_name, 193 params={'project': properties.VALUES.core.project.GetOrFail}, 194 collection='deploymentmanager.deployments') 195 if not args.IsSpecified('format') and args.async_: 196 args.format = flags.OPERATION_FORMAT 197 198 patch_request = False 199 deployment = self.messages.Deployment( 200 name=deployment_ref.deployment, 201 ) 202 203 if not (args.config is None and args.template is None 204 and args.composite_type is None): 205 deployment.target = importer.BuildTargetConfig( 206 self.messages, 207 config=args.config, 208 template=args.template, 209 composite_type=args.composite_type, 210 properties=args.properties) 211 elif (self.ReleaseTrack() in [base.ReleaseTrack.ALPHA, 212 base.ReleaseTrack.BETA] 213 and args.manifest_id): 214 deployment.target = importer.BuildTargetConfigFromManifest( 215 self.client, self.messages, 216 dm_base.GetProject(), 217 deployment_ref.deployment, args.manifest_id, args.properties) 218 # Get the fingerprint from the deployment to update. 219 try: 220 current_deployment = self.client.deployments.Get( 221 self.messages.DeploymentmanagerDeploymentsGetRequest( 222 project=dm_base.GetProject(), 223 deployment=deployment_ref.deployment 224 ) 225 ) 226 227 if args.fingerprint: 228 deployment.fingerprint = dm_util.DecodeFingerprint(args.fingerprint) 229 else: 230 # If no fingerprint is present, default to an empty fingerprint. 231 # TODO(b/34966984): Remove the empty default after cleaning up all 232 # deployments that has no fingerprint 233 deployment.fingerprint = current_deployment.fingerprint or b'' 234 235 # Get the credential from the deployment to update. 236 if self.ReleaseTrack() in [base.ReleaseTrack.ALPHA] and args.credential: 237 deployment.credential = dm_util.CredentialFrom(self.messages, 238 args.credential) 239 240 # Update the labels of the deployment 241 242 deployment.labels = self._GetUpdatedDeploymentLabels( 243 args, current_deployment) 244 # If no config or manifest_id are specified, but try to update labels, 245 # only add patch_request header when directly updating a non-previewed 246 # deployment 247 248 no_manifest = (self.ReleaseTrack() is 249 base.ReleaseTrack.GA) or not args.manifest_id 250 patch_request = not args.config and no_manifest and ( 251 bool(args.update_labels) or bool(args.remove_labels)) 252 if args.description is None: 253 deployment.description = current_deployment.description 254 elif not args.description or args.description.isspace(): 255 deployment.description = None 256 else: 257 deployment.description = args.description 258 except apitools_exceptions.HttpError as error: 259 raise exceptions.HttpException(error, dm_api_util.HTTP_ERROR_FORMAT) 260 261 if patch_request: 262 args.format = flags.DEPLOYMENT_FORMAT 263 try: 264 # Necessary to handle API Version abstraction below 265 parsed_delete_flag = Update._delete_policy_flag_map.GetEnumForChoice( 266 args.delete_policy).name 267 if self.ReleaseTrack() in [ 268 base.ReleaseTrack.ALPHA, base.ReleaseTrack.BETA 269 ]: 270 parsed_create_flag = ( 271 Update._create_policy_v2beta_flag_map.GetEnumForChoice( 272 args.create_policy).name) 273 else: 274 parsed_create_flag = ( 275 Update._create_policy_flag_map.GetEnumForChoice( 276 args.create_policy).name) 277 request = self.messages.DeploymentmanagerDeploymentsUpdateRequest( 278 deploymentResource=deployment, 279 project=dm_base.GetProject(), 280 deployment=deployment_ref.deployment, 281 preview=args.preview, 282 createPolicy=(self.messages.DeploymentmanagerDeploymentsUpdateRequest. 283 CreatePolicyValueValuesEnum(parsed_create_flag)), 284 deletePolicy=(self.messages.DeploymentmanagerDeploymentsUpdateRequest. 285 DeletePolicyValueValuesEnum(parsed_delete_flag))) 286 client = self.client 287 client.additional_http_headers['X-Cloud-DM-Patch'] = six.text_type( 288 patch_request) 289 operation = client.deployments.Update(request) 290 291 # Fetch and print the latest fingerprint of the deployment. 292 updated_deployment = dm_api_util.FetchDeployment( 293 self.client, self.messages, dm_base.GetProject(), 294 deployment_ref.deployment) 295 if patch_request: 296 if args.async_: 297 log.warning( 298 'Updating Deployment metadata is synchronous, --async flag ' 299 'is ignored.') 300 log.status.Print('Update deployment metadata completed successfully.') 301 return updated_deployment 302 dm_util.PrintFingerprint(updated_deployment.fingerprint) 303 except apitools_exceptions.HttpError as error: 304 raise exceptions.HttpException(error, dm_api_util.HTTP_ERROR_FORMAT) 305 if args.async_: 306 return operation 307 else: 308 op_name = operation.name 309 try: 310 operation = dm_write.WaitForOperation( 311 self.client, 312 self.messages, 313 op_name, 314 'update', 315 dm_base.GetProject(), 316 timeout=OPERATION_TIMEOUT) 317 dm_util.LogOperationStatus(operation, 'Update') 318 except apitools_exceptions.HttpError as error: 319 raise exceptions.HttpException(error, dm_api_util.HTTP_ERROR_FORMAT) 320 321 return dm_api_util.FetchResourcesAndOutputs( 322 self.client, self.messages, dm_base.GetProject(), 323 deployment_ref.deployment, 324 self.ReleaseTrack() is base.ReleaseTrack.ALPHA) 325 326 def _GetUpdatedDeploymentLabels(self, args, deployment): 327 update_labels = labels_util.GetUpdateLabelsDictFromArgs(args) 328 remove_labels = labels_util.GetRemoveLabelsListFromArgs(args) 329 return dm_labels.UpdateLabels(deployment.labels, 330 self.messages.DeploymentLabelEntry, 331 update_labels, remove_labels) 332 333 334@base.UnicodeIsSupported 335@base.ReleaseTracks(base.ReleaseTrack.ALPHA) 336@dm_base.UseDmApi(dm_base.DmApiVersion.ALPHA) 337class UpdateAlpha(Update): 338 """Update a deployment based on a provided config file. 339 340 This command will update a deployment with the new config file provided. 341 Different policies for create, update, and delete policies can be specified. 342 """ 343 344 @staticmethod 345 def Args(parser): 346 Update.Args(parser, version=base.ReleaseTrack.ALPHA) 347 alpha_flags.AddCredentialFlag(parser) 348 parser.display_info.AddFormat(alpha_flags.RESOURCES_AND_OUTPUTS_FORMAT) 349 350 351@base.UnicodeIsSupported 352@base.ReleaseTracks(base.ReleaseTrack.BETA) 353@dm_base.UseDmApi(dm_base.DmApiVersion.V2BETA) 354class UpdateBeta(Update): 355 """Update a deployment based on a provided config file. 356 357 This command will update a deployment with the new config file provided. 358 Different policies for create, update, and delete policies can be specified. 359 """ 360 361 @staticmethod 362 def Args(parser): 363 Update.Args(parser, version=base.ReleaseTrack.BETA) 364