app-dev/cloud-run-deployment-previews/check_status.py (176 lines of code) (raw):

#!/usr/bin/python # # Copyright 2020 Google LLC # # 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. from __future__ import annotations from collections.abc import Callable import os import re import sys import click import github from github.GithubException import GithubException from googleapiclient import discovery from googleapiclient.errors import HttpError # cloud run tags much be lowercase TAG_PREFIX = "pr-" def make_tag(pr: str) -> str: return f"{TAG_PREFIX}{pr}" def get_pr(tag: str) -> int: return int(tag.replace(TAG_PREFIX, "")) _default_options = [ click.option( "--dry-run", help="Dry-run mode. No tag changes made", default=False, is_flag=True, ), ] _cloudrun_options = [ click.option("--project-id", required=True, help="Google Cloud Project ID"), click.option( "--region", required=True, help="Google Cloud Region", default="us-central1" ), click.option("--service", required=True, help="Google Cloud Run service name"), ] _github_options = [ click.option( "--repo-name", required=True, help="GitHub repo name (user/repo, or org/repo)" ) ] def add_options(options: list[dict]) -> Callable: def _add_options(func: Callable) -> Callable: for option in reversed(options): func = option(func) return func return _add_options def error(msg: str, context: str = None) -> None: click.secho(f"Error {context}: ", fg="red", bold=True, nl=False) click.echo(msg) sys.exit(1) def get_service(project_id: str, region: str, service_name: str) -> dict: """Get the Cloud Run service object""" api = discovery.build("run", "v1") fqname = f"projects/{project_id}/locations/{region}/services/{service_name}" try: service = api.projects().locations().services().get(name=fqname).execute() except HttpError as e: error(re.search('"(.*)"', str(e)).group(0), context="finding service") return service def update_service(project_id: str, region: str, service_name: str, body: dict) -> dict: """Update the Cloud Run service.""" api = discovery.build("run", "v1") fqname = f"projects/{project_id}/locations/{region}/services/{service_name}" try: result = ( api.projects() .locations() .services() .replaceService(name=fqname, body=body) .execute() ) except HttpError as e: error(re.search('"(.*)"', str(e)).group(0), context="updating service") return result def get_revision_url(service_obj: dict, tag: str) -> str: """Get the revision URL for the tag specified on the service""" for revision in service_obj["status"]["traffic"]: if revision.get("tag", None) == tag: return revision["url"] error( f"Tag on service {service_obj['metadata']['name']} does not exist.", context=f"finding revision tagged {tag}", ) def get_revision_tags(service: dict) -> list[str]: """Get all tags associated to a service""" revs = [] for revision in service["status"]["traffic"]: if revision.get("tag", None): revs.append(revision) return revs @click.group() def cli() -> None: """Tool for setting GitHub Status Checks to Cloud Run Revision URLs""" @cli.command() @add_options(_default_options) @add_options(_cloudrun_options) @add_options(_github_options) def cleanup( dry_run: str, project_id: str, region: str, service: str, repo_name: str ) -> None: """Cleanup any revision URLs against closed pull requests""" service_obj = get_service(project_id, region, service) revs = get_revision_tags(service_obj) if not revs: click.echo("No revision tags found, nothing to clean up") sys.exit(0) ghtoken = os.environ.get("GITHUB_TOKEN", None) if not ghtoken: raise ValueError("GITHUB_TOKEN not defined.") try: repo = github.Github(ghtoken).get_repo(repo_name) except GithubException as e: error(e.data["message"], context=f"finding repo {repo_name}") tags_to_delete = [] for rev in revs: tag = rev["tag"] pr = get_pr(tag) pull_request = repo.get_pull(pr) if pull_request.state == "closed": if dry_run: click.secho("Dry-run: ", fg="blue", bold=True, nl=False) click.echo( f"PR {pr} is closed, so would remove tag {tag} on service {service}" ) else: tags_to_delete.append(tag) if tags_to_delete: # Edit the service by removing the tags from the traffic spec, then replace the service # with this new configuration. for tag in tags_to_delete: for traffic in service_obj["spec"]["traffic"]: if "tag" in traffic.keys() and tag == traffic["tag"]: service_obj["spec"]["traffic"].remove(traffic) click.echo(f"Updating the service to remove tags: {','.join(tags_to_delete)}.") update_service(project_id, region, service, service_obj) else: click.echo("Did not identify any tags to delete.") @cli.command() @add_options(_default_options) @add_options(_cloudrun_options) @add_options(_github_options) @click.option("--pull-request", required=True, help="GitHub Pull Request ID", type=int) @click.option("--commit-sha", required=True, help="GitHub commit (SHORT_SHA)") def set( dry_run: str, project_id: str, region: str, service: str, repo_name: str, commit_sha: str, pull_request: str, ) -> None: """Set a status on a GitHub commit to a specific revision URL""" service_obj = get_service(project_id, region, service) revision_url = get_revision_url(service_obj, tag=make_tag(pull_request)) ghtoken = os.environ.get("GITHUB_TOKEN", None) if not ghtoken: raise ValueError("GITHUB_TOKEN not defined.") try: repo = github.Github(ghtoken).get_repo(repo_name) except GithubException as e: error( e.data["message"], context=f"finding repo {repo_name}. Is it a private repo, and does your token have the correct permissions?", ) try: commit = repo.get_commit(sha=commit_sha) except GithubException as e: error(e.data["message"], context=f"finding commit {commit_sha}") if dry_run: click.secho("Dry-run: ", fg="blue", bold=True, nl=False) click.echo( f"Status would have been created on {repo_name}, " f"commit {commit.sha[:7]}, linking to {revision_url} " f"on service {service_obj['metadata']['name']}" ) return commit.create_status( state="success", target_url=revision_url, context=f"Deployment Preview for {service}", description="Your preview is now available.", ) click.secho("Success: ", fg="green", bold=True, nl=False) click.echo( f"Status created on {repo_name}, commit {commit.sha[:7]}, " f"linking to {revision_url} on service {service_obj['metadata']['name']}" ) if __name__ == "__main__": cli()