# -*- coding: utf-8 -*- #
# Copyright 2016 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.
"""Functions for creating GCE container (Docker) deployments."""


import enum
import itertools
import re

from googlecloudsdk.api_lib.compute import exceptions
from googlecloudsdk.api_lib.compute import metadata_utils
from googlecloudsdk.api_lib.compute.operations import poller
from googlecloudsdk.api_lib.util import waiter
from googlecloudsdk.calliope import exceptions as calliope_exceptions
from googlecloudsdk.core import properties
from googlecloudsdk.core import yaml
from googlecloudsdk.core.universe_descriptor import universe_descriptor
from googlecloudsdk.core.util import files
from googlecloudsdk.core.util import times
import six


USER_INIT_TEMPLATE = """#cloud-config
runcmd:
- ['/usr/bin/kubelet',
   '--allow-privileged=%s',
   '--manifest-url=http://metadata.google.internal/computeMetadata/v1/instance/attributes/google-container-manifest',
   '--manifest-url-header=Metadata-Flavor:Google',
   '--config=/etc/kubernetes/manifests']
"""

MANIFEST_DISCLAIMER = """# DISCLAIMER:
# The option to deploy a container during VM creation using the container
# startup agent is deprecated. Use alternative services to run containers on
# your VMs. Learn more at https://cloud.google.com/compute/docs/containers/migrate-containers.

"""

CONTAINER_MANIFEST_KEY = 'google-container-manifest'

GCE_CONTAINER_DECLARATION = 'gce-container-declaration'

STACKDRIVER_LOGGING_AGENT_CONFIGURATION = 'google-logging-enabled'

GKE_DOCKER = 'gci-ensure-gke-docker'

ALLOWED_PROTOCOLS = ['TCP', 'UDP']

# Prefix of all COS image major release names
COS_MAJOR_RELEASE_PREFIX = 'cos-stable-'

# Pin this version of gcloud to COS image major release version
COS_MAJOR_RELEASE = COS_MAJOR_RELEASE_PREFIX + '55'

COS_PROJECT = 'cos-cloud'

_MIN_PREFERRED_COS_VERSION = 63

# Translation from CLI to API wording
RESTART_POLICY_API = {
    'never': 'Never',
    'on-failure': 'OnFailure',
    'always': 'Always',
}


class MountVolumeMode(enum.Enum):
  READ_ONLY = (1,)
  READ_WRITE = (2,)

  def isReadOnly(self):
    return self == MountVolumeMode.READ_ONLY


_DEFAULT_MODE = MountVolumeMode.READ_WRITE


def _GetUserInit(allow_privileged):
  """Gets user-init metadata value for COS image."""
  allow_privileged_val = 'true' if allow_privileged else 'false'
  return USER_INIT_TEMPLATE % (allow_privileged_val)


class Error(exceptions.Error):
  """Base exception for containers."""


class InvalidMetadataKeyException(Error):
  """InvalidMetadataKeyException is for not allowed metadata keys."""

  def __init__(self, metadata_key):
    super(InvalidMetadataKeyException, self).__init__(
        'Metadata key "{0}" is not allowed when running containerized VM.'
        .format(metadata_key)
    )


class NoGceContainerDeclarationMetadataKey(Error):
  """Raised on attempt to update-container on instance without containers."""

  def __init__(self):
    super(NoGceContainerDeclarationMetadataKey, self).__init__(
        "Instance doesn't have {} metadata key - it is not a container.".format(
            GCE_CONTAINER_DECLARATION
        )
    )


def ValidateUserMetadata(metadata):
  """Validates if user-specified metadata.

  Checks if it contains values which may conflict with container deployment.
  Args:
    metadata: user-specified VM metadata.

  Raises:
    InvalidMetadataKeyException: if there is conflict with user-provided
    metadata
  """
  for entry in metadata.items:
    if entry.key in [CONTAINER_MANIFEST_KEY, GKE_DOCKER]:
      raise InvalidMetadataKeyException(entry.key)


def CreateTagsMessage(messages, tags):
  """Create tags message with parameters for container VM or VM templates."""
  if tags:
    return messages.Tags(items=tags)


def GetLabelsMessageWithCosVersion(
    labels, image_uri, resources, resource_class
):
  """Returns message with labels for instance / instance template.

  Args:
    labels: dict, labels to assign to the resource.
    image_uri: URI of image used as a base for the resource. The function
      extracts COS version from the URI and uses it as a value of `container-vm`
      label.
    resources: object that can parse image_uri.
    resource_class: class of the resource to which labels will be assigned. Must
      contain LabelsValue class and resource_class.LabelsValue must contain
      AdditionalProperty class.
  """
  cos_version = (
      resources.Parse(image_uri, collection='compute.images')
      .Name()
      .replace('/', '-')
  )
  if labels is None:
    labels = {}
  labels['container-vm'] = cos_version
  additional_properties = [
      resource_class.LabelsValue.AdditionalProperty(key=k, value=v)
      for k, v in sorted(six.iteritems(labels))
  ]
  return resource_class.LabelsValue(additionalProperties=additional_properties)


class NoCosImageException(Error):
  """Raised when COS image could not be found."""

  def __init__(self):
    super(NoCosImageException, self).__init__(
        "Could not find COS (Cloud OS) for release family '{0}'".format(
            COS_MAJOR_RELEASE
        )
    )


def _AddUniversePrefix(project_name):
  if properties.IsDefaultUniverse():
    return project_name
  else:
    prefix = (
        universe_descriptor.UniverseDescriptor()
        .Get(properties.GetUniverseDomain())
        .project_prefix
    )
    return prefix + ':' + project_name


def ExpandCosImageFlag(compute_client):
  """Select a COS image to run Docker."""
  compute = compute_client.apitools_client
  images = compute_client.MakeRequests([(
      compute.images,
      'List',
      compute_client.messages.ComputeImagesListRequest(
          project=_AddUniversePrefix(COS_PROJECT)
      ),
  )])
  return _SelectNewestCosImage(images)


def _SelectNewestCosImage(images):
  """Selects newest COS image from the list."""
  cos_images = sorted(
      [image for image in images if image.name.startswith(COS_MAJOR_RELEASE)],
      key=lambda x: times.ParseDateTime(x.creationTimestamp),
  )
  if not cos_images:
    raise NoCosImageException()
  return cos_images[-1].selfLink


def _ValidateAndParsePortMapping(port_mappings):
  """Parses and validates port mapping."""
  ports_config = []
  for port_mapping in port_mappings:
    mapping_match = re.match(r'^(\d+):(\d+):(\S+)$', port_mapping)
    if not mapping_match:
      raise calliope_exceptions.InvalidArgumentException(
          '--port-mappings',
          'Port mappings should follow PORT:TARGET_PORT:PROTOCOL format.',
      )
    port, target_port, protocol = mapping_match.groups()
    if protocol not in ALLOWED_PROTOCOLS:
      raise calliope_exceptions.InvalidArgumentException(
          '--port-mappings',
          'Protocol should be one of [{0}]'.format(
              ', '.join(ALLOWED_PROTOCOLS)
          ),
      )
    ports_config.append({
        'containerPort': int(target_port),
        'hostPort': int(port),
        'protocol': protocol,
    })
  return ports_config


def ExpandKonletCosImageFlag(compute_client):
  """Select a COS image to run Konlet.

  This function scans three families in order:
  - stable
  - beta
  - dev
  looking for the first image with version at least _MIN_PREFERRED_COS_VERSION.

  Args:
    compute_client: ClientAdapter, The Compute API client adapter

  Returns:
    COS image at version _MIN_PREFERRED_COS_VERSION or later.

  Raises:
    NoCosImageException: No COS image at version at least
    _MIN_PREFERRED_COS_VERSION was found. This should not happen if backend is
    healthy.
  """
  compute = compute_client.apitools_client
  images = compute_client.MakeRequests([(
      compute.images,
      'List',
      compute_client.messages.ComputeImagesListRequest(
          project=_AddUniversePrefix(COS_PROJECT)
      ),
  )])
  name_re_template = r'cos-{}-(\d+)-.*'
  image_families = ['stable', 'beta', 'dev']

  for family in image_families:
    name_re = name_re_template.format(family)

    def MakeCreateComparisonKey(name_re):
      def CreateComparisonKey(image):
        version = int(re.match(name_re, image.name).group(1))
        timestamp = times.ParseDateTime(image.creationTimestamp)
        return version, timestamp

      return CreateComparisonKey

    cos_images = sorted(
        [image for image in images if re.match(name_re, image.name)],
        key=MakeCreateComparisonKey(name_re),
    )
    if (
        cos_images
        and MakeCreateComparisonKey(name_re)(cos_images[-1])[0]
        >= _MIN_PREFERRED_COS_VERSION
    ):
      return cos_images[-1].selfLink

  raise NoCosImageException()


def _ReadDictionary(filename):
  # pylint:disable=line-too-long
  r"""Read environment variable from file.

  File format:

  It is intended (but not guaranteed) to follow standard docker format
  [](https://docs.docker.com/engine/reference/commandline/run/#set-environment-variables--e---env---env-file)
  but without capturing environment variables from host machine.
  Lines starting by "#" character are comments.
  Empty lines are ignored.
  Below grammar production follow in EBNF format.

  file = (whitespace* statement '\n')*
  statement = comment
            | definition
  whitespace = ' '
             | '\t'
  comment = '#' [^\n]*
  definition = [^#=\n] [^= \t\n]* '=' [^\n]*

  Args:
    filename: str, name of the file to read

  Returns:
    A dictionary mapping environment variable names to their values.
  """
  env_vars = {}
  if not filename:
    return env_vars
  with files.FileReader(filename) as f:
    for i, line in enumerate(f):
      # Strip whitespace at the beginning and end of line
      line = line.strip()
      # Ignore comments and empty lines
      if len(line) <= 1 or line[0] == '#':
        continue
      # Find first left '=' character
      assignment_op_loc = line.find('=')
      if assignment_op_loc == -1:
        raise calliope_exceptions.BadFileException(
            'Syntax error in {}:{}: Expected VAR=VAL, got {}'.format(
                filename, i, line
            )
        )
      env = line[:assignment_op_loc]
      val = line[assignment_op_loc + 1 :]
      if ' ' in env or '\t' in env:
        raise calliope_exceptions.BadFileException(
            'Syntax error in {}:{} Variable name cannot contain whitespaces,'
            ' got "{}"'.format(filename, i, env)
        )
      env_vars[env] = val
  return env_vars


def _GetHostPathDiskName(idx):
  return 'host-path-{}'.format(idx)


def _GetTmpfsDiskName(idx):
  return 'tmpfs-{}'.format(idx)


def _GetPersistentDiskName(idx):
  return 'pd-{}'.format(idx)


def _AddMountedDisksToManifest(
    container_mount_disk, volumes, volume_mounts, used_names=None, disks=None
):
  """Add volume specs from --container-mount-disk."""
  used_names = used_names or []
  disks = disks or []
  idx = 0
  for mount_disk in container_mount_disk:
    while _GetPersistentDiskName(idx) in used_names:
      idx += 1

    device_name = mount_disk.get('name')
    partition = mount_disk.get('partition')

    def _GetMatchingVolume(device_name, partition):
      for volume_spec in volumes:
        pd = volume_spec.get('gcePersistentDisk', {})
        if pd.get('pdName') == device_name and pd.get('partition') == partition:
          return volume_spec

    repeated = _GetMatchingVolume(device_name, partition)
    if repeated:
      name = repeated['name']
    else:
      name = _GetPersistentDiskName(idx)
      used_names.append(name)

    if not device_name:
      # This should not be needed - any command that accepts container mount
      # disks should validate that there is only one disk before calling this
      # function.
      if len(disks) != 1:
        raise calliope_exceptions.InvalidArgumentException(
            '--container-mount-disk',
            'Must specify the name of the disk to be mounted unless exactly '
            'one disk is attached to the instance.',
        )
      device_name = disks[0].get('name')
      if disks[0].get('device-name', device_name) != device_name:
        raise exceptions.InvalidArgumentException(
            '--container-mount-disk',
            'Must not have a device-name that is different from disk name if '
            'disk is being attached to the instance and mounted to a container:'
            ' [{}]'.format(disks[0].get('device-name')),
        )

    volume_mounts.append({
        'name': name,
        'mountPath': mount_disk['mount-path'],
        'readOnly': mount_disk.get('mode', _DEFAULT_MODE).isReadOnly(),
    })

    if repeated:
      continue
    volume_spec = {
        'name': name,
        'gcePersistentDisk': {'pdName': device_name, 'fsType': 'ext4'},
    }
    if partition:
      volume_spec['gcePersistentDisk'].update({'partition': partition})
    volumes.append(volume_spec)
    idx += 1


def _CreateContainerManifest(
    args,
    instance_name,
    container_mount_disk_enabled=False,
    container_mount_disk=None,
):
  """Create container manifest from argument namespace and instance name."""
  container = {'image': args.container_image, 'name': instance_name}

  if args.container_command is not None:
    container['command'] = [args.container_command]

  if args.container_arg is not None:
    container['args'] = args.container_arg

  container['stdin'] = args.container_stdin
  container['tty'] = args.container_tty
  container['securityContext'] = {'privileged': args.container_privileged}

  env_vars = _ReadDictionary(args.container_env_file)
  for env_var_dict in args.container_env or []:
    for env, val in six.iteritems(env_var_dict):
      env_vars[env] = val
  if env_vars:
    container['env'] = [
        {'name': env, 'value': val} for env, val in six.iteritems(env_vars)
    ]

  volumes = []
  volume_mounts = []

  for idx, volume in enumerate(args.container_mount_host_path or []):
    volumes.append({
        'name': _GetHostPathDiskName(idx),
        'hostPath': {'path': volume['host-path']},
    })
    volume_mounts.append({
        'name': _GetHostPathDiskName(idx),
        'mountPath': volume['mount-path'],
        'readOnly': volume.get('mode', _DEFAULT_MODE).isReadOnly(),
    })

  for idx, tmpfs in enumerate(args.container_mount_tmpfs or []):
    volumes.append(
        {'name': _GetTmpfsDiskName(idx), 'emptyDir': {'medium': 'Memory'}}
    )
    volume_mounts.append(
        {'name': _GetTmpfsDiskName(idx), 'mountPath': tmpfs['mount-path']}
    )

  if container_mount_disk_enabled:
    container_mount_disk = container_mount_disk or []
    disks = (args.disk or []) + (args.create_disk or [])
    _AddMountedDisksToManifest(
        container_mount_disk, volumes, volume_mounts, disks=disks
    )

  container['volumeMounts'] = volume_mounts

  manifest = {
      'spec': {
          'containers': [container],
          'volumes': volumes,
          'restartPolicy': RESTART_POLICY_API[args.container_restart_policy],
      }
  }

  return manifest


def DumpYaml(data):
  """Dumps data dict to YAML in format expected by Konlet."""
  return MANIFEST_DISCLAIMER + yaml.dump(data)


def _CreateYamlContainerManifest(
    args,
    instance_name,
    container_mount_disk_enabled=False,
    container_mount_disk=None,
):
  """Helper to create the container manifest."""
  return DumpYaml(
      _CreateContainerManifest(
          args,
          instance_name,
          container_mount_disk_enabled=container_mount_disk_enabled,
          container_mount_disk=container_mount_disk,
      )
  )


def CreateKonletMetadataMessage(
    messages,
    args,
    instance_name,
    user_metadata,
    container_mount_disk_enabled=False,
    container_mount_disk=None,
):
  """Helper to create the metadata for konlet."""
  konlet_metadata = {
      GCE_CONTAINER_DECLARATION: _CreateYamlContainerManifest(
          args,
          instance_name,
          container_mount_disk_enabled=container_mount_disk_enabled,
          container_mount_disk=container_mount_disk,
      ),
      # Since COS 69, having logs for Container-VMs written requires enabling
      # Cloud Logging agent.
      STACKDRIVER_LOGGING_AGENT_CONFIGURATION: 'true',
  }
  return metadata_utils.ConstructMetadataMessage(
      messages, metadata=konlet_metadata, existing_metadata=user_metadata
  )


def UpdateInstance(
    holder,
    client,
    instance_ref,
    instance,
    args,
    container_mount_disk_enabled=False,
    container_mount_disk=None,
):
  """Update an instance and its container metadata."""
  operation_poller = poller.Poller(client.apitools_client.instances)

  result = _UpdateShieldedInstanceConfig(
      holder, client, operation_poller, instance_ref, args
  )

  result = (
      _SetShieldedInstanceIntegrityPolicy(
          holder, client, operation_poller, instance_ref, args
      )
      or result
  )

  # find gce-container-declaration metadata entry
  for metadata in instance.metadata.items:
    if metadata.key == GCE_CONTAINER_DECLARATION:
      UpdateMetadata(
          holder,
          metadata,
          args,
          instance,
          container_mount_disk_enabled=container_mount_disk_enabled,
          container_mount_disk=container_mount_disk,
      )

      # update Google Compute Engine resource
      operation = client.apitools_client.instances.SetMetadata(
          client.messages.ComputeInstancesSetMetadataRequest(
              metadata=instance.metadata, **instance_ref.AsDict()
          )
      )

      operation_ref = holder.resources.Parse(
          operation.selfLink, collection='compute.zoneOperations'
      )

      set_metadata_waiter = waiter.WaitFor(
          operation_poller,
          operation_ref,
          'Updating specification of container [{0}]'.format(
              instance_ref.Name()
          ),
      )

      if (
          instance.status
          == client.messages.Instance.StatusValueValuesEnum.TERMINATED
      ):
        return set_metadata_waiter or result
      elif (
          instance.status
          == client.messages.Instance.StatusValueValuesEnum.SUSPENDED
      ):
        return _StopVm(holder, client, instance_ref) or result
      else:
        _StopVm(holder, client, instance_ref)
        return _StartVm(holder, client, instance_ref) or result

  raise NoGceContainerDeclarationMetadataKey()


def _UpdateShieldedInstanceConfig(
    holder, client, operation_poller, instance_ref, args
):
  """Update the Shielded Instance Config."""
  if (
      args.shielded_vm_secure_boot is None
      and args.shielded_vm_vtpm is None
      and args.shielded_vm_integrity_monitoring is None
  ):
    return None
  shielded_config_msg = client.messages.ShieldedInstanceConfig(
      enableSecureBoot=args.shielded_vm_secure_boot,
      enableVtpm=args.shielded_vm_vtpm,
      enableIntegrityMonitoring=args.shielded_vm_integrity_monitoring,
  )
  request = client.messages.ComputeInstancesUpdateShieldedInstanceConfigRequest(
      instance=instance_ref.Name(),
      project=instance_ref.project,
      shieldedInstanceConfig=shielded_config_msg,
      zone=instance_ref.zone,
  )

  operation = client.apitools_client.instances.UpdateShieldedInstanceConfig(
      request
  )
  operation_ref = holder.resources.Parse(
      operation.selfLink, collection='compute.zoneOperations'
  )
  return waiter.WaitFor(
      operation_poller,
      operation_ref,
      'Setting shieldedInstanceConfig of instance [{0}]'.format(
          instance_ref.Name()
      ),
  )


def _SetShieldedInstanceIntegrityPolicy(
    holder, client, operation_poller, instance_ref, args
):
  """Set the Shielded Instance Integrity Policy."""
  shielded_integrity_policy_msg = (
      client.messages.ShieldedInstanceIntegrityPolicy(
          updateAutoLearnPolicy=True
      )
  )

  if not args.IsSpecified('shielded_vm_learn_integrity_policy'):
    return None
  request = (
      client.messages.ComputeInstancesSetShieldedInstanceIntegrityPolicyRequest(
          instance=instance_ref.Name(),
          project=instance_ref.project,
          shieldedInstanceIntegrityPolicy=shielded_integrity_policy_msg,
          zone=instance_ref.zone,
      )
  )

  operation = (
      client.apitools_client.instances.SetShieldedInstanceIntegrityPolicy(
          request
      )
  )
  operation_ref = holder.resources.Parse(
      operation.selfLink, collection='compute.zoneOperations'
  )

  return waiter.WaitFor(
      operation_poller,
      operation_ref,
      'Setting shieldedInstanceIntegrityPolicy of instance [{0}]'.format(
          instance_ref.Name()
      ),
  )


def _StopVm(holder, client, instance_ref):
  """Stop the Virtual Machine."""
  operation = client.apitools_client.instances.Stop(
      client.messages.ComputeInstancesStopRequest(**instance_ref.AsDict())
  )

  operation_ref = holder.resources.Parse(
      operation.selfLink, collection='compute.zoneOperations'
  )

  operation_poller = poller.Poller(client.apitools_client.instances)
  return waiter.WaitFor(
      operation_poller,
      operation_ref,
      'Stopping instance [{0}]'.format(instance_ref.Name()),
  )


def _StartVm(holder, client, instance_ref):
  """Start the Virtual Machine."""
  operation = client.apitools_client.instances.Start(
      client.messages.ComputeInstancesStartRequest(**instance_ref.AsDict())
  )

  operation_ref = holder.resources.Parse(
      operation.selfLink, collection='compute.zoneOperations'
  )

  operation_poller = poller.Poller(client.apitools_client.instances)
  return waiter.WaitFor(
      operation_poller,
      operation_ref,
      'Starting instance [{0}]'.format(instance_ref.Name()),
  )


def UpdateMetadata(
    holder,
    metadata,
    args,
    instance,
    container_mount_disk_enabled=False,
    container_mount_disk=None,
):
  """Update konlet metadata entry using user-supplied data."""
  # precondition: metadata.key == GCE_CONTAINER_DECLARATION

  manifest = yaml.load(metadata.value)

  if args.IsSpecified('container_image'):
    manifest['spec']['containers'][0]['image'] = args.container_image

  if args.IsSpecified('container_command'):
    manifest['spec']['containers'][0]['command'] = [args.container_command]

  if args.IsSpecified('clear_container_command'):
    manifest['spec']['containers'][0].pop('command', None)

  if args.IsSpecified('container_arg'):
    manifest['spec']['containers'][0]['args'] = args.container_arg

  if args.IsSpecified('clear_container_args'):
    manifest['spec']['containers'][0].pop('args', None)

  if args.container_privileged is True:
    manifest['spec']['containers'][0]['securityContext']['privileged'] = True

  if args.container_privileged is False:
    manifest['spec']['containers'][0]['securityContext']['privileged'] = False

  if container_mount_disk_enabled:
    container_mount_disk = container_mount_disk or []
    disks = instance.disks
  else:
    container_mount_disk = []
    # Only need disks for updating the container mount disk.
    disks = []
  _UpdateMounts(
      holder,
      manifest,
      args.remove_container_mounts or [],
      args.container_mount_host_path or [],
      args.container_mount_tmpfs or [],
      container_mount_disk,
      disks,
  )

  _UpdateEnv(
      manifest,
      itertools.chain.from_iterable(args.remove_container_env or []),
      args.container_env_file,
      args.container_env or [],
  )

  if args.container_stdin is True:
    manifest['spec']['containers'][0]['stdin'] = True

  if args.container_stdin is False:
    manifest['spec']['containers'][0]['stdin'] = False

  if args.container_tty is True:
    manifest['spec']['containers'][0]['tty'] = True

  if args.container_tty is False:
    manifest['spec']['containers'][0]['tty'] = False

  if args.IsSpecified('container_restart_policy'):
    manifest['spec']['restartPolicy'] = RESTART_POLICY_API[
        args.container_restart_policy
    ]

  metadata.value = DumpYaml(manifest)


def _UpdateMounts(
    holder,
    manifest,
    remove_container_mounts,
    container_mount_host_path,
    container_mount_tmpfs,
    container_mount_disk,
    disks,
):
  """Updates mounts in container manifest."""

  _CleanupMounts(
      manifest,
      remove_container_mounts,
      container_mount_host_path,
      container_mount_tmpfs,
      container_mount_disk=container_mount_disk,
  )

  used_names = [volume['name'] for volume in manifest['spec']['volumes']]
  volumes = []
  volume_mounts = []
  next_volume_index = 0
  for volume in container_mount_host_path:
    while _GetHostPathDiskName(next_volume_index) in used_names:
      next_volume_index += 1
    name = _GetHostPathDiskName(next_volume_index)
    next_volume_index += 1
    volumes.append({
        'name': name,
        'hostPath': {'path': volume['host-path']},
    })
    volume_mounts.append({
        'name': name,
        'mountPath': volume['mount-path'],
        'readOnly': volume.get('mode', _DEFAULT_MODE).isReadOnly(),
    })
  for tmpfs in container_mount_tmpfs:
    while _GetTmpfsDiskName(next_volume_index) in used_names:
      next_volume_index += 1
    name = _GetTmpfsDiskName(next_volume_index)
    next_volume_index += 1
    volumes.append({'name': name, 'emptyDir': {'medium': 'Memory'}})
    volume_mounts.append({'name': name, 'mountPath': tmpfs['mount-path']})

  if container_mount_disk:
    # Convert to dict to match helper input needs.
    # The disk must already have a device name that matches its
    # name. For disks that were attached to the instance already.
    disks = [
        {
            'device-name': disk.deviceName,
            'name': holder.resources.Parse(disk.source).Name(),
        }
        for disk in disks
    ]
    _AddMountedDisksToManifest(
        container_mount_disk,
        volumes,
        volume_mounts,
        used_names=used_names,
        disks=disks,
    )

  manifest['spec']['containers'][0]['volumeMounts'].extend(volume_mounts)
  manifest['spec']['volumes'].extend(volumes)


def _CleanupMounts(
    manifest,
    remove_container_mounts,
    container_mount_host_path,
    container_mount_tmpfs,
    container_mount_disk=None,
):
  """Remove all specified mounts from container manifest."""
  container_mount_disk = container_mount_disk or []

  # volumeMounts stored in this list should be removed
  mount_paths_to_remove = remove_container_mounts[:]
  for host_path in container_mount_host_path:
    mount_paths_to_remove.append(host_path['mount-path'])
  for tmpfs in container_mount_tmpfs:
    mount_paths_to_remove.append(tmpfs['mount-path'])
  for disk in container_mount_disk:
    mount_paths_to_remove.append(disk['mount-path'])

  # volumeMounts stored in this list are used
  used_mounts = []
  used_mounts_names = []
  removed_mount_names = []
  for mount in manifest['spec']['containers'][0].get('volumeMounts', []):
    if mount['mountPath'] not in mount_paths_to_remove:
      used_mounts.append(mount)
      used_mounts_names.append(mount['name'])
    else:
      removed_mount_names.append(mount['name'])

  # override volumeMounts
  manifest['spec']['containers'][0]['volumeMounts'] = used_mounts
  # garbage collect volumes which become orphaned, skip volumes orphaned before
  # start of the procedure
  used_volumes = []
  for volume in manifest['spec'].get('volumes', []):
    if (
        volume['name'] in used_mounts_names
        or volume['name'] not in removed_mount_names
    ):
      used_volumes.append(volume)

  # override volumes
  manifest['spec']['volumes'] = used_volumes


def _UpdateEnv(
    manifest, remove_container_env, container_env_file, container_env
):
  """Update environment variables in container manifest."""

  current_env = {}
  for env_val in manifest['spec']['containers'][0].get('env', []):
    current_env[env_val['name']] = env_val.get('value')

  for env in remove_container_env:
    current_env.pop(env, None)

  current_env.update(_ReadDictionary(container_env_file))

  for env_var_dict in container_env:
    for env, val in six.iteritems(env_var_dict):
      current_env[env] = val
  if current_env:
    manifest['spec']['containers'][0]['env'] = [
        {'name': env, 'value': val} for env, val in six.iteritems(current_env)
    ]
