k8s.io/test-infra@v0.0.0-20240520184403-27c6b4c223d8/gubernator/view_base.py (about) 1 # Copyright 2016 The Kubernetes Authors. 2 # 3 # Licensed under the Apache License, Version 2.0 (the "License"); 4 # you may not use this file except in compliance with the License. 5 # You may obtain a copy of the License at 6 # 7 # http://www.apache.org/licenses/LICENSE-2.0 8 # 9 # Unless required by applicable law or agreed to in writing, software 10 # distributed under the License is distributed on an "AS IS" BASIS, 11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 # See the License for the specific language governing permissions and 13 # limitations under the License. 14 15 import cPickle as pickle 16 import functools 17 import logging 18 import os 19 import re 20 21 import cloudstorage as gcs 22 import jinja2 23 import webapp2 24 25 from google.appengine.api import urlfetch 26 from google.appengine.api import memcache 27 from webapp2_extras import sessions 28 from webapp2_extras import security 29 30 import secrets 31 import filters as jinja_filters 32 33 JINJA_ENVIRONMENT = jinja2.Environment( 34 loader=jinja2.FileSystemLoader(os.path.dirname(__file__) + '/templates'), 35 extensions=['jinja2.ext.autoescape', 'jinja2.ext.loopcontrols'], 36 trim_blocks=True, 37 autoescape=True) 38 JINJA_ENVIRONMENT.line_statement_prefix = '%' 39 jinja_filters.register(JINJA_ENVIRONMENT.filters) 40 41 42 def get_session_secret(): 43 try: 44 return str(secrets.get('session')) 45 except KeyError: 46 # Make a new session key -- only happens once per hostname! 47 logging.warning('creating new session key!') 48 session_key = security.generate_random_string(entropy=256) 49 secrets.put('session', session_key) 50 return session_key 51 52 53 class BaseHandler(webapp2.RequestHandler): 54 """Base class for Handlers that render Jinja templates.""" 55 def __init__(self, *args, **kwargs): 56 super(BaseHandler, self).__init__(*args, **kwargs) 57 # The default deadline of 5 seconds is too aggressive of a target for GCS 58 # directory listing operations. 59 urlfetch.set_default_fetch_deadline(60) 60 61 def check_csrf(self): 62 # https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet 63 # #Checking_The_Referer_Header 64 origin = self.request.headers.get('origin') + '/' 65 expected = self.request.host_url + '/' 66 if not (origin and origin == expected): 67 logging.error('csrf check failed for %s, origin: %r', self.request.url, origin) 68 self.abort(403) 69 70 # This example code is from: 71 # http://webapp2.readthedocs.io/en/latest/api/webapp2_extras/sessions.html 72 def dispatch(self): 73 # pylint: disable=attribute-defined-outside-init 74 75 # maybe initialize secrets (first request) 76 sessions_config = self.app.config['webapp2_extras.sessions'] 77 if not sessions_config['secret_key']: 78 sessions_config['secret_key'] = get_session_secret() 79 80 # Get a session store for this request. 81 self.session_store = sessions.get_store(request=self.request) 82 83 try: 84 # Dispatch the request. 85 webapp2.RequestHandler.dispatch(self) 86 finally: 87 # Save all sessions. 88 self.session_store.save_sessions(self.response) 89 90 @webapp2.cached_property 91 def session(self): 92 # Returns a session using the default cookie key. 93 return self.session_store.get_session() 94 95 def render(self, template, context): 96 """Render a context dictionary using a given template.""" 97 template = JINJA_ENVIRONMENT.get_template(template) 98 self.response.write(template.render(context)) 99 100 101 class IndexHandler(BaseHandler): 102 """Render the index.""" 103 def get(self): 104 self.render("index.html", {'jobs': self.app.config['jobs']}) 105 106 107 def memcache_memoize(prefix, expires=60 * 60, neg_expires=60): 108 """Decorate a function to memoize its results using memcache. 109 110 The function must take a single string as input, and return a pickleable 111 type. 112 113 Args: 114 prefix: A prefix for memcache keys to use for memoization. 115 expires: How long to memoized values, in seconds. 116 neg_expires: How long to memoize falsey values, in seconds 117 Returns: 118 A decorator closure to wrap the function. 119 """ 120 # setting the namespace based on the current version prevents different 121 # versions from sharing cache values -- meaning there's no need to worry 122 # about incompatible old key/value pairs 123 namespace = os.environ['CURRENT_VERSION_ID'] 124 def wrapper(func): 125 @functools.wraps(func) 126 def wrapped(*args): 127 key = '%s%s' % (prefix, args) 128 data = memcache.get(key, namespace=namespace) 129 if data is not None: 130 return data 131 else: 132 data = func(*args) 133 serialized_length = len(pickle.dumps(data, pickle.HIGHEST_PROTOCOL)) 134 if serialized_length > 1000000: 135 logging.warning('data too large to fit in memcache: %s > 1MB', 136 serialized_length) 137 return data 138 try: 139 if data: 140 memcache.add(key, data, expires, namespace=namespace) 141 else: 142 memcache.add(key, data, neg_expires, namespace=namespace) 143 except ValueError: 144 logging.exception('unable to write to memcache') 145 return data 146 return wrapped 147 return wrapper 148 149 150 @memcache_memoize('gs-ls://', expires=60) 151 def gcs_ls(path): 152 """Enumerate files in a GCS directory. Returns a list of FileStats.""" 153 if path[-1] != '/': 154 path += '/' 155 return list(gcs.listbucket(path, delimiter='/')) 156 157 @memcache_memoize('gs-ls-recursive://', expires=60) 158 def gcs_ls_recursive(path): 159 """Enumerate files in a GCS directory recursively. Returns a list of FileStats.""" 160 if path[-1] != '/': 161 path += '/' 162 163 return list(gcs.listbucket(path)) 164 165 def pad_numbers(s): 166 """Modify a string to make its numbers suitable for natural sorting.""" 167 return re.sub(r'\d+', lambda m: m.group(0).rjust(16, '0'), s)