"""Revert S3 objects by copying previous version to latest.""" import os import boto3 from botocore.exceptions import ClientError """ Use these naming conventions for consistency with Cloudfront and other downstream processes """ APP_NAME = os.environ.get("APP_NAME") ENV = os.environ.get("ENV") ROLLBACK_COUNT = int(os.environ.get("NUMBER_OF_DEPLOYMENTS_TO_ROLLBACK", 1)) CUSTOM_BUCKET = os.environ.get("S3_BUCKET") # This covers both SPA and legacy patterns S3_OBJECTS_TO_REVERT = [ "index.html", "index-awal.html", "index-sme.html", "main-index.html", # frontend-workstation shell entry point, not hashed so needs rollback "main.dml.js", "main.dml.js.map", # source map for the above "manifest", "manifest.json", "dml.json", ] """ These replicate changes to failover CDN buckets, so we should only directly make changes to these canonical buckets. """ if CUSTOM_BUCKET: S3_BUCKETS = [CUSTOM_BUCKET] elif ENV == "qa": S3_BUCKETS = ["qa-orcd-cdn"] elif ENV == "uat": S3_BUCKETS = ["uat-orcd-cdn"] elif ENV == "prod": S3_BUCKETS = ["prod-orcd-cdn"] else: raise SystemExit("Environment not valid: please specify qa, uat, or prod") def revert_object(client, bucket, object_key, account_id): """Revert an S3 object to its immediately preceding ($CURRENT - 1) version. Args: client (obj): a boto3 S3 client bucket (str): an S3 bucket object_key (str): an S3 object key account_id (str): AWS account ID for bucket ownership verification Returns: str: Version ID of new object """ try: # ensure object exists and not deleted try: client.get_object( Bucket=bucket, Key=object_key, ExpectedBucketOwner=account_id, ) except ClientError as error: if error.response["Error"]["Code"] in ("NoSuchKey", "404"): print(f"{object_key} does not exist in {bucket}, skipping") return None raise object_versions = client.list_object_versions( Bucket=bucket, MaxKeys=10, Prefix=object_key, ExpectedBucketOwner=account_id, ) # Check that object has enough versions to roll back to if ( "Versions" in object_versions and len(object_versions["Versions"]) > ROLLBACK_COUNT ): current_object_key = object_versions["Versions"][0]["Key"] # Check that object key matches the object key from the response # see SYS-22297 for more details if object_key != current_object_key: print(f"{object_key} doesn't match {current_object_key}") return None current_version_id = object_versions["Versions"][0]["VersionId"] previous_version_id = object_versions["Versions"][ROLLBACK_COUNT][ "VersionId" ] else: print( f"{object_key} not found in {bucket} or " "does not have multiple versions" ) return None print( f"Reverting from object version {current_version_id} " f"to {previous_version_id}" ) revert_response = client.copy_object( ACL="private", Bucket=bucket, CopySource={ "Bucket": bucket, "Key": object_key, "VersionId": previous_version_id, }, Key=object_key, MetadataDirective="COPY", TaggingDirective="COPY", ServerSideEncryption="AES256", ExpectedBucketOwner=account_id, ) print(f"Revert successful for object {object_key}") except ClientError as error: print(error.response["Error"]["Message"]) raise SystemExit(f"Error reverting {object_key} in {bucket}") return revert_response["VersionId"] def main(): """Run the frontend rollback process.""" assert APP_NAME, "Environment variable APP_NAME must be set" client = boto3.client("s3") account_id = ( os.environ.get("AWS_ACCOUNT_ID") or boto3.client("sts").get_caller_identity()["Account"] ) for bucket in S3_BUCKETS: prefix = f"{APP_NAME}/" paginator = client.get_paginator("list_objects_v2") prefix_check = client.list_objects_v2( Bucket=bucket, Prefix=prefix, MaxKeys=1, ExpectedBucketOwner=account_id, ) if prefix_check.get("KeyCount", 0) == 0: raise SystemExit( f"No objects found under {prefix} in {bucket}. " f"Is APP_NAME correct?" ) has_index_html = False for page in paginator.paginate( Bucket=bucket, Prefix=f"{APP_NAME}/index", ExpectedBucketOwner=account_id, ): if any( obj["Key"].endswith(".html") for obj in page.get("Contents", []) ): has_index_html = True break if not has_index_html: raise SystemExit( f"No index*.html found under {APP_NAME}/index in {bucket}. " f"Does not look like a deployed frontend app." ) has_js = False for page in paginator.paginate( Bucket=bucket, Prefix=prefix, ExpectedBucketOwner=account_id ): if any( obj["Key"].endswith(".js") for obj in page.get("Contents", []) ): has_js = True break if not has_js: raise SystemExit( f"No .js files found under {prefix} in {bucket}. " f"Does not look like a deployed frontend app." ) for s3_object in S3_OBJECTS_TO_REVERT: object_key = "{}/{}".format(APP_NAME, s3_object) new_version = revert_object(client, bucket, object_key, account_id) print(f"Object is {s3_object} and new version is {new_version}") if __name__ == "__main__": main()