зеркало из https://github.com/microsoft/spinnaker.git
593 строки
24 KiB
Python
593 строки
24 KiB
Python
#!/usr/bin/python
|
|
#
|
|
# Copyright 2015 Google Inc. 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.
|
|
|
|
import argparse
|
|
import collections
|
|
import os
|
|
import sys
|
|
|
|
from spinnaker.run import check_run_and_monitor
|
|
from spinnaker.run import check_run_quick
|
|
from spinnaker.run import run_and_monitor
|
|
from spinnaker.run import run_quick
|
|
|
|
|
|
def get_repository_dir(name):
|
|
"""Determine the local directory that a given repository is in.
|
|
|
|
We assume that refresh_source is being run in the build directory
|
|
that contains all the repositories. Except spinnaker/ itself is not
|
|
in the build directory so special case it.
|
|
|
|
Args:
|
|
name [string]: The repository name.
|
|
"""
|
|
if name == 'spinnaker':
|
|
return os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
|
|
else:
|
|
return name
|
|
|
|
|
|
class SourceRepository(
|
|
collections.namedtuple('SourceRepository', ['name', 'owner'])):
|
|
"""Denotes a github repository.
|
|
|
|
Attributes:
|
|
name: The [short] name of the repository.
|
|
owner: The github user name owning the repository
|
|
"""
|
|
pass
|
|
|
|
|
|
class Refresher(object):
|
|
"""Provides branch management capabilities across Spinnaker repositories.
|
|
|
|
The Spinnaker repositories are federated across several independent
|
|
repositories. This class provides convenient support to update local
|
|
repositories from remote and vice-versa.
|
|
|
|
The origin repository is specified using --github_user option. This specifies
|
|
the github repository owner for the origin repositories. It is only relevant
|
|
when a repository needs to be cloned to establish a local repository. The
|
|
value 'upstream' can be used to indicate that the repository should be cloned
|
|
from its authoritative source as opposed to another user's fork.
|
|
|
|
When the refresher clones new repositories, it establishes an "upstream"
|
|
remote to the authoritative repository (based on hard-coded mappings)
|
|
unless the origin is the upstream. Upstream pulls are disabled (including
|
|
when the origin is the upstream) and only the master branch can be pulled
|
|
from upstream.
|
|
|
|
If --pull_branch is used then the local repositories will pull their current
|
|
branch from the origin repository. If a local repository does not yet exist,
|
|
then it will be cloned from the --github_user using the branch specified
|
|
by --pull_branch. The --pull_origin option is similar but implies that the
|
|
branch is 'master'. This is intended to perform complete updates of the
|
|
local repositories.
|
|
|
|
--push_branch (or --push_master, implying 'master' branch) will push the
|
|
local repository branch back to the origin, but only if the local repository
|
|
is in the specified branch. This is for safety to prevent accidental pushes.
|
|
It is assumed that multi-repository changes will have a common feature-branch
|
|
name, and not all repositories will be affected.
|
|
|
|
Of course, individual repositories can still be managed using explicit git
|
|
commands. This class is intended for cross-cutting management.
|
|
"""
|
|
|
|
__OPTIONAL_REPOSITORIES = [SourceRepository('citest', 'google')]
|
|
__REQUIRED_REPOSITORIES = [
|
|
SourceRepository('spinnaker', 'spinnaker'),
|
|
SourceRepository('clouddriver', 'spinnaker'),
|
|
SourceRepository('orca', 'spinnaker'),
|
|
SourceRepository('front50', 'spinnaker'),
|
|
SourceRepository('echo', 'spinnaker'),
|
|
SourceRepository('rosco', 'spinnaker'),
|
|
SourceRepository('gate', 'spinnaker'),
|
|
SourceRepository('fiat', 'spinnaker'),
|
|
SourceRepository('igor', 'spinnaker'),
|
|
SourceRepository('deck', 'spinnaker')]
|
|
|
|
@property
|
|
def pull_branch(self):
|
|
"""Gets the branch that we want to pull.
|
|
|
|
This may raise a ValueError if the specification is inconsistent.
|
|
This is determined lazily rather than at construction to be consistent
|
|
with the push_branch property.
|
|
"""
|
|
if self.__options.pull_origin:
|
|
if (self.__options.pull_branch
|
|
and self.__options.pull_branch != 'master'):
|
|
raise ValueError(
|
|
'--pull_origin is incompatible with --pull_branch={branch}'
|
|
.format(branch=self.__options.pull_branch))
|
|
return 'master'
|
|
return self.__options.pull_branch
|
|
|
|
@property
|
|
def push_branch(self):
|
|
"""Gets the branch that we want to push.
|
|
|
|
This may raise a ValueError if the specification is inconsistent.
|
|
This is determined lazily rather than at construction because the
|
|
option to push is not necessarily present depending on the use case.
|
|
"""
|
|
if self.__options.push_master:
|
|
if (self.__options.push_branch
|
|
and self.__options.push_branch != 'master'):
|
|
raise ValueError(
|
|
'--push_origin is incompatible with --push_branch={branch}'
|
|
.format(branch=self.__options.push_branch))
|
|
return 'master'
|
|
return self.__options.push_branch
|
|
|
|
def __init__(self, options):
|
|
self.__options = options
|
|
self.__extra_repositories = self.__OPTIONAL_REPOSITORIES
|
|
if options.extra_repos:
|
|
for extra in options.extra_repos.split(','):
|
|
pair = extra.split('=')
|
|
if len(pair) != 2:
|
|
raise ValueError(
|
|
'Invalid --extra_repos value "{extra}"'.format(extra=extra))
|
|
self.__extra_repositories.append(SourceRepository(pair[0], pair[1]))
|
|
|
|
def get_remote_repository_url(self, path, which='origin'):
|
|
"""Determine the repository that a given path is from.
|
|
|
|
Args:
|
|
path [string]: The path to the repository
|
|
which [string]: The remote repository name (origin or upstream).
|
|
|
|
Returns:
|
|
The origin url for path, or None if not a git repository.
|
|
"""
|
|
result = run_quick('git -C {path} config --get remote.{which}.url'
|
|
.format(path=path, which=which),
|
|
echo=False)
|
|
if result.returncode:
|
|
return None
|
|
return result.stdout.strip()
|
|
|
|
def get_local_branch_name(self, name):
|
|
"""Determine which git branch a local repository is in.
|
|
|
|
Args:
|
|
name [string]: The repository name.
|
|
|
|
Returns:
|
|
The name of the branch.
|
|
"""
|
|
result = run_quick('git -C "{dir}" rev-parse --abbrev-ref HEAD'
|
|
.format(dir=get_repository_dir(name)),
|
|
echo=False)
|
|
if result.returncode:
|
|
error = 'Could not determine branch: ' + result.stdout.strip()
|
|
raise RuntimeError(error)
|
|
return result.stdout.strip()
|
|
|
|
def get_github_repository_url(self, repository, owner=None):
|
|
"""Determine the URL for a given github repository.
|
|
|
|
Args:
|
|
repository [string]: The upstream SourceRepository.
|
|
owner [string]: The explicit owner for the repository we want.
|
|
If not provided then use the github_user in the bound options.
|
|
"""
|
|
|
|
user = owner or self.__options.github_user
|
|
if not user:
|
|
raise ValueError('No --github_user specified.')
|
|
|
|
if user == 'default' or user == 'upstream':
|
|
user = repository.owner
|
|
url_pattern = ('https://github.com/{user}/{name}.git'
|
|
if self.__options.use_https
|
|
else 'git@github.com:{user}/{name}.git')
|
|
return url_pattern.format(user=user, name=repository.name)
|
|
|
|
def git_clone(self, repository, owner=None):
|
|
"""Clone the specified repository
|
|
|
|
Args:
|
|
repository [string]: The name of the github repository (without owner).
|
|
owner [string]: An explicit repository owner.
|
|
If not provided use the configured options.
|
|
"""
|
|
name = repository.name
|
|
repository_dir = get_repository_dir(name)
|
|
upstream_user = repository.owner
|
|
branch = self.pull_branch or 'master'
|
|
origin_url = self.get_github_repository_url(repository, owner=owner)
|
|
upstream_url = 'https://github.com/{upstream_user}/{name}.git'.format(
|
|
upstream_user=upstream_user, name=name)
|
|
|
|
# Don't echo because we're going to hide some failure.
|
|
print 'Cloning {name} from {origin_url} -b {branch}.'.format(
|
|
name=name, origin_url=origin_url, branch=branch)
|
|
shell_result = run_and_monitor(
|
|
'git clone {url} -b {branch}'.format(url=origin_url, branch=branch),
|
|
echo=False)
|
|
if not shell_result.returncode:
|
|
if shell_result.stdout:
|
|
print shell_result.stdout
|
|
else:
|
|
if repository in self.__extra_repositories:
|
|
sys.stderr.write('WARNING: Missing optional repository {name}.\n'
|
|
.format(name=name))
|
|
sys.stderr.write(' Continue on without it.\n')
|
|
return
|
|
sys.stderr.write(shell_result.stderr or shell_result.stdout)
|
|
sys.stderr.write(
|
|
'FATAL: Cannot continue without required repository {name}.\n'
|
|
' Consider using github to fork one from {upstream}.\n'.
|
|
format(name=name, upstream=upstream_url))
|
|
raise SystemExit('Repository {url} not found.'.format(url=origin_url))
|
|
|
|
if self.__options.add_upstream and origin_url != upstream_url:
|
|
print ' Adding upstream repository {upstream}.'.format(
|
|
upstream=upstream_url)
|
|
check_run_quick('git -C "{dir}" remote add upstream {url}'
|
|
.format(dir=repository_dir, url=upstream_url),
|
|
echo=False)
|
|
|
|
if self.__options.disable_upstream_push:
|
|
which = 'upstream' if origin_url != upstream_url else 'origin'
|
|
print ' Disabling git pushes to {which} {upstream}'.format(
|
|
which=which, upstream=upstream_url)
|
|
check_run_quick(
|
|
'git -C "{dir}" remote set-url --push {which} disabled'
|
|
.format(dir=repository_dir, which=which),
|
|
echo=False)
|
|
|
|
def pull_from_origin(self, repository):
|
|
"""Pulls the current branch from the git origin.
|
|
|
|
Args:
|
|
repository [string]: The local repository to update.
|
|
"""
|
|
name = repository.name
|
|
repository_dir = get_repository_dir(name)
|
|
if not os.path.exists(repository_dir):
|
|
self.git_clone(repository)
|
|
return
|
|
|
|
print 'Updating {name} from origin'.format(name=name)
|
|
branch = self.get_local_branch_name(name)
|
|
if branch != self.pull_branch:
|
|
if self.__options.force_pull:
|
|
sys.stderr.write(
|
|
'WARNING: Updating {name} branch={branch}, *NOT* "{want}"\n'
|
|
.format(name=name, branch=branch, want=self.pull_branch))
|
|
else:
|
|
sys.stderr.write(
|
|
'WARNING: Skipping {name} because branch={branch},'
|
|
' *NOT* "{want}"\n'
|
|
.format(name=name, branch=branch, want=self.pull_branch))
|
|
return
|
|
try:
|
|
check_run_and_monitor('git -C "{dir}" pull origin {branch} --tags'
|
|
.format(dir=repository_dir, branch=branch),
|
|
echo=True)
|
|
except RuntimeError:
|
|
result = check_run_and_monitor('git -C "{dir}" branch -r'
|
|
.format(dir=repository_dir),
|
|
echo=False)
|
|
if result.stdout.find('origin/{branch}\n') >= 0:
|
|
raise
|
|
sys.stderr.write(
|
|
'WARNING {name} branch={branch} is not known to the origin.\n'
|
|
.format(name=name, branch=branch))
|
|
|
|
def pull_from_upstream_if_master(self, repository):
|
|
"""Pulls the master branch from the upstream repository.
|
|
|
|
This will only have effect if the local repository exists
|
|
and is currently in the master branch.
|
|
|
|
Args:
|
|
repository [string]: The name of the local repository to update.
|
|
"""
|
|
name = repository.name
|
|
repository_dir = get_repository_dir(name)
|
|
if not os.path.exists(repository_dir):
|
|
self.pull_from_origin(repository)
|
|
branch = self.get_local_branch_name(name)
|
|
if branch != 'master':
|
|
sys.stderr.write('Skipping {name} because it is in branch={branch}.\n'
|
|
.format(name=name, branch=branch))
|
|
return
|
|
|
|
print 'Pulling master {name} from upstream'.format(name=name)
|
|
check_run_and_monitor('git -C "{dir}" pull upstream master --tags'
|
|
.format(dir=repository_dir),
|
|
echo=True)
|
|
|
|
def push_to_origin_if_target_branch(self, repository):
|
|
"""Pushes the current target branch of the local repository to the origin.
|
|
|
|
This will only have effect if the local repository exists
|
|
and is currently in the target branch.
|
|
|
|
Args:
|
|
repository [string]: The name of the local repository to push from.
|
|
"""
|
|
name = repository.name
|
|
repository_dir = get_repository_dir(name)
|
|
if not os.path.exists(repository_dir):
|
|
sys.stderr.write('Skipping {name} because it does not yet exist.\n'
|
|
.format(name=name))
|
|
return
|
|
|
|
branch = self.get_local_branch_name(name)
|
|
if branch != self.push_branch:
|
|
sys.stderr.write(
|
|
'Skipping {name} because it is in branch={branch}, not {want}.\n'
|
|
.format(name=name, branch=branch, want=self.push_branch))
|
|
return
|
|
|
|
print 'Pushing {name} to origin.'.format(name=name)
|
|
check_run_and_monitor('git -C "{dir}" push origin {branch} --tags'.format(
|
|
dir=repository_dir, branch=self.push_branch),
|
|
echo=True)
|
|
|
|
def push_all_to_origin_if_target_branch(self):
|
|
"""Push all the local repositories current target branch to origin.
|
|
|
|
This will skip any local repositories that are not currently in the
|
|
target branch.
|
|
"""
|
|
all_repos = self.__REQUIRED_REPOSITORIES + self.__extra_repositories
|
|
for repository in all_repos:
|
|
self.push_to_origin_if_target_branch(repository)
|
|
|
|
def pull_all_from_upstream_if_master(self):
|
|
"""Pull all the upstream master branches into their local repository.
|
|
|
|
This will skip any local repositories that are not currently in the master
|
|
branch.
|
|
"""
|
|
all_repos = self.__REQUIRED_REPOSITORIES + self.__extra_repositories
|
|
for repository in all_repos:
|
|
self.pull_from_upstream_if_master(repository)
|
|
|
|
def pull_all_from_origin(self):
|
|
"""Pull all the origin target branches into their local repository.
|
|
|
|
This will skip any local repositories that are not currently in the
|
|
target branch.
|
|
"""
|
|
all_repos = self.__REQUIRED_REPOSITORIES + self.__extra_repositories
|
|
for repository in all_repos:
|
|
try:
|
|
self.pull_from_origin(repository)
|
|
except RuntimeError as ex:
|
|
if repository in self.__extra_repositories and not os.path.exists(
|
|
get_repository_dir(repository)):
|
|
sys.stderr.write(
|
|
'IGNORING error "{msg}" in optional repository {name}'
|
|
' because the local repository does not yet exist.\n'
|
|
.format(msg=ex.message, name=repository.name))
|
|
else:
|
|
raise
|
|
|
|
def __determine_spring_config_location(self):
|
|
root = '{dir}/config'.format(
|
|
dir=os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
|
home = os.path.join(os.environ['HOME'] + '/.spinnaker')
|
|
return '{root}/,{home}/'.format(home=home, root=root)
|
|
|
|
def write_gradle_run_script(self, repository):
|
|
"""Generate a dev_run.sh script for the local repository.
|
|
|
|
Args:
|
|
repository [string]: The name of the local repository to generate in.
|
|
"""
|
|
name = repository.name
|
|
path = '{name}/start_dev.sh'.format(name=name)
|
|
|
|
with open(path, 'w') as f:
|
|
f.write("""#!/bin/bash
|
|
d=$(dirname "$0")
|
|
cd "$d"
|
|
LOG_DIR=${{LOG_DIR:-../logs}}
|
|
|
|
DEF_SYS_PROPERTIES="-Dspring.config.location='{spring_location}'"
|
|
bash -c "(./gradlew $DEF_SYS_PROPERTIES $@ > '$LOG_DIR/{name}.log') 2>&1\
|
|
| tee -a '$LOG_DIR/{name}.log' >& '$LOG_DIR/{name}.err' &"
|
|
""".format(name=name,
|
|
spring_location=self.__determine_spring_config_location()))
|
|
os.chmod(path, 0777)
|
|
|
|
def write_deck_run_script(self, repository):
|
|
"""Generate a dev_run.sh script for running deck locally.
|
|
|
|
Args:
|
|
repository [string]: The name of the local repository to generate in.
|
|
"""
|
|
name = repository.name
|
|
path = '{name}/start_dev.sh'.format(name=name)
|
|
with open(path, 'w') as f:
|
|
f.write("""#!/bin/bash
|
|
d=$(dirname "$0")
|
|
cd "$d"
|
|
LOG_DIR=${{LOG_DIR:-../logs}}
|
|
|
|
if [[ node_modules -ot .git ]]; then
|
|
# Update npm, otherwise assume nothing changed and we're good.
|
|
npm install >& "$LOG_DIR/deck.log"
|
|
else
|
|
echo "deck npm node_modules looks up to date already."
|
|
fi
|
|
|
|
# Append to the log file we just started.
|
|
bash -c "(npm start >> '$LOG_DIR/{name}.log') 2>&1\
|
|
| tee -a '$LOG_DIR/{name}.log' >& '$LOG_DIR/{name}.err' &"
|
|
""".format(name=name))
|
|
os.chmod(path, 0777)
|
|
|
|
def update_spinnaker_run_scripts(self):
|
|
"""Regenerate the local dev_run.sh script for each local repository."""
|
|
for repository in self.__REQUIRED_REPOSITORIES:
|
|
name = repository.name
|
|
if not os.path.exists(name):
|
|
continue
|
|
|
|
if name == 'deck':
|
|
self.write_deck_run_script(repository)
|
|
else:
|
|
self.write_gradle_run_script(repository)
|
|
|
|
@classmethod
|
|
def init_extra_argument_parser(cls, parser):
|
|
"""Initialize additional arguments for managing remote repositories.
|
|
|
|
This is to sync the origin and upstream repositories. The intent
|
|
is to ultimately sync the origin from the upstream repository, but
|
|
this might be in two steps so the upstream can be verified [again]
|
|
before pushing the changes to the origin.
|
|
"""
|
|
|
|
# Note that we only pull the master branch from upstream.
|
|
# Pulling other branches don't normally make sense.
|
|
parser.add_argument('--pull_upstream', default=False,
|
|
action='store_true',
|
|
help='If the local branch is master, then refresh it'
|
|
' from the upstream repository.'
|
|
' Otherwise leave as is.')
|
|
parser.add_argument('--nopull_upstream',
|
|
dest='pull_upstream',
|
|
action='store_false')
|
|
|
|
# Note we only push target branches to origin specified by --push_branch
|
|
# To push another branch, you must explicitly push it with git
|
|
# (or another invocation).
|
|
parser.add_argument('--push_master', action='store_true',
|
|
help='Push the current branch to origin if it is'
|
|
' master. This is the same as --push_branch=master.')
|
|
parser.add_argument('--nopush_master', dest='push_master',
|
|
action='store_false')
|
|
|
|
parser.add_argument('--push_branch', default='',
|
|
help='If specified and the local repository is in'
|
|
' this branch then push it to the origin'
|
|
' repository. Otherwise do not push it.')
|
|
|
|
@classmethod
|
|
def init_argument_parser(cls, parser):
|
|
"""Initialize command-line arguments."""
|
|
parser.add_argument('--use_https', default=True, action='store_true',
|
|
help='Use https when cloning github repositories.')
|
|
parser.add_argument('--use_ssh', dest='use_https', action='store_false',
|
|
help='Use SSH when cloning github repositories.')
|
|
|
|
parser.add_argument('--add_upstream', default=True,
|
|
action='store_true',
|
|
help='Add upstream repository when cloning.')
|
|
parser.add_argument('--noadd_upstream', dest='add_upstream',
|
|
action='store_false')
|
|
|
|
parser.add_argument('--disable_upstream_push', default=True,
|
|
action='store_true',
|
|
help='Disable future pushes to the upstream'
|
|
' repository when cloning a repository.')
|
|
parser.add_argument('--nodisable_upstream_push',
|
|
dest='disable_upstream_push',
|
|
action='store_false')
|
|
|
|
parser.add_argument('--pull_origin', default=False,
|
|
action='store_true',
|
|
help='Refresh the local branch from the origin.'
|
|
' If cloning, then clone the master branch.'
|
|
' See --pull_branch for a more general option.')
|
|
parser.add_argument('--nopull_origin', dest='pull_origin',
|
|
action='store_false')
|
|
|
|
parser.add_argument('--pull_branch', default='',
|
|
help='Refresh the local branch from the origin if'
|
|
' it is in the specified branch,'
|
|
' otherwise skip it.'
|
|
' If cloning, then clone this branch.')
|
|
|
|
parser.add_argument('--force_pull', default=False,
|
|
help='Force pulls, even if the current branch'
|
|
' differs from the pulled branch.')
|
|
|
|
parser.add_argument(
|
|
'--extra_repos', default=None,
|
|
help='A comma-delimited list of name=owner optional repositories.'
|
|
'name is the repository name,'
|
|
' owner is the authoritative github user name owning it.'
|
|
' The --github_user will still be used to determine the origin.')
|
|
|
|
parser.add_argument('--github_user', default=None,
|
|
help='Pull from this github user\'s repositories.'
|
|
' If the user is "default" then use the'
|
|
' authoritative (upstream) repository.')
|
|
|
|
@classmethod
|
|
def main(cls):
|
|
parser = argparse.ArgumentParser()
|
|
cls.init_argument_parser(parser)
|
|
cls.init_extra_argument_parser(parser)
|
|
options = parser.parse_args()
|
|
|
|
refresher = cls(options)
|
|
in_repository_url = refresher.get_remote_repository_url('.')
|
|
if in_repository_url:
|
|
sys.stderr.write(
|
|
'ERROR: You cannot run this script from within a local repository.\n'
|
|
' This directory is from "{url}".\n'
|
|
' Did you intend to be in the parent directory?\n'
|
|
.format(url=in_repository_url))
|
|
return -1
|
|
|
|
try:
|
|
# This is ok. Really we want to look for an exception validating these
|
|
# properties so we can fail with a friendly error rather than stack.
|
|
if (refresher.pull_branch != refresher.push_branch
|
|
and refresher.pull_branch and refresher.push_branch):
|
|
sys.stderr.write(
|
|
'WARNING: pulling branch {pull} and pushing branch {push}'
|
|
.format(pull=refresher.pull_branch,
|
|
push=refresher.push_branch))
|
|
except Exception as ex:
|
|
sys.stderr.write('FAILURE: {0}\n'.format(ex.message))
|
|
return -1
|
|
|
|
nothing = True
|
|
if options.pull_upstream:
|
|
nothing = False
|
|
refresher.pull_all_from_upstream_if_master()
|
|
if refresher.push_branch:
|
|
nothing = False
|
|
refresher.push_all_to_origin_if_target_branch()
|
|
if refresher.pull_branch:
|
|
nothing = False
|
|
refresher.pull_all_from_origin()
|
|
refresher.update_spinnaker_run_scripts()
|
|
|
|
if nothing:
|
|
sys.stderr.write('No pull/push options were specified.\n')
|
|
else:
|
|
print 'DONE'
|
|
return 0
|
|
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(Refresher.main())
|