mirror of
https://github.com/google/nomulus.git
synced 2025-04-29 19:47:51 +02:00
* An automated rollback tool for Nomulus A tool that directs traffic between deployed versions. It handles the conversion between Nomulus tags and AppEngine versions, executes schema compatibility tests, ensures that steps are executed in the correct order, and updates deployment records appropriately.
178 lines
6.6 KiB
Python
178 lines
6.6 KiB
Python
# 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)
|