blob: dd852b9f32a339645f82e0421a5c4bf4de93617a [file] [log] [blame] [edit]
#!/usr/bin/env python3
#
# Copyright 2018 The Bazel Authors. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# pylint: disable=line-too-long
# pylint: disable=missing-function-docstring
# pylint: disable=unspecified-encoding
# pylint: disable=invalid-name
"""The CI script for Bazel Central Registry Postsubmit.
This script does one thing:
- Sync the bazel_registry.json and modules/ directory in the main branch of the BCR to https://bcr.bazel.build
"""
import base64
import hashlib
import json
import os
import requests
import subprocess
import sys
import tempfile
BCR_BUCKET = "gs://bcr.bazel.build/"
ATTESTATION_METADATA_FILE = "attestations.json"
FILES_WITH_ATTESTATIONS = ("source.json", "MODULE.bazel")
# Basename of the file that contains the most recent commit
# that passed through the post-submit pipeline successfully.
LAST_GREEN_FILE = "last_green.txt"
class AttestationError(Exception):
"""Raised when there is a problem wrt attestations."""
def print_expanded_group(name):
print("\n\n+++ {0}\n\n".format(name))
def get_output(command):
return subprocess.run(
command,
encoding='utf-8',
stdout=subprocess.PIPE,
).stdout
def check_and_write_new_attestations():
print_expanded_group(":cop::copybara: Check & write attestations")
paths = get_new_attestations_json_paths()
if not paths:
# TODO: turn this into an error
print(f"No {ATTESTATION_METADATA_FILE} files were changed.")
return
for p in paths:
check_and_write_module_attestations(p)
def get_new_attestations_json_paths():
cwd = os.getcwd()
cmd = ["git", "diff-tree", "--no-commit-id", "--name-only", "-r"]
# last_green should be the parent commit. However, sometimes the
# pipeline can fail due to infra issues. In this case we need
# to mirror attestations in the commits of the failing runs, too.
last_green = get_last_green()
if last_green:
cmd.append(last_green)
paths = get_output(cmd + [get_commit()])
return [os.path.join(cwd, p) for p in paths.split("\n") if p.endswith(f"/{ATTESTATION_METADATA_FILE}")]
def get_last_green():
url = os.path.join(
BCR_BUCKET.replace("gs://", "https://storage.googleapis.com/"), LAST_GREEN_FILE
)
with requests.get(url) as response:
if response.status_code != 200:
return ""
return response.content.decode("utf-8")
def get_commit():
return os.getenv("BUILDKITE_COMMIT")
def check_and_write_module_attestations(attestations_json_path):
print(f"Checking {attestations_json_path}...")
dest_dir = os.path.dirname(attestations_json_path)
with open(attestations_json_path, "rb") as af:
metadata = json.loads(af.read())
for f in FILES_WITH_ATTESTATIONS:
try:
entry = metadata["attestations"][f]
check_and_write_single_attestation(entry["url"], entry["integrity"], dest_dir)
except Exception as ex:
raise AttestationError(f"{attestations_json_path} - {f}: {ex}") from ex
print("Done!")
def check_and_write_single_attestation(url, integrity, dest_dir):
print(f"\tFound attestation @ {url}")
with requests.get(url) as response:
if response.status_code != 200:
raise AttestationError(f"{url}: HTTP {response.status_code}")
raw_content = response.content
check_integrity(raw_content, integrity)
print("\t\tIntegrity: OK")
dest = os.path.join(dest_dir, get_canonical_basename(url))
print(f"\t\tWriting attestation to {dest}...")
with open(dest, "wb") as f:
f.write(raw_content)
def check_integrity(data, expected):
algorithm, _, _ = expected.partition("-")
assert algorithm in {"sha224", "sha256", "sha384", "sha512"}, "Unsupported SRI algorithm"
hash = getattr(hashlib, algorithm)(data)
encoded = base64.b64encode(hash.digest()).decode()
actual = f"{algorithm}-{encoded}"
if actual != expected:
raise AttestationError(f"Expected checksum {expected}, got {actual}.")
# Attestation files in GitHub releases may have prefixes in their basename
# to avoid conflicts when multiple modules are released together
# (e.g. rules_python and rules_python_gazelle_plugin).
# In this case we need to get the canonical basename.
def get_canonical_basename(url):
actual_basename = os.path.basename(url)
for f in FILES_WITH_ATTESTATIONS:
if f in actual_basename:
return f"{f}.intoto.jsonl"
raise AttestationError(f"Invalid basename of {url}.")
def sync_bcr_content():
print_expanded_group(":gcloud: Sync BCR content")
subprocess.check_output(
["gsutil", "-h", "Cache-Control:no-cache", "cp", "./bazel_registry.json", BCR_BUCKET]
)
subprocess.check_output(
["gsutil", "-h", "Cache-Control:no-cache", "-m", "rsync", "-r", "./modules", BCR_BUCKET + "modules"]
)
def update_last_green():
path = os.path.join(tempfile.mkdtemp(), LAST_GREEN_FILE)
with open(path, "wt") as f:
f.write(get_commit())
dest = os.path.join(BCR_BUCKET, LAST_GREEN_FILE)
subprocess.check_output(["gsutil", "cp", path, dest])
def main():
check_and_write_new_attestations()
sync_bcr_content()
update_last_green()
return 0
if __name__ == "__main__":
sys.exit(main())