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