# Copyright 2020 The Nomulus 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. """Script to rollback the Nomulus server on AppEngine.""" import argparse import dataclasses import sys import textwrap from typing import Any, Optional, Tuple import appengine import gcs import plan MAIN_HELP = 'Script to roll back the Nomulus server on AppEngine.' ROLLBACK_HELP = 'Rolls back Nomulus to the target release.' GET_SERVING_RELEASE_HELP = 'Shows the release tag(s) of the serving versions.' GET_RECENT_DEPLOYMENTS_HELP = ('Shows recently deployed versions and their ' 'release tags.') ROLLBACK_MODE_HELP = textwrap.dedent("""\ The execution mode. - dryrun: Prints descriptions of all steps. - interactive: Prompts for confirmation before executing each step. - auto: Executes all steps in one go. """) @dataclasses.dataclass(frozen=True) class Argument: """Describes a command line argument. This class is for use with argparse.ArgumentParser. Except for the 'arg_names' attribute which specifies the argument name and/or flags, all other attributes must match an accepted parameter in the parser's add_argument() method. """ arg_names: Tuple[str, ...] help: str default: Optional[Any] = None required: bool = True choices: Optional[Tuple[str, ...]] = None def get_arg_attrs(self): return dict((k, v) for k, v in vars(self).items() if k != 'arg_names') ARGUMENTS = (Argument(('--dev_project', '-d'), 'The GCP project with Nomulus deployment records.'), Argument(('--project', '-p'), 'The GCP project where the Nomulus server is deployed.'), Argument(('--env', '-e'), 'The name of the Nomulus server environment.', choices=('production', 'sandbox', 'crash', 'alpha'))) ROLLBACK_ARGUMENTS = (Argument(('--target_release', '-t'), 'The release to be deployed.'), Argument(('--run_mode', '-m'), ROLLBACK_MODE_HELP, required=False, default='dryrun', choices=('dryrun', 'interactive', 'auto'))) def rollback(dev_project: str, project: str, env: str, target_release: str, run_mode: str) -> None: """Rolls back a Nomulus server to the target release. Args: dev_project: The GCP project with deployment records. project: The GCP project of the Nomulus server. env: The environment name of the Nomulus server. target_release: The tag of the release to be brought up. run_mode: How to handle the rollback steps: print-only (dryrun) one step at a time with user confirmation (interactive), or all steps in one shot (automatic). """ steps = plan.get_rollback_plan(gcs.GcsClient(dev_project), appengine.AppEngineAdmin(project), env, target_release) print('Rollback steps:\n\n') for step in steps: print(f'{step.info()}\n') if run_mode == 'dryrun': continue if run_mode == 'interactive': confirmation = input( 'Do you wish to (c)ontinue, (s)kip, or (a)bort? ') if confirmation == 'a': return if confirmation == 's': continue step.execute() def show_serving_release(dev_project: str, project: str, env: str) -> None: """Shows the release tag(s) of the currently serving versions.""" serving_versions = appengine.AppEngineAdmin(project).get_serving_versions() versions_to_tags = gcs.GcsClient(dev_project).get_releases_by_versions( env, serving_versions) print(f'{project}:') for version, tag in versions_to_tags.items(): print(f'{version.service_id}\t{version.version_id}\t{tag}') def show_recent_deployments(dev_project: str, project: str, env: str) -> None: """Show release and version of recent deployments.""" num_services = len(appengine.SERVICES) num_records = 3 * num_services print(f'{project}:') for version, tag in gcs.GcsClient(dev_project).get_recent_deployments( env, num_records).items(): print(f'{version.service_id}\t{version.version_id}\t{tag}') def main() -> int: parser = argparse.ArgumentParser(prog='nom_rollback', description=MAIN_HELP) subparsers = parser.add_subparsers(dest='command', help='Supported commands') rollback_parser = subparsers.add_parser( 'rollback', help=ROLLBACK_HELP, formatter_class=argparse.RawTextHelpFormatter) for flag in ARGUMENTS: rollback_parser.add_argument(*flag.arg_names, **flag.get_arg_attrs()) for flag in ROLLBACK_ARGUMENTS: rollback_parser.add_argument(*flag.arg_names, **flag.get_arg_attrs()) show_serving_release_parser = subparsers.add_parser( 'show_serving_release', help=GET_SERVING_RELEASE_HELP) for flag in ARGUMENTS: show_serving_release_parser.add_argument(*flag.arg_names, **flag.get_arg_attrs()) show_recent_deployments_parser = subparsers.add_parser( 'show_recent_deployments', help=GET_RECENT_DEPLOYMENTS_HELP) for flag in ARGUMENTS: show_recent_deployments_parser.add_argument(*flag.arg_names, **flag.get_arg_attrs()) args = parser.parse_args() command = args.command args = {k: v for k, v in vars(args).items() if k != 'command'} { 'rollback': rollback, 'show_recent_deployments': show_recent_deployments, 'show_serving_release': show_serving_release }[command](**args) return 0 if __name__ == '__main__': try: sys.exit(main()) except Exception as ex: # pylint: disable=broad-except print(ex) sys.exit(1)