Added recursive option

Now you can organize your yaml files into subdirectories and apply them
all just passing --recursive option or adding the proper configuration
file option.

In adding support for multiple paths the Builder.load_files method was
reworked to be backward compatible with individual paths or file-like
objects, while also now handling being passed a list of paths or
file-like objects to be parsed.

Change-Id: I126751e347622716c592c6ed1a57b8acb7bf79a4
This commit is contained in:
David Caro 2014-04-03 18:42:31 +02:00 committed by Darragh Bailey
parent df37b9b7c7
commit a70126cd94
5 changed files with 100 additions and 20 deletions

View File

@ -70,6 +70,10 @@ job_builder section
job builder will search for any files specified by the custom application
yaml tags 'include', 'include-raw' and 'include-raw-escaped'.
**recursive**
(Optional) If set to True, jenkins job builder will search for job
definition files recursively
jenkins section
^^^^^^^^^^^^^^^

View File

@ -2,6 +2,7 @@
ignore_cache=True
keep_descriptions=False
include_path=.:scripts:~/git/
recursive=False
[jenkins]
user=jenkins

View File

@ -559,20 +559,37 @@ class Builder(object):
def load_files(self, fn):
self.parser = YamlParser(self.global_config)
if hasattr(fn, 'read'):
self.parser.parse_fp(fn)
return
# handle deprecated behavior
if not hasattr(fn, '__iter__'):
logger.warning(
'Passing single elements for the `fn` argument in '
'Builder.load_files is deprecated. Please update your code '
'to use a list as support for automatic conversion will be '
'removed in a future version.')
fn = [fn]
if os.path.isdir(fn):
files_to_process = [os.path.join(fn, f)
for f in os.listdir(fn)
if (f.endswith('.yml') or f.endswith('.yaml'))]
else:
files_to_process = [fn]
files_to_process = []
for path in fn:
if os.path.isdir(path):
files_to_process.extend([os.path.join(path, f)
for f in os.listdir(path)
if (f.endswith('.yml')
or f.endswith('.yaml'))])
else:
files_to_process.append(path)
for in_file in files_to_process:
logger.debug("Parsing YAML file {0}".format(in_file))
self.parser.parse(in_file)
# use of ask-for-permissions instead of ask-for-forgiveness
# performs better when low use cases.
if hasattr(in_file, 'name'):
fname = in_file.name
else:
fname = in_file
logger.debug("Parsing YAML file {0}".format(fname))
if hasattr(in_file, 'read'):
self.parser.parse_fp(in_file)
else:
self.parser.parse(in_file)
def delete_old_managed(self, keep):
jobs = self.jenkins.get_jobs()

View File

@ -31,6 +31,7 @@ DEFAULT_CONF = """
[job_builder]
keep_descriptions=False
ignore_cache=False
recursive=False
[jenkins]
url=http://localhost:8080/
@ -45,18 +46,32 @@ def confirm(question):
sys.exit('Aborted')
def recurse_path(root):
basepath = os.path.realpath(root)
pathlist = [basepath]
for root, dirs, files in os.walk(basepath, topdown=True):
pathlist.extend([os.path.join(root, path) for path in dirs])
return pathlist
def create_parser():
parser = argparse.ArgumentParser()
recursive_parser = argparse.ArgumentParser(add_help=False)
recursive_parser.add_argument('-r', '--recursive', action='store_true',
dest='recursive', default=False,
help='look for yaml files recursively')
subparser = parser.add_subparsers(help='update, test or delete job',
dest='command')
parser_update = subparser.add_parser('update')
parser_update = subparser.add_parser('update', parents=[recursive_parser])
parser_update.add_argument('path', help='path to YAML file or directory')
parser_update.add_argument('names', help='name(s) of job(s)', nargs='*')
parser_update.add_argument('--delete-old', help='delete obsolete jobs',
action='store_true',
dest='delete_old', default=False,)
parser_test = subparser.add_parser('test')
parser_test = subparser.add_parser('test', parents=[recursive_parser])
parser_test.add_argument('path', help='path to YAML file or directory',
nargs='?', default=sys.stdin)
parser_test.add_argument('-o', dest='output_dir', default=sys.stdout,
@ -166,13 +181,22 @@ def execute(options, config):
ignore_cache=ignore_cache,
flush_cache=options.flush_cache)
if hasattr(options, 'path') and options.path == sys.stdin:
logger.debug("Input file is stdin")
if options.path.isatty():
key = 'CTRL+Z' if platform.system() == 'Windows' else 'CTRL+D'
logger.warn(
"Reading configuration from STDIN. Press %s to end input.",
key)
if hasattr(options, 'path'):
if options.path == sys.stdin:
logger.debug("Input file is stdin")
if options.path.isatty():
key = 'CTRL+Z' if platform.system() == 'Windows' else 'CTRL+D'
logger.warn(
"Reading configuration from STDIN. Press %s to end input.",
key)
# expand or convert options.path to a list
if (getattr(options, 'recursive', False)
or config.getboolean('job_builder', 'recursive')) and \
os.path.isdir(options.path):
options.path = recurse_path(options.path)
else:
options.path = [options.path]
if options.command == 'delete':
for job in options.name:

View File

@ -98,3 +98,37 @@ class CmdTests(testtools.TestCase):
config = cmd.setup_config_settings(args)
self.assertEqual(config.get('jenkins', 'url'),
"http://test-jenkins.with.non.default.url:8080/")
@mock.patch('jenkins_jobs.cmd.Builder.update_job')
@mock.patch('jenkins_jobs.cmd.os.path.isdir')
@mock.patch('jenkins_jobs.cmd.os.walk')
def test_recursive_path_option(self, os_walk_mock, isdir_mock,
update_job_mock):
"""
Test handling of recursive path option
"""
os_walk_mock.return_value = [
('/jjb_configs', ('dir1', 'dir2', 'dir3'), ()),
('/jjb_configs/dir1', ('bar',), ()),
('/jjb_configs/dir2', ('baz',), ()),
('/jjb_configs/dir3', (), ()),
('/jjb_configs/dir1/bar', (), ()),
('/jjb_configs/dir2/baz', (), ()),
]
isdir_mock.return_value = True
paths = [path for path, _, _ in os_walk_mock.return_value]
args = self.parser.parse_args(['test', '-r', '/jjb_configs'])
args.output_dir = mock.MagicMock()
config = ConfigParser.ConfigParser()
config.readfp(cStringIO.StringIO(cmd.DEFAULT_CONF))
cmd.execute(args, config) # probably better to fail here
update_job_mock.assert_called_with(paths, [], output=args.output_dir)
args = self.parser.parse_args(['test', '/jjb_configs'])
config.set('job_builder', 'recursive', 'True')
cmd.execute(args, config) # probably better to fail here
update_job_mock.assert_called_with(paths, [], output=args.output_dir)