text: # -*- coding: utf-8 -*- #
  # Copyright 2014 Google Inc. 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.
  
  """Module to parse .yaml files for an appengine app."""
  
  from __future__ import absolute_import
  from __future__ import division
  from __future__ import unicode_literals
  
  import os
  
  from googlecloudsdk.api_lib.app import env
  from googlecloudsdk.core import exceptions
  from googlecloudsdk.core import log
  from googlecloudsdk.core.util import files
  from googlecloudsdk.third_party.appengine.api import appinfo
  from googlecloudsdk.third_party.appengine.api import appinfo_errors
  from googlecloudsdk.third_party.appengine.api import appinfo_includes
  from googlecloudsdk.third_party.appengine.api import croninfo
  from googlecloudsdk.third_party.appengine.api import dispatchinfo
  from googlecloudsdk.third_party.appengine.api import dosinfo
  from googlecloudsdk.third_party.appengine.api import queueinfo
  from googlecloudsdk.third_party.appengine.api import validation
  from googlecloudsdk.third_party.appengine.api import yaml_errors
  from googlecloudsdk.third_party.appengine.datastore import datastore_index
  
  
  HINT_PROJECT = ('This field is not used by gcloud and must be removed. '
                  'Project name should instead be specified either by '
                  '`gcloud config set project MY_PROJECT` or by setting the '
                  '`--project` flag on individual command executions.')
  
  HINT_VERSION = ('This field is not used by gcloud and must be removed. '
                  'Versions are generated automatically by default but can also '
                  'be manually specified by setting the `--version` flag on '
                  'individual command executions.')
  
  HINT_THREADSAFE = ('This field is not supported with runtime [{}] and can '
                     'safely be removed.')
  
  HINT_READABLE = ('This field is not configurable with runtime [{}] since '
                   'static files are always readable by the application. It '
                   'can safely be removed.')
  
  MANAGED_VMS_DEPRECATION_WARNING = """\
  Deployments using `vm: true` have been deprecated.  Please update your \
  app.yaml to use `env: flex`. To learn more, please visit \
  https://cloud.google.com/appengine/docs/flexible/migration.
  """
  
  UPGRADE_FLEX_PYTHON_URL = (
      'https://cloud.google.com/appengine/docs/flexible/python/migrating')
  
  APP_ENGINE_APIS_DEPRECATION_WARNING = (
      'Support for the compat runtimes and their base images '
      '(enable_app_engine_apis: true) has been deprecated.  Please migrate to a '
      'new base image, or use a Google managed runtime. '
      'To learn more, visit {}.').format(UPGRADE_FLEX_PYTHON_URL)
  
  PYTHON_SSL_WARNING = (
      'You are using an outdated version [2.7] of the Python SSL library. '
      'Please update your app.yaml file to specify SSL library [latest] to '
      'avoid security risks. On April 2, 2018, version 2.7 will be '
      'decommissioned and your app will be blocked from deploying until you '
      'you specify SSL library [latest] or [2.7.11].'
      'For more information, visit {}.'
  ).format('https://cloud.google.com/appengine/docs/deprecations/python-ssl-27')
  
  FLEX_PY34_WARNING = (
      'You are using a deprecated version [3.4] of Python on the App '
      'Engine Flexible environment. Please update your app.yaml file to specify '
      '[python_version: latest]. Python 3.4 will be decommissioned on March 29, '
      '2019. After this date, new deployments will fail. For more information '
      'about this deprecation, visit  {}.'
  ).format('https://cloud.google.com/appengine/docs/deprecations/python34')
  
  # This is the equivalent of the following in app.yaml:
  # skip_files:
  # - ^(.*/)?#.*#$
  # - ^(.*/)?.*~$
  # - ^(.*/)?.*\.py[co]$
  # - ^(.*/)?.*/RCS/.*$
  # - ^(.*/)?.git(ignore|/.*)$
  # - ^(.*/)?node_modules/.*
  DEFAULT_SKIP_FILES_FLEX = (r'^(.*/)?#.*#$|'
                             r'^(.*/)?.*~$|'
                             r'^(.*/)?.*\.py[co]$|'
                             r'^(.*/)?.*/RCS/.*$|'
                             r'^(.*/)?.git(ignore|/.*)$|'
                             r'^(.*/)?node_modules/.*$')
  
  
  class Error(exceptions.Error):
    """A base error for this module."""
    pass
  
  
  class YamlParseError(Error):
    """An exception for when a specific yaml file is not well formed."""
  
    def __init__(self, file_path, e):
      """Creates a new Error.
  
      Args:
        file_path: str, The full path of the file that failed to parse.
        e: Exception, The exception that was originally raised.
      """
      super(YamlParseError, self).__init__(
          'An error occurred while parsing file: [{file_path}]\n{err}'
          .format(file_path=file_path, err=e))
  
  
  class YamlValidationError(Error):
    """An exception for when a specific yaml file has invalid info."""
    pass
  
  
  class AppConfigError(Error):
    """Errors in Application Config."""
  
  
  class _YamlInfo(object):
    """A base class for holding some basic attributes of a parsed .yaml file."""
  
    def __init__(self, file_path, parsed):
      """Creates a new _YamlInfo.
  
      Args:
        file_path: str, The full path the file that was parsed.
        parsed: The parsed yaml data as one of the *_info objects.
      """
      self.file = file_path
      self.parsed = parsed
  
    @staticmethod
    def _ParseYaml(file_path, parser):
      """Parses the given file using the given parser.
  
      Args:
        file_path: str, The full path of the file to parse.
        parser: str, The parser to use to parse this yaml file.
  
      Returns:
        The result of the parse.
      """
      with files.FileReader(file_path) as fp:
        return parser(fp)
  
  
  class ConfigYamlInfo(_YamlInfo):
    """A class for holding some basic attributes of a parsed config .yaml file."""
  
    CRON = 'cron'
    DISPATCH = 'dispatch'
    DOS = 'dos'
    INDEX = 'index'
    QUEUE = 'queue'
  
    CONFIG_YAML_PARSERS = {
        CRON: croninfo.LoadSingleCron,
        DISPATCH: dispatchinfo.LoadSingleDispatch,
        DOS: dosinfo.LoadSingleDos,
        INDEX: datastore_index.ParseIndexDefinitions,
        QUEUE: queueinfo.LoadSingleQueue,
    }
  
    def __init__(self, file_path, config, parsed):
      """Creates a new ConfigYamlInfo.
  
      Args:
        file_path: str, The full path the file that was parsed.
        config: str, The name of the config that was parsed (i.e. 'cron')
        parsed: The parsed yaml data as one of the *_info objects.
      """
      super(ConfigYamlInfo, self).__init__(file_path, parsed)
      self.config = config
  
    @property
    def name(self):
      """Name of the config file without extension, e.g. `cron`."""
      (base, _) = os.path.splitext(os.path.basename(self.file))
      return base
  
    @staticmethod
    def FromFile(file_path):
      """Parses the given config file.
  
      Args:
        file_path: str, The full path to the config file.
  
      Raises:
        YamlParseError: If the file is not valid.
  
      Returns:
        A ConfigYamlInfo object for the parsed file.
      """
      base, ext = os.path.splitext(os.path.basename(file_path))
      parser = (ConfigYamlInfo.CONFIG_YAML_PARSERS.get(base)
                if os.path.isfile(file_path) and ext.lower() in ['.yaml', '.yml']
                else None)
      if not parser:
        return None
      try:
        parsed = _YamlInfo._ParseYaml(file_path, parser)
        if not parsed:
          raise YamlParseError(file_path, 'The file is empty')
      except (yaml_errors.Error, validation.Error) as e:
        raise YamlParseError(file_path, e)
  
      _CheckIllegalAttribute(
          name='application',
          yaml_info=parsed,
          extractor_func=lambda yaml: yaml.application,
          file_path=file_path,
          msg=HINT_PROJECT)
  
      if base == 'dispatch':
        return DispatchConfigYamlInfo(file_path, config=base, parsed=parsed)
      return ConfigYamlInfo(file_path, config=base, parsed=parsed)
  
  
  class DispatchConfigYamlInfo(ConfigYamlInfo):
    """Provides methods for getting 1p-ready representation."""
  
    def _HandlerToDict(self, handler):
      parsed_url = dispatchinfo.ParsedURL(handler.url)
      dispatch_domain = parsed_url.host
      if not parsed_url.host_exact:
        dispatch_domain = '*' + dispatch_domain
  
      dispatch_path = parsed_url.path
      if not parsed_url.path_exact:
        dispatch_path = dispatch_path.rstrip('/') + '/*'
      return {
          'domain': dispatch_domain,
          'path': dispatch_path,
          'service': handler.service,
      }
  
    def GetRules(self):
      """Get dispatch rules on a format suitable for Admin API.
  
      Returns:
        [{'service': str, 'domain': str, 'path': str}], rules.
      """
      return [self._HandlerToDict(h) for h in self.parsed.dispatch or []]
  
  
  class ServiceYamlInfo(_YamlInfo):
    """A class for holding some basic attributes of a parsed service yaml file."""
    DEFAULT_SERVICE_NAME = 'default'
  
    def __init__(self, file_path, parsed):
      """Creates a new ServiceYamlInfo.
  
      Args:
        file_path: str, The full path the file that was parsed.
        parsed: appinfo.AppInfoExternal, parsed Application Configuration.
      """
      super(ServiceYamlInfo, self).__init__(file_path, parsed)
      self.module = parsed.service or ServiceYamlInfo.DEFAULT_SERVICE_NAME
  
      if parsed.env in ['2', 'flex', 'flexible']:
        self.env = env.FLEX
      elif parsed.vm or parsed.runtime == 'vm':
        self.env = env.MANAGED_VMS
      else:
        self.env = env.STANDARD
  
      # All `env: flex` apps are hermetic. All `env: standard` apps are not
      # hermetic. All `vm: true` apps are hermetic IFF they don't use static
      # files.
      if self.env is env.FLEX:
        self.is_hermetic = True
      elif parsed.vm:
        for urlmap in parsed.handlers:
          if urlmap.static_dir or urlmap.static_files:
            self.is_hermetic = False
            break
        else:
          self.is_hermetic = True
      else:
        self.is_hermetic = False
  
      self._InitializeHasExplicitSkipFiles(file_path, parsed)
      self._UpdateSkipFiles(parsed)
  
      if (self.env is env.MANAGED_VMS) or self.is_hermetic:
        self.runtime = parsed.GetEffectiveRuntime()
        self._UpdateVMSettings()
      else:
        self.runtime = parsed.runtime
  
      # New "Ti" style runtimes
      self.is_ti_runtime = env.GetTiRuntimeRegistry().Get(self.runtime, self.env)
  
    @staticmethod
    def FromFile(file_path):
      """Parses the given service file.
  
      Args:
        file_path: str, The full path to the service file.
  
      Raises:
        YamlParseError: If the file is not a valid Yaml-file.
        YamlValidationError: If validation of parsed info fails.
  
      Returns:
        A ServiceYamlInfo object for the parsed file.
      """
      try:
        parsed = _YamlInfo._ParseYaml(file_path, appinfo_includes.Parse)
      except (yaml_errors.Error, appinfo_errors.Error) as e:
        raise YamlParseError(file_path, e)
  
      info = ServiceYamlInfo(file_path, parsed)
      info.Validate()
      return info
  
    def Validate(self):
      """Displays warnings and raises exceptions for non-schema errors.
  
      Raises:
        YamlValidationError: If validation of parsed info fails.
      """
      if self.parsed.runtime == 'vm':
        vm_runtime = self.parsed.GetEffectiveRuntime()
      else:
        vm_runtime = None
        if self.parsed.runtime == 'python':
          raise YamlValidationError(
              'Service [{service}] uses unsupported Python 2.5 runtime. '
              'Please use [runtime: python27] instead.'.format(
                  service=(self.parsed.service or
                           ServiceYamlInfo.DEFAULT_SERVICE_NAME)))
        elif self.parsed.runtime == 'python-compat':
          raise YamlValidationError(
              '"python-compat" is not a supported runtime.')
        elif self.parsed.runtime == 'custom' and not self.parsed.env:
          raise YamlValidationError(
              'runtime "custom" requires that env be explicitly specified.')
  
      if self.env is env.MANAGED_VMS:
        log.warning(MANAGED_VMS_DEPRECATION_WARNING)
  
      if (self.env is env.FLEX and self.parsed.beta_settings and
          self.parsed.beta_settings.get('enable_app_engine_apis')):
        log.warning(APP_ENGINE_APIS_DEPRECATION_WARNING)
  
      if self.env is env.FLEX and vm_runtime == 'python27':
        raise YamlValidationError(
            'The "python27" is not a valid runtime in env: flex.  '
            'Please use [python] instead.')
  
      if self.env is env.FLEX and vm_runtime == 'python-compat':
        log.warning('[runtime: {}] is deprecated.  Please use [runtime: python] '
                    'instead.  See {} for more info.'
                    .format(vm_runtime, UPGRADE_FLEX_PYTHON_URL))
  
      for warn_text in self.parsed.GetWarnings():
        log.warning('In file [{0}]: {1}'.format(self.file, warn_text))
  
      if (self.env is env.STANDARD and
          self.parsed.runtime == 'python27' and
          HasLib(self.parsed, 'ssl', '2.7')):
        log.warning(PYTHON_SSL_WARNING)
  
      if (self.env is env.FLEX and
          vm_runtime == 'python' and
          GetRuntimeConfigAttr(self.parsed, 'python_version') == '3.4'):
        log.warning(FLEX_PY34_WARNING)
  
      _CheckIllegalAttribute(
          name='application',
          yaml_info=self.parsed,
          extractor_func=lambda yaml: yaml.application,
          file_path=self.file,
          msg=HINT_PROJECT)
  
      _CheckIllegalAttribute(
          name='version',
          yaml_info=self.parsed,
          extractor_func=lambda yaml: yaml.version,
          file_path=self.file,
          msg=HINT_VERSION)
  
      self._ValidateTi()
  
    def _ValidateTi(self):
      """Validation specifically for Ti-runtimes."""
      if not self.is_ti_runtime:
        return
      _CheckIllegalAttribute(
          name='threadsafe',
          yaml_info=self.parsed,
          extractor_func=lambda yaml: yaml.threadsafe,
          file_path=self.file,
          msg=HINT_THREADSAFE.format(self.runtime))
  
      # pylint: disable=cell-var-from-loop
      for handler in self.parsed.handlers:
        _CheckIllegalAttribute(
            name='application_readable',
            yaml_info=handler,
            extractor_func=lambda yaml: handler.application_readable,
            file_path=self.file,
            msg=HINT_READABLE.format(self.runtime))
  
    def RequiresImage(self):
      """Returns True if we'll need to build a docker image."""
      return self.env is env.MANAGED_VMS or self.is_hermetic
  
    def _UpdateVMSettings(self):
      """Overwrites vm_settings for App Engine services with VMs.
  
      Also sets module_yaml_path which is needed for some runtimes.
  
      Raises:
        AppConfigError: if the function was called for a standard service
      """
      if self.env not in [env.MANAGED_VMS, env.FLEX]:
        raise AppConfigError(
            'This is not an App Engine Flexible service. Please set `env` '
            'field to `flex`.')
      if not self.parsed.vm_settings:
        self.parsed.vm_settings = appinfo.VmSettings()
  
      self.parsed.vm_settings['module_yaml_path'] = os.path.basename(self.file)
  
    def GetAppYamlBasename(self):
      """Returns the basename for the app.yaml file for this service."""
      return os.path.basename(self.file)
  
    def HasExplicitSkipFiles(self):
      """Returns whether user has explicitly defined skip_files in app.yaml."""
      return self._has_explicit_skip_files
  
    def _InitializeHasExplicitSkipFiles(self, file_path, parsed):
      """Read app.yaml to determine whether user explicitly defined skip_files."""
      if getattr(parsed, 'skip_files', None) == appinfo.DEFAULT_SKIP_FILES:
        # Make sure that this was actually a default, not from the file.
        try:
          contents = files.ReadFileContents(file_path)
        except files.Error:  # If the class was initiated with a non-existent file
          contents = ''
        self._has_explicit_skip_files = 'skip_files' in contents
      else:
        self._has_explicit_skip_files = True
  
    def _UpdateSkipFiles(self, parsed):
      """Resets skip_files field to Flex default if applicable."""
      if self.RequiresImage() and not self.HasExplicitSkipFiles():
        # pylint:disable=protected-access
        parsed.skip_files = validation._RegexStrValue(
            validation.Regex(DEFAULT_SKIP_FILES_FLEX),
            DEFAULT_SKIP_FILES_FLEX,
            'skip_files')
        # pylint:enable=protected-access
  
  
  def HasLib(parsed, name, version=None):
    """Check if the parsed yaml has specified the given library.
  
    Args:
      parsed: parsed from yaml to python object
      name: str, Name of the library
      version: str, If specified, also matches against the version of the library.
  
    Returns:
      True if library with optionally the given version is present.
    """
    libs = parsed.libraries or []
    if version:
      return any(lib.name == name and lib.version == version for lib in libs)
    else:
      return any(lib.name == name for lib in libs)
  
  
  def GetRuntimeConfigAttr(parsed, attr):
    """Retrieve an attribute under runtime_config section.
  
    Args:
      parsed: parsed from yaml to python object
      attr: str, Attribute name, e.g. `runtime_version`
  
    Returns:
      The value of runtime_config.attr or None if the attribute isn't set.
    """
    return (parsed.runtime_config or {}).get(attr)
  
  
  def _CheckIllegalAttribute(name, yaml_info, extractor_func, file_path, msg=''):
    """Validates that an illegal attribute is not set.
  
    Args:
      name: str, The name of the attribute in the yaml files.
      yaml_info: AppInfoExternal, The yaml to validate.
      extractor_func: func(AppInfoExternal)->str, A function to extract the
        value of the attribute from a _YamlInfo object.
      file_path: str, The path of file from which yaml_info was parsed.
      msg: str, Message to couple with the error
  
    Raises:
        YamlValidationError: If illegal attribute is set.
  
    """
    attribute = extractor_func(yaml_info)
    if attribute is not None:
      # Disallow use of the given attribute.
      raise YamlValidationError(
          'The [{0}] field is specified in file [{1}]. '.format(name, file_path)
          + msg)
  