# -*- coding: utf-8 -*- # # Copyright 2020 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. """Wraps a resource message with a container with convenience methods.""" from __future__ import absolute_import from __future__ import division from __future__ import unicode_literals import collections import json from typing import Mapping, Sequence from googlecloudsdk.api_lib.run import k8s_object try: # Python 3.3 and above. collections_abc = collections.abc except AttributeError: collections_abc = collections CLOUDSQL_ANNOTATION = k8s_object.RUN_GROUP + '/cloudsql-instances' VPC_ACCESS_ANNOTATION = 'run.googleapis.com/vpc-access-connector' SANDBOX_ANNOTATION = 'run.googleapis.com/execution-environment' CMEK_KEY_ANNOTATION = 'run.googleapis.com/encryption-key' POST_CMEK_KEY_REVOCATION_ACTION_TYPE_ANNOTATION = ( 'run.googleapis.com/post-key-revocation-action-type' ) ENCRYPTION_KEY_SHUTDOWN_HOURS_ANNOTATION = ( 'run.googleapis.com/encryption-key-shutdown-hours' ) SECRETS_ANNOTATION = 'run.googleapis.com/secrets' CPU_THROTTLE_ANNOTATION = 'run.googleapis.com/cpu-throttling' COLD_START_BOOST_ANNOTATION = 'run.googleapis.com/startup-cpu-boost' DISABLE_HEALTH_CHECK_ANNOTATION = 'run.googleapis.com/health-check-disabled' DISABLE_IAM_ANNOTATION = 'run.googleapis.com/invoker-iam-disabled' DISABLE_URL_ANNOTATION = 'run.googleapis.com/default-url-disabled' EGRESS_SETTINGS_ANNOTATION = 'run.googleapis.com/vpc-access-egress' EGRESS_SETTINGS_ALL = 'all' EGRESS_SETTINGS_ALL_TRAFFIC = 'all-traffic' EGRESS_SETTINGS_PRIVATE_RANGES_ONLY = 'private-ranges-only' class ContainerResource(k8s_object.KubernetesObject): """Wraps a resource message with a container, making fields more convenient. Provides convience fields for Cloud Run resources that contain a container. These resources also typically have other overlapping fields such as volumes which are also handled by this wrapper. """ @property def env_vars(self): """Returns a mutable, dict-like object to manage env vars. The returned object can be used like a dictionary, and any modifications to the returned object (i.e. setting and deleting keys) modify the underlying nested env vars fields. """ return self.container.env_vars @property def image(self): """URL to container.""" return self.container.image @image.setter def image(self, value): self.container.image = value @property def command(self): """command to be invoked by container.""" return self.container.command @property def container(self): """The container in the revisionTemplate.""" containers = self.containers.values() if not containers: return self.containers[''] if len(containers) == 1: return next(iter(containers)) if self.KIND == 'TaskTemplateSpec': raise ValueError( 'the target job has multiple containers, a container name must be' ' specified via --container flag' ) for container in containers: if container.ports: return container raise ValueError('missing ingress container') @property def containers(self): """The containers in the revisionTemplate.""" return ContainersAsDictionaryWrapper( self.spec.containers, self.volumes, self._messages ) @property def resource_limits(self): """The resource limits as a dictionary { resource name: limit}.""" return self.container.resource_limits @property def volumes(self): """Returns a dict-like object to manage volumes. There are additional properties on the object (e.g. `.secrets`) that can be used to access a mutable, dict-like object for managing volumes of a given type. Any modifications to the returned object for these properties (i.e. setting and deleting keys) modify the underlying nested volumes. """ return VolumesAsDictionaryWrapper(self.spec.volumes, self._messages.Volume) @property def dependencies(self) -> Mapping[str, Sequence[str]]: """Returns a dictionary of container dependencies. Container dependencies are stored in the 'run.googleapis.com/container-dependencies' annotation. The returned dictionary maps containers to a list of their dependencies by name. Note that updates to the returned dictionary do not update the resource's container dependencies unless the dependencies setter is used. """ dependencies = {} if k8s_object.CONTAINER_DEPENDENCIES_ANNOTATION in self.annotations: dependencies = json.loads( self.annotations[k8s_object.CONTAINER_DEPENDENCIES_ANNOTATION] ) return dependencies @dependencies.setter def dependencies(self, dependencies: Mapping[str, Sequence[str]]): """Sets the resource's container dependencies. Args: dependencies: A dictionary mapping containers to a list of their dependencies by name. Container dependencies are stored in the 'run.googleapis.com/container-dependencies' annotation as json. Setting an empty set of dependencies will clear this annotation. """ if dependencies: self.annotations[k8s_object.CONTAINER_DEPENDENCIES_ANNOTATION] = ( json.dumps({k: list(v) for k, v in dependencies.items()}) ) elif k8s_object.CONTAINER_DEPENDENCIES_ANNOTATION in self.annotations: del self.annotations[k8s_object.CONTAINER_DEPENDENCIES_ANNOTATION] class Container(object): """Wraps a container message with dict-like wrappers for env_vars, volume_mounts, and resource_limits. All other properties are delegated to the underlying container message. """ def __init__(self, volumes, messages_mod, container=None, **kwargs): if not container: container = messages_mod.Container(**kwargs) object.__setattr__(self, '_volumes', volumes) object.__setattr__(self, '_messages', messages_mod) object.__setattr__(self, '_m', container) @property def env_vars(self): """Returns a mutable, dict-like object to manage env vars. The returned object can be used like a dictionary, and any modifications to the returned object (i.e. setting and deleting keys) modify the underlying nested env vars fields. """ return EnvVarsAsDictionaryWrapper(self._m.env, self._messages.EnvVar) @property def volume_mounts(self): """Returns a mutable, dict-like object to manage volume mounts. The returned object can be used like a dictionary, and any modifications to the returned object (i.e. setting and deleting keys) modify the underlying nested volume mounts. There are additional properties on the object (e.g. `.secrets` that can be used to access a mutable dict-like object for a volume mounts that mount volumes of a given type. """ return VolumeMountsAsDictionaryWrapper( self._volumes, self._m.volumeMounts, self._messages.VolumeMount ) def _EnsureResources(self): limits_cls = self._messages.ResourceRequirements.LimitsValue if self.resources is not None: if self.resources.limits is None: self.resources.limits = k8s_object.InitializedInstance(limits_cls) else: self.resources = k8s_object.InitializedInstance( self._messages.ResourceRequirements ) @property def resource_limits(self): """The resource limits as a dictionary { resource name: limit}.""" self._EnsureResources() return k8s_object.KeyValueListAsDictionaryWrapper( self.resources.limits.additionalProperties, self._messages.ResourceRequirements.LimitsValue.AdditionalProperty, key_field='key', value_field='value', ) def MakeSerializable(self): return self._m def __getattr__(self, name): return getattr(self._m, name) def __setattr__(self, name, value): setattr(self._m, name, value) def MountedVolumeJoin(self, subgroup=None): vols = self._volumes mounts = self.volume_mounts if subgroup: vols = getattr(vols, subgroup) mounts = getattr(mounts, subgroup) return {path: vols.get(vol) for path, vol in mounts.items()} def NamedMountedVolumeJoin(self, subgroup=None): vols = self._volumes mounts = self.volume_mounts if subgroup: vols = getattr(vols, subgroup) mounts = getattr(mounts, subgroup) return {path: (vol, vols.get(vol)) for path, vol in mounts.items()} class ContainerSequenceWrapper(collections_abc.MutableSequence): """Wraps a list of containers wrapping each element with the Container wrapper class.""" def __init__(self, containers_to_wrap, volumes, messages_mod): super(ContainerSequenceWrapper, self).__init__() self._containers = containers_to_wrap self._volumes = volumes self._messages = messages_mod def __getitem__(self, index): return Container(self._volumes, self._messages, self._containers[index]) def __len__(self): return len(self._containers) def __setitem__(self, index, container): self._containers[index] = container.MakeSerializable() def __delitem__(self, index): del self._containers[index] def insert(self, index, value): self._containers.insert(index, value.MakeSerializable()) def MakeSerializable(self): return self._containers class ContainersAsDictionaryWrapper(k8s_object.ListAsDictionaryWrapper): """Wraps a list of containers in a mutable dict-like object mapping containers by name. Accessing a container name that does not exist will automatically add a new container with the specified name to the underlying list. """ def __init__(self, containers_to_wrap, volumes, messages_mod): """Wraps a list of containers in a mutable dict-like object. Args: containers_to_wrap: list[Container], list of containers to treat as a dict. volumes: the volumes defined in the containing resource used to classify volume mounts messages_mod: the messages module """ self._volumes = volumes self._messages = messages_mod super(ContainersAsDictionaryWrapper, self).__init__( ContainerSequenceWrapper(containers_to_wrap, volumes, messages_mod) ) def __getitem__(self, key): try: return super(ContainersAsDictionaryWrapper, self).__getitem__(key) except KeyError: container = Container(self._volumes, self._messages, name=key) self._m.append(container) return container def MakeSerializable(self): return ( super(ContainersAsDictionaryWrapper, self) .MakeSerializable() # ContainerSequenceWrapper .MakeSerializable() ) class EnvVarsAsDictionaryWrapper(k8s_object.ListAsDictionaryWrapper): """Wraps a list of env vars in a dict-like object. Additionally provides properties to access env vars of specific type in a mutable dict-like object. """ def __init__(self, env_vars_to_wrap, env_var_class): """Wraps a list of env vars in a dict-like object. Args: env_vars_to_wrap: list[EnvVar], list of env vars to treat as a dict. env_var_class: type of the underlying EnvVar objects. """ super(EnvVarsAsDictionaryWrapper, self).__init__(env_vars_to_wrap) self._env_vars = env_vars_to_wrap self._env_var_class = env_var_class @property def literals(self): """Mutable dict-like object for env vars with a string literal. Note that if neither value nor valueFrom is specified, the list entry will be treated as a literal empty string. Returns: A mutable, dict-like object for managing string literal env vars. """ return k8s_object.KeyValueListAsDictionaryWrapper( self._env_vars, self._env_var_class, filter_func=lambda env_var: env_var.valueFrom is None, ) @property def secrets(self): """Mutable dict-like object for vars with a secret source type.""" def _FilterSecretEnvVars(env_var): return ( env_var.valueFrom is not None and env_var.valueFrom.secretKeyRef is not None ) return k8s_object.KeyValueListAsDictionaryWrapper( self._env_vars, self._env_var_class, value_field='valueFrom', filter_func=_FilterSecretEnvVars, ) @property def config_maps(self): """Mutable dict-like object for vars with a config map source type.""" def _FilterConfigMapEnvVars(env_var): return ( env_var.valueFrom is not None and env_var.valueFrom.configMapKeyRef is not None ) return k8s_object.KeyValueListAsDictionaryWrapper( self._env_vars, self._env_var_class, value_field='valueFrom', filter_func=_FilterConfigMapEnvVars, ) class VolumesAsDictionaryWrapper(k8s_object.ListAsDictionaryWrapper): """Wraps a list of volumes in a dict-like object. Additionally provides properties to access volumes of specific type in a mutable dict-like object. """ def __init__(self, volumes_to_wrap, volume_class): """Wraps a list of volumes in a dict-like object. Args: volumes_to_wrap: list[Volume], list of volumes to treat as a dict. volume_class: type of the underlying Volume objects. """ super(VolumesAsDictionaryWrapper, self).__init__(volumes_to_wrap) self._volumes = volumes_to_wrap self._volume_class = volume_class @property def secrets(self): """Mutable dict-like object for volumes with a secret source type.""" return k8s_object.KeyValueListAsDictionaryWrapper( self._volumes, self._volume_class, value_field='secret', filter_func=lambda volume: volume.secret is not None, ) @property def config_maps(self): """Mutable dict-like object for volumes with a config map source type.""" return k8s_object.KeyValueListAsDictionaryWrapper( self._volumes, self._volume_class, value_field='configMap', filter_func=lambda volume: volume.configMap is not None, ) class VolumeMountsAsDictionaryWrapper( k8s_object.KeyValueListAsDictionaryWrapper ): """Wraps a list of volume mounts in a mutable dict-like object. Additionally provides properties to access mounts that are mounting volumes of specific type in a mutable dict-like object. """ def __init__(self, volumes, mounts_to_wrap, mount_class): """Wraps a list of volume mounts in a mutable dict-like object. Args: volumes: associated VolumesAsDictionaryWrapper obj mounts_to_wrap: list[VolumeMount], list of mounts to treat as a dict. mount_class: type of the underlying VolumeMount objects. """ super(VolumeMountsAsDictionaryWrapper, self).__init__( mounts_to_wrap, mount_class, key_field='mountPath', value_field='name', ) self._volumes = volumes @property def secrets(self): """Mutable dict-like object for mounts whose volumes have a secret source type.""" return k8s_object.KeyValueListAsDictionaryWrapper( self._m, self._item_class, key_field=self._key_field, value_field=self._value_field, filter_func=lambda mount: mount.name in self._volumes.secrets, ) @property def config_maps(self): """Mutable dict-like object for mounts whose volumes have a config map source type.""" return k8s_object.KeyValueListAsDictionaryWrapper( self._m, self._item_class, key_field=self._key_field, value_field=self._value_field, filter_func=lambda mount: mount.name in self._volumes.config_maps, )