1# -*- coding: utf-8 -*- # 2# Copyright 2018 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"""Class for representing various changes to a Configuration.""" 16 17from __future__ import absolute_import 18from __future__ import division 19from __future__ import print_function 20from __future__ import unicode_literals 21 22import abc 23import copy 24 25from googlecloudsdk.api_lib.run import container_resource 26from googlecloudsdk.api_lib.run import job 27from googlecloudsdk.api_lib.run import k8s_object 28from googlecloudsdk.api_lib.run import revision 29from googlecloudsdk.api_lib.run import service 30from googlecloudsdk.calliope import base 31from googlecloudsdk.command_lib.run import exceptions 32from googlecloudsdk.command_lib.run import name_generator 33from googlecloudsdk.command_lib.run import platforms 34from googlecloudsdk.command_lib.util.args import labels_util 35from googlecloudsdk.command_lib.util.args import repeated 36 37import six 38 39 40class ConfigChanger(six.with_metaclass(abc.ABCMeta, object)): 41 """An abstract class representing configuration changes.""" 42 43 @abc.abstractmethod 44 def Adjust(self, resource): 45 """Adjust the given Service configuration. 46 47 Args: 48 resource: the k8s_object to adjust. 49 50 Returns: 51 A k8s_object that reflects applying the requested update. 52 May be resource after a mutation or a different object. 53 """ 54 return resource 55 56 57def WithChanges(resource, changes): 58 """Apply ConfigChangers to resource. 59 60 It's undefined whether the input resource is modified. 61 62 Args: 63 resource: KubernetesObject, probably a Service. 64 changes: List of ConfigChangers. 65 66 Returns: 67 Changed resource. 68 """ 69 for config_change in changes: 70 resource = config_change.Adjust(resource) 71 return resource 72 73 74def _AssertValidSecretKey(key, platform): 75 if platform == platforms.PLATFORM_MANAGED: 76 if not (key.isdigit() or key == 'latest'): 77 raise exceptions.ConfigurationError( 78 "Secret key must be an integer or 'latest'.") 79 80 81class LabelChanges(ConfigChanger): 82 """Represents the user intent to modify metadata labels.""" 83 84 LABELS_NOT_ALLOWED_IN_REVISION = ([service.ENDPOINT_VISIBILITY]) 85 86 def __init__(self, diff, copy_to_revision=True): 87 super(LabelChanges, self).__init__() 88 self._diff = diff 89 self._copy_to_revision = copy_to_revision 90 91 def Adjust(self, resource): 92 # Currently assumes all "system"-owned labels are applied by the control 93 # plane and it's ok for us to clear them on the client. 94 update_result = self._diff.Apply( 95 k8s_object.Meta(resource.MessagesModule()).LabelsValue, 96 resource.metadata.labels) 97 maybe_new_labels = update_result.GetOrNone() 98 if maybe_new_labels: 99 resource.metadata.labels = maybe_new_labels 100 if self._copy_to_revision and hasattr(resource.template, 'labels'): 101 # Service labels are the source of truth and *overwrite* revision labels 102 # See go/run-labels-prd for deets. 103 # However, we need to preserve the nonce if there is one. 104 nonce = resource.template.labels.get(revision.NONCE_LABEL) 105 resource.template.metadata.labels = copy.deepcopy(maybe_new_labels) 106 for label_to_remove in self.LABELS_NOT_ALLOWED_IN_REVISION: 107 if label_to_remove in resource.template.labels: 108 del resource.template.labels[label_to_remove] 109 if nonce: 110 resource.template.labels[revision.NONCE_LABEL] = nonce 111 return resource 112 113 114class ReplaceServiceChange(ConfigChanger): 115 """Represents the user intent to replace the service.""" 116 117 def __init__(self, new_service): 118 super(ReplaceServiceChange, self).__init__() 119 self._service = new_service 120 121 def Adjust(self, resource): 122 """Returns a replacement for resource. 123 124 The returned service is the service provided to the constructor. If 125 resource.metadata.resourceVersion is not empty to None returned service 126 has metadata.resourceVersion set to this value. 127 128 Args: 129 resource: service.Service, The service to adjust. 130 """ 131 if resource.metadata.resourceVersion: 132 self._service.metadata.resourceVersion = resource.metadata.resourceVersion 133 # Knative will complain if you try to edit (incl remove) serving annots. 134 # So replicate them here. 135 for k, v in resource.annotations.items(): 136 if k.startswith(k8s_object.SERVING_GROUP): 137 self._service.annotations[k] = v 138 return self._service 139 140 141class EndpointVisibilityChange(LabelChanges): 142 """Represents the user intent to modify the endpoint visibility. 143 144 Only applies to Cloud Run for Anthos. 145 """ 146 147 def __init__(self, endpoint_visibility): 148 """Determine label changes for modifying endpoint visibility. 149 150 Args: 151 endpoint_visibility: bool, True if Cloud Run on GKE service should only be 152 addressable from within the cluster. False if it should be publicly 153 addressable. 154 """ 155 if endpoint_visibility: 156 diff = labels_util.Diff( 157 additions={service.ENDPOINT_VISIBILITY: service.CLUSTER_LOCAL}) 158 else: 159 diff = labels_util.Diff(subtractions=[service.ENDPOINT_VISIBILITY]) 160 # Don't copy this label to the revision because it's not supported there. 161 # See b/154664962. 162 super(EndpointVisibilityChange, self).__init__(diff, False) 163 164 165class SetAnnotationChange(ConfigChanger): 166 """Represents the user intent to set an annotation.""" 167 168 def __init__(self, key, value): 169 super(SetAnnotationChange, self).__init__() 170 self._key = key 171 self._value = value 172 173 def Adjust(self, resource): 174 resource.annotations[self._key] = self._value 175 return resource 176 177 178class DeleteAnnotationChange(ConfigChanger): 179 """Represents the user intent to delete an annotation.""" 180 181 def __init__(self, key): 182 super(DeleteAnnotationChange, self).__init__() 183 self._key = key 184 185 def Adjust(self, resource): 186 annotations = resource.annotations 187 if self._key in annotations: 188 del annotations[self._key] 189 return resource 190 191 192class SetTemplateAnnotationChange(ConfigChanger): 193 """Represents the user intent to set a template annotation.""" 194 195 def __init__(self, key, value): 196 super(SetTemplateAnnotationChange, self).__init__() 197 self._key = key 198 self._value = value 199 200 def Adjust(self, resource): 201 resource.template.annotations[self._key] = self._value 202 return resource 203 204 205class DeleteTemplateAnnotationChange(ConfigChanger): 206 """Represents the user intent to delete a template annotation.""" 207 208 def __init__(self, key): 209 super(DeleteTemplateAnnotationChange, self).__init__() 210 self._key = key 211 212 def Adjust(self, resource): 213 annotations = resource.template.annotations 214 if self._key in annotations: 215 del annotations[self._key] 216 return resource 217 218 219class SetLaunchStageAnnotationChange(ConfigChanger): 220 """Sets a VPC connector annotation on the service.""" 221 222 def __init__(self, launch_stage): 223 super(SetLaunchStageAnnotationChange, self).__init__() 224 self._launch_stage = launch_stage 225 226 def Adjust(self, resource): 227 if self._launch_stage == base.ReleaseTrack.GA: 228 return resource 229 else: 230 resource.annotations[ 231 k8s_object.LAUNCH_STAGE_ANNOTATION] = self._launch_stage.id 232 return resource 233 234 235class SetClientNameAndVersionAnnotationChange(ConfigChanger): 236 """Sets the client name and version annotations.""" 237 238 def __init__(self, client_name, client_version): 239 super(SetClientNameAndVersionAnnotationChange, self).__init__() 240 self._client_name = client_name 241 self._client_version = client_version 242 243 def Adjust(self, resource): 244 if self._client_name is not None: 245 resource.annotations[ 246 k8s_object.CLIENT_NAME_ANNOTATION] = self._client_name 247 resource.template.annotations[ 248 k8s_object.CLIENT_NAME_ANNOTATION] = self._client_name 249 if self._client_version is not None: 250 resource.annotations[ 251 k8s_object.CLIENT_VERSION_ANNOTATION] = self._client_version 252 resource.template.annotations[ 253 k8s_object.CLIENT_VERSION_ANNOTATION] = self._client_version 254 return resource 255 256 257class SandboxChange(ConfigChanger): 258 """Sets a sandbox annotation on the service.""" 259 260 def __init__(self, sandbox): 261 super(SandboxChange, self).__init__() 262 self._sandbox = sandbox 263 264 def Adjust(self, resource): 265 resource.template.annotations[ 266 container_resource.SANDBOX_ANNOTATION] = self._sandbox 267 return resource 268 269 270class VpcConnectorChange(ConfigChanger): 271 """Sets a VPC connector annotation on the service.""" 272 273 def __init__(self, connector_name): 274 super(VpcConnectorChange, self).__init__() 275 self._connector_name = connector_name 276 277 def Adjust(self, resource): 278 resource.template.annotations[ 279 container_resource.VPC_ACCESS_ANNOTATION] = self._connector_name 280 return resource 281 282 283class ClearVpcConnectorChange(ConfigChanger): 284 """Clears a VPC connector annotation on the service.""" 285 286 def Adjust(self, resource): 287 annotations = resource.template.annotations 288 if container_resource.VPC_ACCESS_ANNOTATION in annotations: 289 del annotations[container_resource.VPC_ACCESS_ANNOTATION] 290 if container_resource.EGRESS_SETTINGS_ANNOTATION in annotations: 291 del annotations[container_resource.EGRESS_SETTINGS_ANNOTATION] 292 return resource 293 294 295class ImageChange(ConfigChanger): 296 """A Cloud Run container deployment.""" 297 298 deployment_type = 'container' 299 300 def __init__(self, image): 301 super(ImageChange, self).__init__() 302 self.image = image 303 304 def Adjust(self, resource): 305 resource.annotations[container_resource.USER_IMAGE_ANNOTATION] = ( 306 self.image) 307 if hasattr(resource.template, 'annotations'): 308 resource.template.annotations[ 309 container_resource.USER_IMAGE_ANNOTATION] = ( 310 self.image) 311 resource.image = self.image 312 return resource 313 314 315def _PruneMapping(mapping, keys_to_remove, clear_others): 316 if clear_others: 317 mapping.clear() 318 else: 319 for var_or_path in keys_to_remove: 320 if var_or_path in mapping: 321 del mapping[var_or_path] 322 323 324class EnvVarLiteralChanges(ConfigChanger): 325 """Represents the user intent to modify environment variables string literals.""" 326 327 def __init__(self, updates, removes, clear_others): 328 """Initialize a new EnvVarLiteralChanges object. 329 330 Args: 331 updates: {str, str}, Update env var names and values. 332 removes: [str], List of env vars to remove. 333 clear_others: bool, If true, clear all non-updated env vars. 334 """ 335 super(EnvVarLiteralChanges, self).__init__() 336 self._updates = updates 337 self._removes = removes 338 self._clear_others = clear_others 339 340 def Adjust(self, resource): 341 """Mutates the given config's env vars to match the desired changes. 342 343 Args: 344 resource: k8s_object to adjust 345 346 Returns: 347 The adjusted resource 348 349 Raises: 350 ConfigurationError if there's an attempt to replace the source of an 351 existing environment variable whose source is of a different type 352 (e.g. env var's secret source can't be replaced with a config map 353 source). 354 """ 355 _PruneMapping(resource.template.env_vars.literals, self._removes, 356 self._clear_others) 357 358 try: 359 resource.template.env_vars.literals.update(self._updates) 360 except KeyError as e: 361 raise exceptions.ConfigurationError( 362 'Cannot update environment variable [{}] to string literal ' 363 'because it has already been set with a different type.'.format( 364 e.args[0])) 365 return resource 366 367 368class SecretEnvVarChanges(ConfigChanger): 369 """Represents the user intent to modify environment variable secrets.""" 370 371 def __init__(self, updates, removes, clear_others): 372 """Initialize a new SecretEnvVarChanges object. 373 374 Args: 375 updates: {str, str}, Update env var names and values. 376 removes: [str], List of env vars to remove. 377 clear_others: bool, If true, clear all non-updated env vars. 378 379 Raises: 380 ConfigurationError if a key hasn't been provided for a source. 381 """ 382 super(SecretEnvVarChanges, self).__init__() 383 self._updates = {} 384 for name, v in updates.items(): 385 # Split the given values into 2 parts: 386 # [env var source name, source data item key] 387 value = v.split(':', 1) 388 if len(value) < 2: 389 value.append(self._OmittedSecretKeyDefault(name)) 390 self._updates[name] = value 391 self._removes = removes 392 self._clear_others = clear_others 393 394 def _OmittedSecretKeyDefault(self, name): 395 if platforms.GetPlatform() == platforms.PLATFORM_MANAGED: 396 return 'latest' 397 raise exceptions.ConfigurationError( 398 'Missing required item key for environment variable [{}].'.format(name)) 399 400 def Adjust(self, resource): 401 """Mutates the given config's env vars to match the desired changes. 402 403 Args: 404 resource: k8s_object to adjust 405 406 Returns: 407 The adjusted resource 408 409 Raises: 410 ConfigurationError if there's an attempt to replace the source of an 411 existing environment variable whose source is of a different type 412 (e.g. env var's secret source can't be replaced with a config map 413 source). 414 """ 415 env_vars = resource.template.env_vars.secrets 416 _PruneMapping(env_vars, self._removes, self._clear_others) 417 418 for name, (source_name, source_key) in self._updates.items(): 419 try: 420 env_vars[name] = self._MakeEnvVarSource(resource.MessagesModule(), 421 source_name, source_key) 422 except KeyError: 423 raise exceptions.ConfigurationError( 424 'Cannot update environment variable [{}] to the given type ' 425 'because it has already been set with a different type.'.format( 426 name)) 427 return resource 428 429 def _MakeEnvVarSource(self, messages, name, key): 430 _AssertValidSecretKey(key, platforms.GetPlatform()) 431 return messages.EnvVarSource( 432 secretKeyRef=messages.SecretKeySelector(name=name, key=key)) 433 434 435class ConfigMapEnvVarChanges(ConfigChanger): 436 """Represents the user intent to modify environment variable config maps.""" 437 438 def __init__(self, updates, removes, clear_others): 439 """Initialize a new ConfigMapEnvVarChanges object. 440 441 Args: 442 updates: {str, str}, Update env var names and values. 443 removes: [str], List of env vars to remove. 444 clear_others: bool, If true, clear all non-updated env vars. 445 446 Raises: 447 ConfigurationError if a key hasn't been provided for a source. 448 """ 449 super(ConfigMapEnvVarChanges, self).__init__() 450 self._updates = {} 451 for name, v in updates.items(): 452 # Split the given values into 2 parts: 453 # [env var source name, source data item key] 454 value = v.split(':', 1) 455 if len(value) < 2: 456 value.append(self._OmittedSecretKeyDefault(name)) 457 self._updates[name] = value 458 self._removes = removes 459 self._clear_others = clear_others 460 461 def _OmittedSecretKeyDefault(self, name): 462 if platforms.GetPlatform() == platforms.PLATFORM_MANAGED: 463 return 'latest' 464 raise exceptions.ConfigurationError( 465 'Missing required item key for environment variable [{}].'.format(name)) 466 467 def Adjust(self, resource): 468 """Mutates the given config's env vars to match the desired changes. 469 470 Args: 471 resource: k8s_object to adjust 472 473 Returns: 474 The adjusted resource 475 476 Raises: 477 ConfigurationError if there's an attempt to replace the source of an 478 existing environment variable whose source is of a different type 479 (e.g. env var's secret source can't be replaced with a config map 480 source). 481 """ 482 env_vars = resource.template.env_vars.config_maps 483 _PruneMapping(env_vars, self._removes, self._clear_others) 484 485 for name, (source_name, source_key) in self._updates.items(): 486 try: 487 env_vars[name] = self._MakeEnvVarSource(resource.MessagesModule(), 488 source_name, source_key) 489 except KeyError: 490 raise exceptions.ConfigurationError( 491 'Cannot update environment variable [{}] to the given type ' 492 'because it has already been set with a different type.'.format( 493 name)) 494 return resource 495 496 def _MakeEnvVarSource(self, messages, name, key): 497 return messages.EnvVarSource( 498 configMapKeyRef=messages.ConfigMapKeySelector( 499 name=name, 500 key=key)) 501 502 503class ResourceChanges(ConfigChanger): 504 """Represents the user intent to update resource limits.""" 505 506 def __init__(self, memory=None, cpu=None): 507 super(ResourceChanges, self).__init__() 508 self._memory = memory 509 self._cpu = cpu 510 511 def Adjust(self, resource): 512 """Mutates the given config's resource limits to match what's desired.""" 513 if self._memory is not None: 514 resource.template.resource_limits['memory'] = self._memory 515 if self._cpu is not None: 516 resource.template.resource_limits['cpu'] = self._cpu 517 return resource 518 519 520class CloudSQLChanges(ConfigChanger): 521 """Represents the intent to update the Cloug SQL instances.""" 522 523 def __init__(self, project, region, args): 524 """Initializes the intent to update the Cloud SQL instances. 525 526 Args: 527 project: Project to use as the default project for Cloud SQL instances. 528 region: Region to use as the default region for Cloud SQL instances 529 args: Args to the command. 530 """ 531 super(CloudSQLChanges, self).__init__() 532 self._project = project 533 self._region = region 534 self._args = args 535 536 # Here we are a proxy through to the actual args to set some extra augmented 537 # information on each one, so each cloudsql instance gets the region and 538 # project. 539 @property 540 def add_cloudsql_instances(self): 541 return self._AugmentArgs('add_cloudsql_instances') 542 543 @property 544 def remove_cloudsql_instances(self): 545 return self._AugmentArgs('remove_cloudsql_instances') 546 547 @property 548 def set_cloudsql_instances(self): 549 return self._AugmentArgs('set_cloudsql_instances') 550 551 @property 552 def clear_cloudsql_instances(self): 553 return getattr(self._args, 'clear_cloudsql_instances', None) 554 555 def _AugmentArgs(self, arg_name): 556 val = getattr(self._args, arg_name, None) 557 if val is None: 558 return None 559 return [self._Augment(i) for i in val] 560 561 def Adjust(self, resource): 562 def GetCurrentInstances(): 563 annotation_val = resource.template.annotations.get( 564 container_resource.CLOUDSQL_ANNOTATION) 565 if annotation_val: 566 return annotation_val.split(',') 567 return [] 568 569 instances = repeated.ParsePrimitiveArgs( 570 self, 'cloudsql-instances', GetCurrentInstances) 571 if instances is not None: 572 resource.template.annotations[ 573 container_resource.CLOUDSQL_ANNOTATION] = ','.join(instances) 574 return resource 575 576 def _Augment(self, instance_str): 577 instance = instance_str.split(':') 578 if len(instance) == 3: 579 ret = tuple(instance) 580 elif len(instance) == 1: 581 if not self._project: 582 raise exceptions.CloudSQLError( 583 'To specify a Cloud SQL instance by plain name, you must specify a ' 584 'project.') 585 if not self._region: 586 raise exceptions.CloudSQLError( 587 'To specify a Cloud SQL instance by plain name, you must be ' 588 'deploying to a managed Cloud Run region.') 589 ret = self._project, self._region, instance[0] 590 else: 591 raise exceptions.CloudSQLError( 592 'Malformed CloudSQL instance string: {}'.format( 593 instance_str)) 594 return ':'.join(ret) 595 596 597class ConcurrencyChanges(ConfigChanger): 598 """Represents the user intent to update concurrency preference.""" 599 600 def __init__(self, concurrency): 601 super(ConcurrencyChanges, self).__init__() 602 self._concurrency = None if concurrency == 'default' else int(concurrency) 603 604 def Adjust(self, resource): 605 """Mutates the given config's resource limits to match what's desired.""" 606 resource.template.concurrency = self._concurrency 607 return resource 608 609 610class TimeoutChanges(ConfigChanger): 611 """Represents the user intent to update request duration.""" 612 613 def __init__(self, timeout): 614 super(TimeoutChanges, self).__init__() 615 self._timeout = timeout 616 617 def Adjust(self, resource): 618 """Mutates the given config's timeout to match what's desired.""" 619 resource.template.timeout = self._timeout 620 return resource 621 622 623class ServiceAccountChanges(ConfigChanger): 624 """Represents the user intent to change service account for the revision.""" 625 626 def __init__(self, service_account): 627 super(ServiceAccountChanges, self).__init__() 628 self._service_account = service_account 629 630 def Adjust(self, resource): 631 """Mutates the given config's service account to match what's desired.""" 632 resource.template.service_account = self._service_account 633 return resource 634 635 636_MAX_RESOURCE_NAME_LENGTH = 63 637 638 639class RevisionNameChanges(ConfigChanger): 640 """Represents the user intent to change revision name.""" 641 642 def __init__(self, revision_suffix): 643 super(RevisionNameChanges, self).__init__() 644 self._revision_suffix = revision_suffix 645 646 def Adjust(self, resource): 647 """Mutates the given config's revision name to match what's desired.""" 648 max_prefix_length = ( 649 _MAX_RESOURCE_NAME_LENGTH - len(self._revision_suffix) - 1) 650 resource.template.name = '{}-{}'.format(resource.name[:max_prefix_length], 651 self._revision_suffix) 652 return resource 653 654 655def _GenerateVolumeName(prefix): 656 """Randomly generated name with the given prefix.""" 657 return name_generator.GenerateName(sections=3, separator='-', prefix=prefix) 658 659 660def _UniqueVolumeName(source_name, existing_volumes): 661 """Generate unique volume name. 662 663 The names that connect volumes and mounts must be unique even if their 664 source volume names match. 665 666 Args: 667 source_name: (Potentially clashing) name. 668 existing_volumes: Names in use. 669 670 Returns: 671 Unique name. 672 """ 673 volume_name = None 674 while volume_name is None or volume_name in existing_volumes: 675 volume_name = _GenerateVolumeName(source_name) 676 return volume_name 677 678 679def _PruneVolumes(volume_mounts, volumes): 680 """Delete all volumes no longer being mounted. 681 682 Args: 683 volume_mounts: resource.template.volume_mounts 684 volumes: resource.template.volumes 685 """ 686 for volume in list(volumes): 687 if volume not in volume_mounts.values(): 688 del volumes[volume] 689 690 691class SecretVolumeChanges(ConfigChanger): 692 """Represents the user intent to change volumes with secret source types.""" 693 694 def __init__(self, updates, removes, clear_others): 695 """Initialize a new SecretVolumeChanges object. 696 697 Args: 698 updates: {str, str}, Update mount path and volume fields. 699 removes: [str], List of mount paths to remove. 700 clear_others: bool, If true, clear all non-updated volumes and mounts of 701 the given [volume_type]. 702 """ 703 super(SecretVolumeChanges, self).__init__() 704 self._updates = {} 705 for k, v in updates.items(): 706 # Split the given values into 2 parts: 707 # [volume source name, data item key] 708 update_value = v.split(':', 1) 709 # Pad with None if no data item key specified 710 if len(update_value) < 2: 711 update_value.append(None) 712 self._updates[k] = update_value 713 self._removes = removes 714 self._clear_others = clear_others 715 716 def Adjust(self, resource): 717 """Mutates the given config's volumes to match the desired changes. 718 719 Args: 720 resource: k8s_object to adjust 721 722 Returns: 723 The adjusted resource 724 725 Raises: 726 ConfigurationError if there's an attempt to replace the volume a mount 727 points to whose existing volume has a source of a different type than 728 the new volume (e.g. mount that points to a volume with a secret source 729 can't be replaced with a volume that has a config map source). 730 """ 731 volume_mounts = resource.template.volume_mounts.secrets 732 volumes = resource.template.volumes.secrets 733 734 _PruneMapping(volume_mounts, self._removes, self._clear_others) 735 736 for file_path, (source_name, source_key) in self._updates.items(): 737 volume_name = _UniqueVolumeName(source_name, resource.template.volumes) 738 739 # volume_mounts is a special mapping that filters for the current kind 740 # of mount and KeyErrors on existing keys with other types. 741 try: 742 volume_mounts[file_path] = volume_name 743 except KeyError: 744 raise exceptions.ConfigurationError( 745 'Cannot update mount [{}] because its mounted volume ' 746 'is of a different source type.'.format(file_path)) 747 volumes[volume_name] = self._MakeVolumeSource(resource.MessagesModule(), 748 source_name, source_key) 749 750 _PruneVolumes(volume_mounts, volumes) 751 return resource 752 753 def _MakeVolumeSource(self, messages, name, key=None): 754 source = messages.SecretVolumeSource(secretName=name) 755 if key is not None: 756 _AssertValidSecretKey(key, platforms.GetPlatform()) 757 source.items.append(messages.KeyToPath(key=key, path=key)) 758 return source 759 760 761class ConfigMapVolumeChanges(ConfigChanger): 762 """Represents the user intent to change volumes with config map source types.""" 763 764 def __init__(self, updates, removes, clear_others): 765 """Initialize a new ConfigMapVolumeChanges object. 766 767 Args: 768 updates: {str, [str, str]}, Update mount path and volume fields. 769 removes: [str], List of mount paths to remove. 770 clear_others: bool, If true, clear all non-updated volumes and mounts of 771 the given [volume_type]. 772 """ 773 super(ConfigMapVolumeChanges, self).__init__() 774 self._updates = {} 775 for k, v in updates.items(): 776 # Split the given values into 2 parts: 777 # [volume source name, data item key] 778 update_value = v.split(':', 1) 779 # Pad with None if no data item key specified 780 if len(update_value) < 2: 781 update_value.append(None) 782 self._updates[k] = update_value 783 self._removes = removes 784 self._clear_others = clear_others 785 786 def Adjust(self, resource): 787 """Mutates the given config's volumes to match the desired changes. 788 789 Args: 790 resource: k8s_object to adjust 791 792 Returns: 793 The adjusted resource 794 795 Raises: 796 ConfigurationError if there's an attempt to replace the volume a mount 797 points to whose existing volume has a source of a different type than 798 the new volume (e.g. mount that points to a volume with a secret source 799 can't be replaced with a volume that has a config map source). 800 """ 801 volume_mounts = resource.template.volume_mounts.config_maps 802 volumes = resource.template.volumes.config_maps 803 804 _PruneMapping(volume_mounts, self._removes, self._clear_others) 805 806 for path, (source_name, source_key) in self._updates.items(): 807 volume_name = _UniqueVolumeName(source_name, resource.template.volumes) 808 809 # volume_mounts is a special mapping that filters for the current kind 810 # of mount and KeyErrors on existing keys with other types. 811 try: 812 volume_mounts[path] = volume_name 813 except KeyError: 814 raise exceptions.ConfigurationError( 815 'Cannot update mount [{}] because its mounted volume ' 816 'is of a different source type.'.format(path)) 817 volumes[volume_name] = self._MakeVolumeSource(resource.MessagesModule(), 818 source_name, source_key) 819 820 _PruneVolumes(volume_mounts, volumes) 821 822 return resource 823 824 def _MakeVolumeSource(self, messages, name, key=None): 825 source = messages.ConfigMapVolumeSource(name=name) 826 if key is not None: 827 source.items.append(messages.KeyToPath(key=key, path=key)) 828 return source 829 830 831class NoTrafficChange(ConfigChanger): 832 """Represents the user intent to block traffic for a new revision.""" 833 834 def Adjust(self, resource): 835 """Removes LATEST from the services traffic assignments.""" 836 if resource.configuration: 837 raise exceptions.UnsupportedOperationError( 838 'This service is using an old version of Cloud Run for Anthos ' 839 'that does not support traffic features. Please upgrade to 0.8 ' 840 'or later.') 841 842 if not resource.generation: 843 raise exceptions.ConfigurationError( 844 '--no-traffic not supported when creating a new service.') 845 846 resource.spec_traffic.ZeroLatestTraffic( 847 resource.status.latestReadyRevisionName) 848 return resource 849 850 851class TrafficChanges(ConfigChanger): 852 """Represents the user intent to change a service's traffic assignments.""" 853 854 def __init__(self, 855 new_percentages, 856 by_tag=False, 857 tags_to_update=None, 858 tags_to_remove=None, 859 clear_other_tags=False): 860 super(TrafficChanges, self).__init__() 861 self._new_percentages = new_percentages 862 self._by_tag = by_tag 863 self._tags_to_update = tags_to_update or {} 864 self._tags_to_remove = tags_to_remove or [] 865 self._clear_other_tags = clear_other_tags 866 867 def Adjust(self, resource): 868 """Mutates the given service's traffic assignments.""" 869 if self._tags_to_update or self._tags_to_remove or self._clear_other_tags: 870 resource.spec_traffic.UpdateTags(self._tags_to_update, 871 self._tags_to_remove, 872 self._clear_other_tags) 873 if self._new_percentages: 874 if self._by_tag: 875 tag_to_key = resource.spec_traffic.TagToKey() 876 percentages = {} 877 for tag in self._new_percentages: 878 try: 879 percentages[tag_to_key[tag]] = self._new_percentages[tag] 880 except KeyError: 881 raise exceptions.ConfigurationError( 882 'There is no revision tagged with [{}] in the traffic allocation for [{}].' 883 .format(tag, resource.name)) 884 else: 885 percentages = self._new_percentages 886 resource.spec_traffic.UpdateTraffic(percentages) 887 return resource 888 889 890class TagOnDeployChange(ConfigChanger): 891 """The intent to provide a tag for the revision you're currently deploying.""" 892 893 def __init__(self, tag): 894 super(TagOnDeployChange, self).__init__() 895 self._tag = tag 896 897 def Adjust(self, resource): 898 """Gives the revision that's being created the given tag.""" 899 tags_to_update = {self._tag: resource.template.name} 900 resource.spec_traffic.UpdateTags(tags_to_update, [], False) 901 return resource 902 903 904class ContainerCommandChange(ConfigChanger): 905 """Represents the user intent to change the 'command' for the container.""" 906 907 def __init__(self, command): 908 super(ContainerCommandChange, self).__init__() 909 self._commands = command 910 911 def Adjust(self, resource): 912 resource.template.container.command = self._commands 913 return resource 914 915 916class ContainerArgsChange(ConfigChanger): 917 """Represents the user intent to change the 'args' for the container.""" 918 919 def __init__(self, args): 920 super(ContainerArgsChange, self).__init__() 921 self._args = args 922 923 def Adjust(self, resource): 924 resource.template.container.args = self._args 925 return resource 926 927 928_HTTP2_NAME = 'h2c' 929_DEFAULT_PORT = 8080 930 931 932class ContainerPortChange(ConfigChanger): 933 """Represents the user intent to change the port name and/or number.""" 934 935 def __init__(self, port=None, use_http2=None): 936 """Initialize a ContainerPortChange. 937 938 Args: 939 port: str, the port number to set the port to, "default" to unset the 940 containerPort field, or None to not modify the port number. 941 use_http2: bool, True to set the port name for http/2, False to unset it, 942 or None to not modify the port name. 943 """ 944 super(ContainerPortChange, self).__init__() 945 self._port = port 946 self._http2 = use_http2 947 948 def Adjust(self, resource): 949 """Modify an existing ContainerPort or create a new one.""" 950 port_msg = ( 951 resource.template.container.ports[0] 952 if resource.template.container.ports else 953 resource.MessagesModule().ContainerPort()) 954 # Set port to given value or clear field 955 if self._port == 'default': 956 port_msg.reset('containerPort') 957 elif self._port is not None: 958 port_msg.containerPort = int(self._port) 959 # Set name for http/2 or clear field 960 if self._http2: 961 port_msg.name = _HTTP2_NAME 962 elif self._http2 is not None: 963 port_msg.reset('name') 964 # A port number must be specified 965 if port_msg.name and not port_msg.containerPort: 966 port_msg.containerPort = _DEFAULT_PORT 967 968 # Use the ContainerPort iff it's not empty 969 if port_msg.containerPort: 970 resource.template.container.ports = [port_msg] 971 else: 972 resource.template.container.reset('ports') 973 return resource 974 975 976class SpecChange(ConfigChanger): 977 """Represents the user intent to update field in the resource's spec.""" 978 979 def __init__(self, field, value): 980 super(SpecChange, self).__init__() 981 self._field = field 982 self._value = value 983 984 def Adjust(self, resource): 985 setattr(resource.spec, self._field, self._value) 986 return resource 987 988 989class JobMaxAttemptsChange(ConfigChanger): 990 """Represents the user intent to update a job's restart policy.""" 991 992 def __init__(self, max_attempts): 993 super(JobMaxAttemptsChange, self).__init__() 994 self._max_attempts = max_attempts 995 996 def Adjust(self, resource): 997 if self._max_attempts == 1: 998 resource.template.restart_policy = job.RestartPolicy.NEVER 999 resource.backoff_limit = 0 1000 else: 1001 resource.template.restart_policy = job.RestartPolicy.ON_FAILURE 1002 resource.backoff_limit = self._max_attempts - 1 1003 return resource 1004