blob: b149c1aa3faca1155e2f9faae50219ad061c074f [file] [log] [blame]
#!/usr/bin/env python
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You 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 os
import sys
import platform
import argparse
import shlex
import subprocess
import re
# the default openwhisk location in openwhisk checkouts
defaultOpenwhisk = os.path.dirname(os.path.realpath(__file__)) + '/../../'
# the openwhisk env var overrides if it exists
whiskHome = os.getenv('WHISK_HOME', defaultOpenwhisk)
def main():
args = getArgs()
if (not args.build and
not args.teardown and
not args.deploy):
args.build = True
args.teardown = True
args.deploy = True
# args.dir is either explicitly set or default to openwhisk home per environment
wskhome = args.dir
# change to wsk home to use build/deploy scripts
os.chdir(wskhome)
props = {}
props['ENV'] = args.target
props['WSK_HOME'] = wskhome
props['MAIN_DOCKER_ENDPOINT'] = getDockerHost()
doComponentSequence(props, args, args.components)
def doComponentSequence(props, args, components):
for c in components:
component = getComponent(c)
if not component:
if args.yaml:
file = c if c.endswith('.yml') else '%s.yml' % c
component = makeComponent('custom deployment', 'deploying using %s' % file, yaml = file, modes = 'clean')
elif args.gradle:
file = c if c.endswith('.gradle') else '%s.gradle' % c
component = makeComponent('custom build target', 'building using %s' % file, yaml = False, gradle = True, tasks = c)
else:
print('unknown component %s' % c)
exit(1)
if component['steps']:
doComponentSequence(props, args, component['steps'])
else:
doOne(component, args, props)
def getArgs():
def detectDeployTarget():
osname = platform.system()
if osname == 'Linux':
return 'local'
elif osname == 'Darwin':
if os.getenv('DOCKER_HOST', None) is not None:
# docker-machine typically has docker host set in the environment
return 'docker-machine'
else:
# otherwise assume docker-for-mac
return 'local'
else:
return None
parser = argparse.ArgumentParser(description='[re]build and [re]deploy a whisk component if no args are given, otherwise do what is instructed')
parser.add_argument('-b', '--build', help='build component', action='store_const', const=True, default=False)
parser.add_argument('-x', '--teardown', help='teardown component', action='store_const', const=True, default=False)
parser.add_argument('-d', '--deploy', help='deploy component', action='store_const', const=True, default=False)
parser.add_argument('-t', '--target', help='deploy target (one of [docker-machine, local])', default=detectDeployTarget())
parser.add_argument('-y', '--yaml', help='deploy target using inferred YAML file if component is not one of known targets', action='store_const', const=True, default=False)
parser.add_argument('-g', '--gradle', help='use target using inferred gradle file if component is not one of known targets', action='store_const', const=True, default=False)
parser.add_argument('-n', '--just-print', help='prints the component configuration but does not run any targets', action='store_const', const=True, default=False, dest='skiprun')
parser.add_argument('-c', '--list-components', help='list known component names and exit', action='store_const', const=True, default=False, dest='list')
parser.add_argument('-a', '--additional-task-arguments', dest='extraArgs', action='append', help='pass additional arguments to gradle build')
parser.add_argument('-e', '--extra-ansible-vars', dest='extraAnsibleVars', action='append', help='pass extra vars to ansible-playbook')
parser.add_argument('components', nargs = '*', help='component name(s) to run (in order specified if more than one)')
parser.add_argument('--dir', help='whisk home directory')
args = parser.parse_args()
if args.target is None:
print('Use "--target" to specify a deployment target because one '
'could not be determined automatically for your platform '
'(supported platforms are GNU/Linux (Ubuntu) and Mac OS X.')
exit(-1)
if args.dir is None:
if whiskHome is None:
print('Must specify whisk home directory with "--dir".')
exit(-1)
else:
args.dir = whiskHome
if args.list:
print("{:<27}{:<40}".format(bold('component'), bold('description')))
for c in Components:
print("{:<30}{:<40}".format(hilite(c['name']), c['description']))
exit(0)
elif not args.components:
parser.print_usage()
exit(0)
else:
return args
class Playbook:
cmd = 'ansible-playbook'
dir = False
file = False
modes = False
env = False
# dir: the ansible directory containing the yaml files, roles, etc.
# file: the yml file for the playbook
# modes: the modes supported for the playbook as a comma separated string (e.g., 'clean')
# env: the environments directory (e.g., 'environment) or None if not environment specific playbook
def __init__(self, dir, file, modes, env):
self.dir = dir
self.file = file
self.modes = modes.split(',')
self.env = env
def path(self, basedir):
return basedir + '/' + self.dir
def execcmd(self, props, mode = False, extraAnsibleVars = []):
if self.dir and self.file and (mode is False or mode in self.modes):
cmd = [ self.cmd ]
if self.env:
cmd.append('-i %s/%s' % (self.env, props['ENV']))
cmd.append(self.file)
if mode:
cmd.append('-e mode=%s' % mode)
if extraAnsibleVars:
cmd.append(' '.join(map(lambda x: "-e '" + str(x) + "'", extraAnsibleVars)))
return ' '.join(cmd)
class Gradle:
cmd = 'gradlew'
tasks = False
components = False
def __init__(self, tasks, components = False):
self.tasks = tasks.split(',')
self.components = components
def execcmd(self, props, task, extraArgs = ''):
if task:
if self.components and self.components is not True:
parts = map(lambda c: '%s:%s' % (c, task), self.components.split(','))
parts = ' '.join(parts)
else:
parts = task
dh = props['MAIN_DOCKER_ENDPOINT']
return '%s %s %s --parallel %s' % (
props['WSK_HOME'] + '/' + self.cmd,
parts,
extraArgs,
('-PdockerHost=%s' % dh) if dh else '')
def getDockerHost():
dh = os.getenv('DOCKER_HOST')
if dh is not None and dh.startswith('tcp://'):
return dh[6:]
def makeComponent(name, # component name, implies playbook default and gradle tasks roots
description,
yaml = True, # true for default file name else the file name
modes = '',
env = 'environments',
dir = 'ansible',
gradle = False, # gradle buildable iff true
tasks = 'distDocker',
steps = None): # comma separated, runs these steps in sequence, each step is a reference to another component (yaml/gradle not allowed)
yaml = ('%s.yml' % name) if yaml is True else yaml
playbook = Playbook(dir, yaml, modes, env) if yaml is not False else None
gradle = Gradle(tasks, gradle) if gradle is not False else None
if steps and (playbook is not None or gradle is not None):
print('Cannot create component "%s" with a sequence of steps and also '
'a playbook with gradle build target' % name)
exit(-1)
elif steps:
steps = map(lambda c: c.strip(), steps.split(','))
return { 'name': name, 'description': description, 'playbook': playbook, 'gradle': gradle, 'steps': steps }
Components = [
makeComponent('fresh',
'setup, build, and deploy a fresh whisk system using couchdb',
yaml = False,
steps = 'setup, couchdb, initdb, wipedb, deploy, catalog'),
makeComponent('fmt',
'apply source code formats',
gradle = True,
yaml = False,
tasks = 'scalafmtAll'),
makeComponent('setup',
'system setup'),
makeComponent('prereq',
'install requisites'),
makeComponent('couchdb',
'deploy couchdb',
modes = 'clean'),
makeComponent('initdb',
'initialize db with guest/system keys'),
makeComponent('wipedb',
'recreate main db for entities',
yaml = 'wipe.yml'),
makeComponent('elasticsearch',
'deploy elasticsearch',
modes = 'clean'),
makeComponent('mongodb',
'deploy mongodb',
modes = 'clean'),
makeComponent('initMongoDB',
'initialize mongodb with guest/system keys'),
makeComponent('build',
'build system',
yaml = False,
gradle = True),
makeComponent('deploy',
'build/deploy system',
yaml = 'openwhisk.yml',
modes = 'clean',
gradle = True),
makeComponent('teardown',
'teardown all deployed containers',
yaml = 'teardown.yml'),
makeComponent('kafka',
'build/deploy kafka',
modes = 'clean'),
makeComponent('controller',
'build/deploy controller',
modes = 'clean',
gradle = 'core:controller'),
makeComponent('invoker',
'build/deploy invoker',
modes = 'clean',
gradle = ':core:invoker'),
makeComponent('edge',
'deploy edge'),
makeComponent('cli',
'download cli from api host',
modes = 'clean',
yaml = 'downloadcli.yml'),
makeComponent('catalog',
'install catalog',
yaml = 'postdeploy.yml'),
makeComponent('apigw',
'deploy api gateway',
gradle = False,
modes = 'clean',
yaml = 'routemgmt.yml apigateway.yml'),
# the following (re)build images via gradle
makeComponent('runtime:([\w.-]+)',
'build a runtime action container, matching name using the regex; NOTE: must use --dir for path to runtime directory',
yaml = False,
gradle = 'core:$1:distDocker'),
makeComponent('actionproxy',
'build action proxy container',
yaml = False,
gradle = 'tools:actionProxy'),
# required for tests
makeComponent('props',
'build whisk.properties file (required for tests)',
yaml = 'properties.yml'),
# convenient to run all tests
makeComponent('tests',
'run all tests',
yaml = False,
gradle = True,
tasks = 'test'),
makeComponent('unit-tests',
'run units tests',
yaml = False,
tasks = 'testUnit',
gradle = 'tests'),
makeComponent('standalone',
'run standalone server',
yaml = False,
tasks = 'bootRun',
gradle = 'core:standalone')
]
def getComponent(component):
for c in Components:
if c['name'] == component:
return c
else:
parts = re.match(c['name'], component)
if parts:
name = parts.group(1)
return makeComponent('runtime:' + name,
'build a ' + name + ' runtime action container',
yaml = False,
gradle = 'core:' + name)
return False
def bold(string):
if sys.stdin.isatty():
attr = []
attr.append('1')
return '\x1b[%sm%s\x1b[0m' % (';'.join(attr), string)
else:
return string
def hilite(string, isError = False):
if sys.stdin.isatty():
attr = []
attr.append('34' if not isError else '31') # blue or red if isError
attr.append('1')
return '\x1b[%sm%s\x1b[0m' % (';'.join(attr), string)
else:
return string
def run(cmd, dir, skiprun, allowToFail = False):
if cmd is not None:
print(hilite(cmd))
if not skiprun:
args = shlex.split(cmd)
p = subprocess.Popen(args, cwd = dir)
p.wait()
if p.returncode and not allowToFail:
abort('command failed', p.returncode)
def runAndGetStdout(cmd):
print(hilite(cmd))
args = shlex.split(cmd)
p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = p.communicate()
# stdout/stderr may be either text or bytes, depending on Python
# version. In the latter case, decode to text.
if isinstance(out, bytes):
out = out.decode('utf-8')
if isinstance(err, bytes):
err = err.decode('utf-8')
if p.returncode:
print(hilite(out))
print(hilite(err, True))
abort('command failed', p.returncode)
return out
def abort(msg, code = -1):
print(hilite(msg, True))
exit(code)
def doOne(component, args, props):
basedir = props['WSK_HOME']
playbook = component['playbook']
gradle = component['gradle']
print(bold(component['description']))
extraArgs = '' if args.extraArgs is None or [] else ' '.join(map(str, args.extraArgs))
if args.build and gradle is not None:
cmd = gradle.execcmd(props, gradle.tasks[0], extraArgs)
run(cmd, basedir, args.skiprun)
if args.teardown and playbook is not None:
cmd = playbook.execcmd(props, 'clean', extraAnsibleVars = args.extraAnsibleVars)
run(cmd, playbook.path(basedir), args.skiprun)
if args.deploy and playbook is not None:
cmd = playbook.execcmd(props, extraAnsibleVars = args.extraAnsibleVars)
run(cmd, playbook.path(basedir), args.skiprun)
if __name__ == '__main__':
main()