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)