github.com/whamcloud/lemur@v0.0.0-20190827193804-4655df8a52af/packaging/ci/lambda/GitPullS3/lambda_function.py (about)

     1  #  Copyright 2016 Amazon Web Services, Inc. or its affiliates. All Rights Reserved.
     2  #  This file is licensed to you under the AWS Customer Agreement (the "License").
     3  #  You may not use this file except in compliance with the License.
     4  #  A copy of the License is located at http://aws.amazon.com/agreement/ .
     5  #  This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express or implied.
     6  #  See the License for the specific language governing permissions and limitations under the License.
     7  #
     8  # mjmac 2016-12-07: Change exclude_git to False. Add code to embed source
     9  #                   metadata in archive object's metadata in order to
    10  #                   enable its use by subsequent pipeline stages.
    11  
    12  from pygit2 import Keypair,credentials,discover_repository,Repository,clone_repository,RemoteCallbacks
    13  from boto3 import client
    14  import os,stat
    15  import shutil
    16  from zipfile import ZipFile
    17  from ipaddress import ip_network, ip_address
    18  import json
    19  import logging
    20  import hmac
    21  import hashlib
    22  
    23  ### If true the function will not include .git folder in the zip
    24  exclude_git=False
    25  
    26  ### If true the function will delete all files at the end of each invocation, useful if you run into storage space constraints, but will slow down invocations as each invoke will need to checkout the entire repo
    27  cleanup=False
    28  
    29  key='enc_key'
    30  
    31  logger = logging.getLogger()
    32  logger.setLevel(logging.INFO)
    33  logger.handlers[0].setFormatter(logging.Formatter('[%(asctime)s][%(levelname)s] %(message)s'))
    34  logging.getLogger('boto3').setLevel(logging.ERROR)
    35  logging.getLogger('botocore').setLevel(logging.ERROR)
    36  
    37  s3 = client('s3')
    38  kms = client('kms')
    39  
    40  def write_key(filename,contents):
    41      logger.info('Writing keys to /tmp/...')
    42      mode = stat.S_IRUSR | stat.S_IWUSR
    43      umask_original = os.umask(0)
    44      try:
    45          handle = os.fdopen(os.open(filename, os.O_WRONLY | os.O_CREAT, mode), 'w')
    46      finally:
    47          os.umask(umask_original)
    48      handle.write(contents+'\n')
    49      handle.close()
    50  
    51  def get_keys(keybucket,pubkey,update=False):
    52      if not os.path.isfile('/tmp/id_rsa') or not os.path.isfile('/tmp/id_rsa.pub') or update:
    53          logger.info('Keys not found on Lambda container, fetching from S3...')
    54          enckey = s3.get_object(Bucket=keybucket,Key=key)['Body'].read()
    55          privkey = kms.decrypt(CiphertextBlob=enckey)['Plaintext']
    56          write_key('/tmp/id_rsa',privkey)
    57          write_key('/tmp/id_rsa.pub',pubkey)
    58      return Keypair('git','/tmp/id_rsa.pub','/tmp/id_rsa','')
    59  
    60  def init_remote(repo, name, url):
    61      remote = repo.remotes.create(name, url, '+refs/*:refs/*')
    62      return remote
    63  
    64  def create_repo(repo_path, remote_url, creds):
    65      if os.path.exists(repo_path):
    66              logger.info('Cleaning up repo path...')
    67              shutil.rmtree(repo_path)
    68      repo = clone_repository(remote_url, repo_path, callbacks=creds )
    69  
    70      return repo
    71  
    72  def pull_repo(repo, remote_url, creds):
    73      remote_exists = False
    74      for r in repo.remotes:
    75          if r.url == remote_url:
    76              remote_exists = True
    77              remote = r
    78      if not remote_exists:
    79          remote = repo.create_remote('origin',remote_url)
    80      logger.info('Fetching and merging changes...')
    81      remote.fetch(callbacks=creds)
    82      remote_master_id = repo.lookup_reference('refs/remotes/origin/master').target
    83      repo.checkout_tree(repo.get(remote_master_id))
    84      master_ref = repo.lookup_reference('refs/heads/master')
    85      master_ref.set_target(remote_master_id)
    86      repo.head.set_target(remote_master_id)
    87      return repo
    88  
    89  def zip_repo(repo_path,repo_name):
    90      logger.info('Creating zipfile...')
    91      zf = ZipFile('/tmp/'+repo_name.replace('/','_')+'.zip','w')
    92      for dirname, subdirs, files in os.walk(repo_path):
    93          if exclude_git:
    94              try:
    95                  subdirs.remove('.git')
    96              except ValueError:
    97                  pass
    98          zdirname = dirname[len(repo_path)+1:]
    99          zf.write(dirname,zdirname)
   100          for filename in files:
   101              zf.write(os.path.join(dirname, filename),os.path.join(zdirname, filename))
   102      zf.close()
   103      return '/tmp/'+repo_name.replace('/','_')+'.zip'
   104  
   105  def push_s3(filename,repo_name,outputbucket,tags={}):
   106      s3key='%s/%s' % (repo_name,filename.replace('/tmp/',''))
   107      logger.info('pushing zip to s3://%s/%s' % (outputbucket,s3key))
   108      data=open(filename,'rb')
   109      s3.put_object(Bucket=outputbucket,Body=data,Key=s3key,Metadata=tags)
   110      logger.info('Completed S3 upload...')
   111  
   112  def lambda_handler(event,context):
   113      print json.dumps(event)
   114      keybucket=event['context']['key-bucket']
   115      outputbucket=event['context']['output-bucket']
   116      pubkey=event['context']['public-key']
   117      ### Source IP ranges to allow requests from, if the IP is in one of these the request will not be chacked for an api key
   118      ipranges=[]
   119      for i in event['context']['allowed-ips'].split(','):
   120          ipranges.append(ip_network(u'%s' % i))
   121      ### APIKeys, it is recommended to use a different API key for each repo that uses this function
   122      apikeys=event['context']['api-secrets'].split(',')
   123      ip = ip_address(event['context']['source-ip'])
   124      secure=False
   125      for net in ipranges:
   126          if ip in net:
   127              secure=True
   128      if 'X-Gitlab-Token' in event['params']['header'].keys():
   129          if event['params']['header']['X-Gitlab-Token'] in apikeys:
   130              secure=True
   131      if 'X-Git-Token' in event['params']['header'].keys():
   132          if event['params']['header']['X-Git-Token'] in apikeys:
   133              secure=True
   134      if 'X-Gitlab-Token' in event['params']['header'].keys():
   135          if event['params']['header']['X-Gitlab-Token'] in apikeys:
   136              secure=True
   137      if 'X-Hub-Signature' in event['params']['header'].keys():
   138          for k in apikeys:
   139              if hmac.new(str(k),str(event['context']['raw-body']),hashlib.sha1).hexdigest() == str(event['params']['header']['X-Hub-Signature'].replace('sha1=','')):
   140                  secure=True
   141      if not secure:
   142          logger.error('Source IP %s is not allowed' % event['context']['source-ip'])
   143          raise Exception('Source IP %s is not allowed' % event['context']['source-ip'])
   144      try:
   145          repo_name = event['body-json']['project']['path_with_namespace']
   146      except:
   147          repo_name = event['body-json']['repository']['full_name']
   148      try:
   149          remote_url = event['body-json']['project']['git_ssh_url']
   150      except:
   151          try:
   152              remote_url = 'git@'+event['body-json']['repository']['links']['html']['href'].replace('https://','').replace('/',':',1)+'.git'
   153          except:
   154              remote_url = event['body-json']['repository']['ssh_url']
   155      repo_path = '/tmp/%s' % repo_name
   156      creds = RemoteCallbacks( credentials=get_keys(keybucket,pubkey), )
   157      try:
   158          repository_path = discover_repository(repo_path)
   159          repo = Repository(repository_path)
   160          logger.info('found existing repo, using that...')
   161      except:
   162          logger.info('creating new repo for %s in %s' % (remote_url, repo_path))
   163          repo = create_repo(repo_path, remote_url, creds)
   164      pull_repo(repo,remote_url,creds)
   165      zipfile = zip_repo(repo_path, repo_name)
   166      s3_tags = dict(
   167          source_revision = event['body-json']['head_commit']['id'],
   168          source_html_url = event['body-json']['repository']['html_url'],
   169          source_ref = event['body-json']['ref']
   170      )
   171      push_s3(zipfile,repo_name,outputbucket,s3_tags)
   172      if cleanup:
   173          logger.info('Cleanup Lambda container...')
   174          shutil.rmtree(repo_path)
   175          shutil.rm(zipfile)
   176          shutil.rm('/tmp/id_rsa')
   177          shutil.rm('/tmp/id_rsa.pub')
   178      return 'Successfully updated %s' % repo_name