1# Copyright 2020 Google Inc. 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################################################################################ 16"""Cloud function to request builds.""" 17import base64 18import logging 19 20import google.auth 21from googleapiclient.discovery import build 22from google.cloud import ndb 23 24import build_lib 25import build_project 26from datastore_entities import BuildsHistory 27from datastore_entities import Project 28 29BASE_PROJECT = 'oss-fuzz-base' 30MAX_BUILD_HISTORY_LENGTH = 64 31QUEUE_TTL_SECONDS = 60 * 60 * 24 # 24 hours. 32 33 34def update_build_history(project_name, build_id, build_tag): 35 """Update build history of project.""" 36 project_key = ndb.Key(BuildsHistory, project_name + '-' + build_tag) 37 project = project_key.get() 38 39 if not project: 40 project = BuildsHistory(id=project_name + '-' + build_tag, 41 build_tag=build_tag, 42 project=project_name, 43 build_ids=[]) 44 45 if len(project.build_ids) >= MAX_BUILD_HISTORY_LENGTH: 46 project.build_ids.pop(0) 47 48 project.build_ids.append(build_id) 49 project.put() 50 51 52def get_project_data(project_name): 53 """Retrieve project metadata from datastore.""" 54 query = Project.query(Project.name == project_name) 55 project = query.get() 56 if not project: 57 raise RuntimeError( 58 'Project {0} not available in cloud datastore'.format(project_name)) 59 project_yaml_contents = project.project_yaml_contents 60 dockerfile_lines = project.dockerfile_contents.split('\n') 61 62 return (project_yaml_contents, dockerfile_lines) 63 64 65def get_build_steps(project_name, image_project, base_images_project): 66 """Retrieve build steps.""" 67 project_yaml_contents, dockerfile_lines = get_project_data(project_name) 68 return build_project.get_build_steps(project_name, project_yaml_contents, 69 dockerfile_lines, image_project, 70 base_images_project) 71 72 73# pylint: disable=no-member 74def run_build(project_name, image_project, build_steps, credentials, tag): 75 """Execute build on cloud build.""" 76 build_body = { 77 'steps': build_steps, 78 'timeout': str(build_lib.BUILD_TIMEOUT) + 's', 79 'options': { 80 'machineType': 'N1_HIGHCPU_32' 81 }, 82 'logsBucket': build_project.GCB_LOGS_BUCKET, 83 'tags': [project_name + '-' + tag,], 84 'queueTtl': str(QUEUE_TTL_SECONDS) + 's', 85 } 86 87 cloudbuild = build('cloudbuild', 88 'v1', 89 credentials=credentials, 90 cache_discovery=False) 91 build_info = cloudbuild.projects().builds().create(projectId=image_project, 92 body=build_body).execute() 93 build_id = build_info['metadata']['build']['id'] 94 95 update_build_history(project_name, build_id, tag) 96 logging.info('Build ID: %s', build_id) 97 logging.info('Logs: %s', build_project.get_logs_url(build_id, image_project)) 98 99 100# pylint: disable=no-member 101def request_build(event, context): 102 """Entry point for cloud function to request builds.""" 103 del context #unused 104 if 'data' in event: 105 project_name = base64.b64decode(event['data']).decode('utf-8') 106 else: 107 raise RuntimeError('Project name missing from payload') 108 109 with ndb.Client().context(): 110 credentials, image_project = google.auth.default() 111 build_steps = get_build_steps(project_name, image_project, BASE_PROJECT) 112 if not build_steps: 113 return 114 run_build(project_name, image_project, build_steps, credentials, 115 build_project.FUZZING_BUILD_TAG) 116