1# -*- coding: utf-8 -*- # 2# Copyright 2015 Google LLC. All Rights Reserved. 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); 5# you may not use this file except in compliance with the License. 6# You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15 16"""Fingerprinting code for the Python runtime.""" 17 18from __future__ import absolute_import 19from __future__ import division 20from __future__ import unicode_literals 21 22import os 23import textwrap 24 25from gae_ext_runtime import ext_runtime 26 27from googlecloudsdk.api_lib.app.images import config 28from googlecloudsdk.core import log 29from googlecloudsdk.core.util import files 30 31NAME = 'Python Compat' 32ALLOWED_RUNTIME_NAMES = ('python27', 'python-compat') 33PYTHON_RUNTIME_NAME = 'python27' 34 35# TODO(b/36055866): this generated app.yaml doesn't work because the compat 36# runtimes need a "handlers" section. Query the user for this information. 37PYTHON_APP_YAML = textwrap.dedent("""\ 38 env: flex 39 runtime: {runtime} 40 api_version: 1 41 threadsafe: false 42 # You must add a handlers section here. Example: 43 # handlers: 44 # - url: .* 45 # script: main.app 46 """) 47APP_YAML_WARNING = ('app.yaml has been generated, but needs to be provided a ' 48 '"handlers" section.') 49DOCKERIGNORE = textwrap.dedent("""\ 50 .dockerignore 51 Dockerfile 52 .git 53 .hg 54 .svn 55 """) 56COMPAT_DOCKERFILE_PREAMBLE = ( 57 'FROM gcr.io/google_appengine/python-compat-multicore\n') 58PYTHON27_DOCKERFILE_PREAMBLE = 'FROM gcr.io/google_appengine/python-compat\n' 59 60DOCKERFILE_INSTALL_APP = 'ADD . /app/\n' 61 62# TODO(b/36057458): Do the check for requirements.txt in the source inspection 63# and don't generate the pip install if it doesn't exist. 64DOCKERFILE_INSTALL_REQUIREMENTS_TXT = ( 65 'RUN if [ -s requirements.txt ]; then pip install -r requirements.txt; ' 66 'fi\n') 67 68 69class PythonConfigurator(ext_runtime.Configurator): 70 """Generates configuration for a Python application.""" 71 72 def __init__(self, path, params, runtime): 73 """Constructor. 74 75 Args: 76 path: (str) Root path of the source tree. 77 params: (ext_runtime.Params) Parameters passed through to the 78 fingerprinters. 79 runtime: (str) The runtime name. 80 """ 81 self.root = path 82 self.params = params 83 self.runtime = runtime 84 85 def GenerateAppYaml(self, notify): 86 """Generate app.yaml. 87 88 Args: 89 notify: depending on whether we're in deploy, write messages to the 90 user or to log. 91 Returns: 92 (bool) True if file was written 93 94 Note: this is not a recommended use-case, 95 python-compat users likely have an existing app.yaml. But users can 96 still get here with the --runtime flag. 97 """ 98 if not self.params.appinfo: 99 app_yaml = os.path.join(self.root, 'app.yaml') 100 if not os.path.exists(app_yaml): 101 notify('Writing [app.yaml] to [%s].' % self.root) 102 runtime = 'custom' if self.params.custom else self.runtime 103 files.WriteFileContents(app_yaml, 104 PYTHON_APP_YAML.format(runtime=runtime)) 105 log.warning(APP_YAML_WARNING) 106 return True 107 return False 108 109 def GenerateDockerfileData(self): 110 """Generates dockerfiles. 111 112 Returns: 113 list(ext_runtime.GeneratedFile) the list of generated dockerfiles 114 """ 115 if self.runtime == 'python-compat': 116 dockerfile_preamble = COMPAT_DOCKERFILE_PREAMBLE 117 else: 118 dockerfile_preamble = PYTHON27_DOCKERFILE_PREAMBLE 119 120 all_config_files = [] 121 122 dockerfile_name = config.DOCKERFILE 123 dockerfile_components = [dockerfile_preamble, DOCKERFILE_INSTALL_APP] 124 if self.runtime == 'python-compat': 125 dockerfile_components.append(DOCKERFILE_INSTALL_REQUIREMENTS_TXT) 126 dockerfile_contents = ''.join(c for c in dockerfile_components) 127 dockerfile = ext_runtime.GeneratedFile(dockerfile_name, 128 dockerfile_contents) 129 all_config_files.append(dockerfile) 130 131 dockerignore = ext_runtime.GeneratedFile('.dockerignore', DOCKERIGNORE) 132 all_config_files.append(dockerignore) 133 134 return all_config_files 135 136 def GenerateConfigs(self): 137 """Generate all config files for the module.""" 138 # Write messages to user or to log depending on whether we're in "deploy." 139 notify = log.info if self.params.deploy else log.status.Print 140 141 self.GenerateAppYaml(notify) 142 143 created = False 144 if self.params.custom or self.params.deploy: 145 dockerfiles = self.GenerateDockerfileData() 146 for dockerfile in dockerfiles: 147 if dockerfile.WriteTo(self.root, notify): 148 created = True 149 if not created: 150 notify('All config files already exist, not generating anything.') 151 return created 152 153 def GenerateConfigData(self): 154 """Generate all config files for the module. 155 156 Returns: 157 list(ext_runtime.GeneratedFile) A list of the config files 158 that were generated 159 """ 160 # Write messages to user or to log depending on whether we're in "deploy." 161 notify = log.info if self.params.deploy else log.status.Print 162 163 self.GenerateAppYaml(notify) 164 if not (self.params.custom or self.params.deploy): 165 return [] 166 all_config_files = self.GenerateDockerfileData() 167 return [f for f in all_config_files 168 if not os.path.exists(os.path.join(self.root, f.filename))] 169 170 171def Fingerprint(path, params): 172 """Check for a Python app. 173 174 Args: 175 path: (str) Application path. 176 params: (ext_runtime.Params) Parameters passed through to the 177 fingerprinters. 178 179 Returns: 180 (PythonConfigurator or None) Returns a module if the path contains a 181 python app. 182 """ 183 log.info('Checking for Python Compat.') 184 185 # The only way we select these runtimes is if either the user has specified 186 # it or a matching runtime is specified in the app.yaml. 187 if (not params.runtime and 188 (not params.appinfo or 189 params.appinfo.GetEffectiveRuntime() not in ALLOWED_RUNTIME_NAMES)): 190 return None 191 192 if params.appinfo: 193 runtime = params.appinfo.GetEffectiveRuntime() 194 else: 195 runtime = params.runtime 196 197 log.info('Python Compat matches ([{0}] specified in "runtime" field)'.format( 198 runtime)) 199 return PythonConfigurator(path, params, runtime) 200