blob: 46f8850eb53770c6f580d7d57a602b877ab1487e [file] [log] [blame]
"""
Release script invoked from git trigger upon submission of changes to release-versions.yaml config file to the cos/tools GoB repo
Parses contents of release-versions.yaml file and copies release candidates to gcr.io/cos-tools
"""
import sys
import yaml
import subprocess
import os
import json
from datetime import datetime
_SBOM_TAG = "sbom"
_SBOM_TAG_PER_ARCH={"amd64": "sbom_amd64", "arm64": "sbom_arm64"}
_VULN_SCANNING_TAG_PREFIX = "public-image-"
_VULN_SCANNING_DEPR_TAG_PREFIX = "no-new-use-public-image-"
_COS_GPU_INSTALLER_STAGING_NAME = "cos-gpu-installer"
def validate_config(release_config):
for release_container in release_config:
for key in ["staging_container_name", "release_container_name", "build_tag", "release_tags"]:
assert key in release_container, "missing {} in entry {}".format(key, release_container)
def validate_src_gcr_path(path):
# path format: gcr.io/cos-infra-prod
return len(path) > len("gcr.io/") and path[:len("gcr.io/")] == "gcr.io/"
def validate_dst_gcr_path(path):
# path format: us-docker.pkg.dev/cos-cloud/us.gcr.io
path = path.split('/')
return len(path) == 3 and len(path[0]) > len("docker.pkg.dev/") and len(path[1]) != 0 and path[2][-len("gcr.io"):] == "gcr.io"
def copy_container_image(src_bucket, dst_bucket, staging_container_name, release_container_name, build_tag, release_tags):
assert validate_src_gcr_path(src_bucket), "cannot use address {}, only gcr.io/ addresses are supported".format(src_bucket)
assert validate_dst_gcr_path(dst_bucket), "cannot use address {}, only <location>-docker.pkg.dev/<project-name>/<location(optional)>gcr.io/ addresses are supported".format(dst_bucket)
src_path = os.path.join(src_bucket, staging_container_name)
dst_path = os.path.join(dst_bucket, release_container_name)
for release_tag in release_tags:
subprocess.run(["gcloud", "container", "images", "add-tag", src_path + ":" + build_tag, dst_path + ":" + release_tag, "-q"], check=True)
# Add tag to scan cos-gpu-installer for vulnerabilities. This tag tells public image scanning pipline to scan the image and create bugs.
def add_tag_for_vuln_scanning(src_bucket, staging_container_name, build_tag):
if staging_container_name != _COS_GPU_INSTALLER_STAGING_NAME:
return
assert validate_src_gcr_path(src_bucket), "cannot use address {}, only gcr.io/ addresses are supported".format(src_bucket)
src_path = os.path.join(src_bucket, staging_container_name)
# Check if we tagged any image with _VULN_SCANNING_TAG before.
tag_filter = '--filter=tags:{}'.format(_VULN_SCANNING_TAG_PREFIX)
existing_tags = subprocess.run(
['gcloud', 'container', 'images', 'list-tags', src_path, tag_filter, '--format=json'],
capture_output = True,
check = True,
)
now = datetime.now()
date_time = now.strftime("%m%d%Y-%H%M%S")
scanning_depr_tag = _VULN_SCANNING_DEPR_TAG_PREFIX + date_time
# If no image is tagged with "public-image", add the tag and return.
if existing_tags.stdout.decode('utf-8').rstrip() == "[]":
subprocess.run(["gcloud", "container", "images", "add-tag", src_path + ":" + build_tag, src_path + ":" + _VULN_SCANNING_TAG_PREFIX, "-q"], check=True)
return
# Use current date-time as a suffix to make deprecation tag unique.
subprocess.run(["gcloud", "container", "images", "add-tag", src_path + ":" + _VULN_SCANNING_TAG_PREFIX, src_path + ":" + scanning_depr_tag, "-q"], check=True)
subprocess.run(["gcloud", "container", "images", "add-tag", src_path + ":" + build_tag, src_path + ":" + _VULN_SCANNING_TAG_PREFIX, "-q"], check=True)
# Add tag for generating and uploading SBOM for cos-gpu-installer via louhi workflow.
def add_tag_for_sbom(src_bucket, staging_container_name, release_container_name, build_tag):
if staging_container_name != _COS_GPU_INSTALLER_STAGING_NAME:
return
assert validate_src_gcr_path(src_bucket), "cannot use address {}, only gcr.io/ addresses are supported".format(src_bucket)
src_path = os.path.join(src_bucket, staging_container_name)
dst_path = os.path.join(src_bucket, release_container_name)
subprocess.run(["gcloud", "container", "images", "add-tag", src_path + ":" + build_tag, dst_path + ":" + _SBOM_TAG, "-q"], check=True)
# We'd also like to add tags to arch specific container images.
# Refer: http://b/331789707#comment20
multiarch_image = f'{src_path}:{build_tag}'
manifest_command = ["docker", "manifest", "inspect", multiarch_image]
manifest_output = subprocess.run(manifest_command, capture_output=True)
# Do not continue if manifest could not be pulled.
if manifest_output.stderr:
return
try:
manifest = json.loads(manifest_output.stdout)
except:
return
assert manifest != None
# The schema can be referred here - http://shortn/_fctoXEj6XM
for manifest_entry in manifest.get('manifests', []):
digest = manifest_entry.get('digest')
architecture = manifest_entry.get('platform', {}).get('architecture')
# Sanity check
if digest == "" or architecture not in _SBOM_TAG_PER_ARCH:
continue
# Add tag now
subprocess.run(["gcloud", "container", "images", "add-tag", src_path + "@" + digest, dst_path + ":" + _SBOM_TAG_PER_ARCH[architecture], "-q"])
def verify_and_release(src_bucket, dst_buckets, release):
with open('release/release-versions.yaml', 'r') as file:
try:
release_config = yaml.safe_load(file)
validate_config(release_config)
if release:
dst_buckets = dst_buckets.split('^')
for release_container in release_config:
staging_container_name = release_container["staging_container_name"]
release_container_name = release_container["release_container_name"]
build_tag = release_container["build_tag"]
release_tags = release_container["release_tags"]
for dst_bucket in dst_buckets:
copy_container_image(src_bucket, dst_bucket, staging_container_name, release_container_name, build_tag, release_tags)
add_tag_for_sbom(src_bucket, staging_container_name, release_container_name, build_tag)
# Wait until we a have bug cloning functionality.
# add_tag_for_vuln_scanning(src_bucket, staging_container_name, build_tag)
except yaml.YAMLError as ex:
raise Exception("Invalid YAML config: %s" % str(ex))
def main():
if len(sys.argv) == 2 and sys.argv[1] == "--verify":
verify_and_release("", "", False)
elif len(sys.argv) == 3:
src_bucket = sys.argv[1]
dst_buckets = sys.argv[2]
verify_and_release(src_bucket, dst_buckets, True)
else:
sys.exit("sample use: ./release_script <source_gcr_path> <destination_gcr_paths> \n \
example use: ./release_script gcr.io/cos-infra-prod us-docker.pkg.dev/cos-cloud/us.gcr.io^europe-docker.pkg.dev/cos-cloud/eu.gcr.io")
if __name__ == '__main__':
main()