Support excludes when recursively processing paths
Include an additional option to exclude some paths/patterns from being included in the search to allow a complex hierarchy of directories to be referenced under a single path. Provide support to allow the user to specify patterns, relative and/or absolute path references. Implements support for excluding based on absolute path, relative path and based on simple shell globing patterns. Change-Id: I236b364c268fd2bf5511a2c6d9a1c87914f3b086
This commit is contained in:
parent
74c8ac4561
commit
c5c0eb4f16
@ -74,6 +74,14 @@ job_builder section
|
|||||||
(Optional) If set to True, jenkins job builder will search for job
|
(Optional) If set to True, jenkins job builder will search for job
|
||||||
definition files recursively
|
definition files recursively
|
||||||
|
|
||||||
|
**exclude**
|
||||||
|
(Optional) If set to a list of values separated by ':', these paths will be
|
||||||
|
excluded from the list of paths to be processed when searching recursively.
|
||||||
|
Values containing no ``/`` will be matched against directory names at all
|
||||||
|
levels, those starting with ``/`` will be considered absolute, while others
|
||||||
|
containing a ``/`` somewhere other than the start of the value will be
|
||||||
|
considered relative to the starting path.
|
||||||
|
|
||||||
**allow_duplicates**
|
**allow_duplicates**
|
||||||
(Optional) By default `jenkins-jobs` will abort any time a duplicate macro,
|
(Optional) By default `jenkins-jobs` will abort any time a duplicate macro,
|
||||||
template, job-group or job name is encountered as it cannot establish the
|
template, job-group or job name is encountered as it cannot establish the
|
||||||
@ -155,6 +163,59 @@ Jenkins instances suited to various needs you may want to share configuration
|
|||||||
between those instances (global). Furthermore, there may be various ways you
|
between those instances (global). Furthermore, there may be various ways you
|
||||||
would like to structure jobs within a given instance.
|
would like to structure jobs within a given instance.
|
||||||
|
|
||||||
|
Recursive Searching of Paths
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
In addition to passing multiple paths to JJB it is also possible to enable
|
||||||
|
recursive searching to process all yaml files in the tree beneath each path.
|
||||||
|
For example::
|
||||||
|
|
||||||
|
For a tree:
|
||||||
|
/path/
|
||||||
|
to/
|
||||||
|
defs/
|
||||||
|
ci_jobs/
|
||||||
|
release_jobs/
|
||||||
|
globals/
|
||||||
|
macros/
|
||||||
|
templates/
|
||||||
|
|
||||||
|
jenkins-jobs update -r /path/to/defs:/path/to/globals
|
||||||
|
|
||||||
|
JJB will search defs/ci_jobs, defs/release_jobs, globals/macros and
|
||||||
|
globals/templates in addition to the defs and globals trees.
|
||||||
|
|
||||||
|
Excluding Paths
|
||||||
|
---------------
|
||||||
|
|
||||||
|
To allow a complex tree of jobs where some jobs are managed differently without
|
||||||
|
needing to explicitly provide each path, the recursive path processing supports
|
||||||
|
excluding paths based on absolute paths, relative paths and patterns. For
|
||||||
|
example::
|
||||||
|
|
||||||
|
For a tree:
|
||||||
|
/path/
|
||||||
|
to/
|
||||||
|
defs/
|
||||||
|
ci_jobs/
|
||||||
|
manual/
|
||||||
|
release_jobs/
|
||||||
|
manual/
|
||||||
|
qa_jobs/
|
||||||
|
globals/
|
||||||
|
macros/
|
||||||
|
templates/
|
||||||
|
special/
|
||||||
|
|
||||||
|
jenkins-jobs update -r -x man*:./qa_jobs -x /path/to/defs/globals/special \
|
||||||
|
/path/to/defs:/path/to/globals
|
||||||
|
|
||||||
|
JJB search the given paths, ignoring the directories qa_jobs, ci_jobs/manual,
|
||||||
|
release_jobs/manual, and globals/special when building the list of yaml files
|
||||||
|
to be processed. Absolute paths are denoted by starting from the root,
|
||||||
|
relative by containing the path separator, and patterns by having neither.
|
||||||
|
Patterns use simple shell globing to match directories.
|
||||||
|
|
||||||
Deleting Jobs
|
Deleting Jobs
|
||||||
^^^^^^^^^^^^^
|
^^^^^^^^^^^^^
|
||||||
Jenkins Job Builder supports deleting jobs from Jenkins.
|
Jenkins Job Builder supports deleting jobs from Jenkins.
|
||||||
|
@ -3,6 +3,7 @@ ignore_cache=True
|
|||||||
keep_descriptions=False
|
keep_descriptions=False
|
||||||
include_path=.:scripts:~/git/
|
include_path=.:scripts:~/git/
|
||||||
recursive=False
|
recursive=False
|
||||||
|
exclude=.*:manual:./development
|
||||||
allow_duplicates=False
|
allow_duplicates=False
|
||||||
|
|
||||||
[jenkins]
|
[jenkins]
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
from six.moves import configparser, StringIO
|
from six.moves import configparser, StringIO
|
||||||
|
import fnmatch
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
@ -33,6 +34,7 @@ DEFAULT_CONF = """
|
|||||||
keep_descriptions=False
|
keep_descriptions=False
|
||||||
ignore_cache=False
|
ignore_cache=False
|
||||||
recursive=False
|
recursive=False
|
||||||
|
exclude=.*
|
||||||
allow_duplicates=False
|
allow_duplicates=False
|
||||||
|
|
||||||
[jenkins]
|
[jenkins]
|
||||||
@ -51,11 +53,28 @@ def confirm(question):
|
|||||||
sys.exit('Aborted')
|
sys.exit('Aborted')
|
||||||
|
|
||||||
|
|
||||||
def recurse_path(root):
|
def recurse_path(root, excludes=None):
|
||||||
|
if excludes is None:
|
||||||
|
excludes = []
|
||||||
|
|
||||||
basepath = os.path.realpath(root)
|
basepath = os.path.realpath(root)
|
||||||
pathlist = [basepath]
|
pathlist = [basepath]
|
||||||
|
|
||||||
|
patterns = [e for e in excludes if os.path.sep not in e]
|
||||||
|
absolute = [e for e in excludes if os.path.isabs(e)]
|
||||||
|
relative = [e for e in excludes if os.path.sep in e and
|
||||||
|
not os.path.isabs(e)]
|
||||||
for root, dirs, files in os.walk(basepath, topdown=True):
|
for root, dirs, files in os.walk(basepath, topdown=True):
|
||||||
|
dirs[:] = [
|
||||||
|
d for d in dirs
|
||||||
|
if not any([fnmatch.fnmatch(d, pattern) for pattern in patterns])
|
||||||
|
if not any([fnmatch.fnmatch(os.path.abspath(os.path.join(root, d)),
|
||||||
|
path)
|
||||||
|
for path in absolute])
|
||||||
|
if not any([fnmatch.fnmatch(os.path.relpath(os.path.join(root, d)),
|
||||||
|
path)
|
||||||
|
for path in relative])
|
||||||
|
]
|
||||||
pathlist.extend([os.path.join(root, path) for path in dirs])
|
pathlist.extend([os.path.join(root, path) for path in dirs])
|
||||||
|
|
||||||
return pathlist
|
return pathlist
|
||||||
@ -68,6 +87,10 @@ def create_parser():
|
|||||||
recursive_parser.add_argument('-r', '--recursive', action='store_true',
|
recursive_parser.add_argument('-r', '--recursive', action='store_true',
|
||||||
dest='recursive', default=False,
|
dest='recursive', default=False,
|
||||||
help='look for yaml files recursively')
|
help='look for yaml files recursively')
|
||||||
|
recursive_parser.add_argument('-x', '--exclude', dest='exclude',
|
||||||
|
action='append', default=[],
|
||||||
|
help='paths to exclude when using recursive'
|
||||||
|
' search, uses standard globbing.')
|
||||||
subparser = parser.add_subparsers(help='update, test or delete job',
|
subparser = parser.add_subparsers(help='update, test or delete job',
|
||||||
dest='command')
|
dest='command')
|
||||||
|
|
||||||
@ -95,7 +118,7 @@ def create_parser():
|
|||||||
|
|
||||||
# subparser: delete
|
# subparser: delete
|
||||||
|
|
||||||
parser_delete = subparser.add_parser('delete')
|
parser_delete = subparser.add_parser('delete', parents=[recursive_parser])
|
||||||
parser_delete.add_argument('name', help='name of job', nargs='+')
|
parser_delete.add_argument('name', help='name of job', nargs='+')
|
||||||
parser_delete.add_argument('-p', '--path', default=None,
|
parser_delete.add_argument('-p', '--path', default=None,
|
||||||
help='colon-separated list of paths to'
|
help='colon-separated list of paths to'
|
||||||
@ -233,10 +256,13 @@ def execute(options, config):
|
|||||||
do_recurse = (getattr(options, 'recursive', False) or
|
do_recurse = (getattr(options, 'recursive', False) or
|
||||||
config.getboolean('job_builder', 'recursive'))
|
config.getboolean('job_builder', 'recursive'))
|
||||||
|
|
||||||
|
excludes = [e for elist in options.exclude
|
||||||
|
for e in elist.split(os.pathsep)] or \
|
||||||
|
config.get('job_builder', 'exclude').split(os.pathsep)
|
||||||
paths = []
|
paths = []
|
||||||
for path in options.path:
|
for path in options.path:
|
||||||
if do_recurse and os.path.isdir(path):
|
if do_recurse and os.path.isdir(path):
|
||||||
paths.extend(recurse_path(path))
|
paths.extend(recurse_path(path, excludes))
|
||||||
else:
|
else:
|
||||||
paths.append(path)
|
paths.append(path)
|
||||||
options.path = paths
|
options.path = paths
|
||||||
|
@ -7,40 +7,41 @@ import jenkins
|
|||||||
|
|
||||||
from jenkins_jobs import cmd
|
from jenkins_jobs import cmd
|
||||||
from jenkins_jobs.errors import JenkinsJobsException
|
from jenkins_jobs.errors import JenkinsJobsException
|
||||||
from tests.cmd.test_cmd import CmdTestsBase
|
|
||||||
from tests.base import mock
|
from tests.base import mock
|
||||||
|
from tests.cmd.test_cmd import CmdTestsBase
|
||||||
|
from tests.cmd.test_recurse_path import fake_os_walk
|
||||||
|
|
||||||
|
|
||||||
os_walk_return_values = {
|
os_walk_return_values = {
|
||||||
'/jjb_projects': [
|
'/jjb_projects': [
|
||||||
('/jjb_projects', ('dir1', 'dir2', 'dir3'), ()),
|
('/jjb_projects', (['dir1', 'dir2', 'dir3'], ())),
|
||||||
('/jjb_projects/dir1', ('bar',), ()),
|
('/jjb_projects/dir1', (['bar'], ())),
|
||||||
('/jjb_projects/dir2', ('baz',), ()),
|
('/jjb_projects/dir2', (['baz'], ())),
|
||||||
('/jjb_projects/dir3', (), ()),
|
('/jjb_projects/dir3', ([], ())),
|
||||||
('/jjb_projects/dir1/bar', (), ()),
|
('/jjb_projects/dir1/bar', ([], ())),
|
||||||
('/jjb_projects/dir2/baz', (), ()),
|
('/jjb_projects/dir2/baz', ([], ())),
|
||||||
],
|
],
|
||||||
'/jjb_templates': [
|
'/jjb_templates': [
|
||||||
('/jjb_templates', ('dir1', 'dir2', 'dir3'), ()),
|
('/jjb_templates', (['dir1', 'dir2', 'dir3'], ())),
|
||||||
('/jjb_templates/dir1', ('bar',), ()),
|
('/jjb_templates/dir1', (['bar'], ())),
|
||||||
('/jjb_templates/dir2', ('baz',), ()),
|
('/jjb_templates/dir2', (['baz'], ())),
|
||||||
('/jjb_templates/dir3', (), ()),
|
('/jjb_templates/dir3', ([], ())),
|
||||||
('/jjb_templates/dir1/bar', (), ()),
|
('/jjb_templates/dir1/bar', ([], ())),
|
||||||
('/jjb_templates/dir2/baz', (), ()),
|
('/jjb_templates/dir2/baz', ([], ())),
|
||||||
],
|
],
|
||||||
'/jjb_macros': [
|
'/jjb_macros': [
|
||||||
('/jjb_macros', ('dir1', 'dir2', 'dir3'), ()),
|
('/jjb_macros', (['dir1', 'dir2', 'dir3'], ())),
|
||||||
('/jjb_macros/dir1', ('bar',), ()),
|
('/jjb_macros/dir1', (['bar'], ())),
|
||||||
('/jjb_macros/dir2', ('baz',), ()),
|
('/jjb_macros/dir2', (['baz'], ())),
|
||||||
('/jjb_macros/dir3', (), ()),
|
('/jjb_macros/dir3', ([], ())),
|
||||||
('/jjb_macros/dir1/bar', (), ()),
|
('/jjb_macros/dir1/bar', ([], ())),
|
||||||
('/jjb_macros/dir2/baz', (), ()),
|
('/jjb_macros/dir2/baz', ([], ())),
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def os_walk_side_effects(path_name, topdown):
|
def os_walk_side_effects(path_name, topdown):
|
||||||
return os_walk_return_values[path_name]
|
return fake_os_walk(os_walk_return_values[path_name])(path_name, topdown)
|
||||||
|
|
||||||
|
|
||||||
@mock.patch('jenkins_jobs.builder.Jenkins.get_plugins_info', mock.MagicMock)
|
@mock.patch('jenkins_jobs.builder.Jenkins.get_plugins_info', mock.MagicMock)
|
||||||
@ -114,8 +115,7 @@ class TestTests(CmdTestsBase):
|
|||||||
path_list = os_walk_return_values.keys()
|
path_list = os_walk_return_values.keys()
|
||||||
paths = []
|
paths = []
|
||||||
for path in path_list:
|
for path in path_list:
|
||||||
paths.extend([p for p, _, _ in
|
paths.extend([p for p, _ in os_walk_return_values[path]])
|
||||||
os_walk_return_values[path]])
|
|
||||||
|
|
||||||
multipath = os.pathsep.join(path_list)
|
multipath = os.pathsep.join(path_list)
|
||||||
|
|
||||||
@ -132,6 +132,34 @@ class TestTests(CmdTestsBase):
|
|||||||
|
|
||||||
update_job_mock.assert_called_with(paths, [], output=args.output_dir)
|
update_job_mock.assert_called_with(paths, [], output=args.output_dir)
|
||||||
|
|
||||||
|
@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_multi_path_with_excludes(self, os_walk_mock, isdir_mock,
|
||||||
|
update_job_mock):
|
||||||
|
"""
|
||||||
|
Run test mode and pass multiple paths with recursive path option.
|
||||||
|
"""
|
||||||
|
|
||||||
|
os_walk_mock.side_effect = os_walk_side_effects
|
||||||
|
isdir_mock.return_value = True
|
||||||
|
|
||||||
|
path_list = os_walk_return_values.keys()
|
||||||
|
paths = []
|
||||||
|
for path in path_list:
|
||||||
|
paths.extend([p for p, __ in os_walk_return_values[path]
|
||||||
|
if 'dir1' not in p and 'dir2' not in p])
|
||||||
|
|
||||||
|
multipath = os.pathsep.join(path_list)
|
||||||
|
|
||||||
|
args = self.parser.parse_args(['test', '-r', multipath, '-x',
|
||||||
|
'dir1:dir2'])
|
||||||
|
args.output_dir = mock.MagicMock()
|
||||||
|
|
||||||
|
cmd.execute(args, self.config)
|
||||||
|
|
||||||
|
update_job_mock.assert_called_with(paths, [], output=args.output_dir)
|
||||||
|
|
||||||
def test_console_output(self):
|
def test_console_output(self):
|
||||||
"""
|
"""
|
||||||
Run test mode and verify that resulting XML gets sent to the console.
|
Run test mode and verify that resulting XML gets sent to the console.
|
||||||
|
136
tests/cmd/test_recurse_path.py
Normal file
136
tests/cmd/test_recurse_path.py
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
import mock
|
||||||
|
import testtools
|
||||||
|
|
||||||
|
from jenkins_jobs import cmd
|
||||||
|
|
||||||
|
|
||||||
|
def fake_os_walk(paths):
|
||||||
|
"""Helper function for mocking os.walk() where must test that manipulation
|
||||||
|
of the returned dirs variable works as expected
|
||||||
|
"""
|
||||||
|
paths_dict = dict(paths)
|
||||||
|
|
||||||
|
def os_walk(top, topdown=True):
|
||||||
|
dirs, nondirs = paths_dict[top]
|
||||||
|
yield top, dirs, nondirs
|
||||||
|
for name in dirs:
|
||||||
|
# hard code use of '/' to ensure the test data can be defined as
|
||||||
|
# simple strings otherwise tests using this helper will break on
|
||||||
|
# platforms where os.path.sep is different.
|
||||||
|
new_path = "/".join([top, name])
|
||||||
|
for x in os_walk(new_path, topdown):
|
||||||
|
yield x
|
||||||
|
return os_walk
|
||||||
|
|
||||||
|
|
||||||
|
# Testing the cmd module can sometimes result in the CacheStorage class
|
||||||
|
# attempting to create the cache directory multiple times as the tests
|
||||||
|
# are run in parallel. Stub out the CacheStorage to ensure that each
|
||||||
|
# test can safely create the object without effect.
|
||||||
|
@mock.patch('jenkins_jobs.builder.CacheStorage', mock.MagicMock)
|
||||||
|
class CmdRecursePath(testtools.TestCase):
|
||||||
|
|
||||||
|
@mock.patch('jenkins_jobs.cmd.os.walk')
|
||||||
|
def test_recursive_path_option_exclude_pattern(self, oswalk_mock):
|
||||||
|
"""
|
||||||
|
Test paths returned by the recursive processing when using pattern
|
||||||
|
excludes.
|
||||||
|
|
||||||
|
testing paths
|
||||||
|
/jjb_configs/dir1/test1/
|
||||||
|
/jjb_configs/dir1/file
|
||||||
|
/jjb_configs/dir2/test2/
|
||||||
|
/jjb_configs/dir3/bar/
|
||||||
|
/jjb_configs/test3/bar/
|
||||||
|
/jjb_configs/test3/baz/
|
||||||
|
"""
|
||||||
|
|
||||||
|
os_walk_paths = [
|
||||||
|
('/jjb_configs', (['dir1', 'dir2', 'dir3', 'test3'], ())),
|
||||||
|
('/jjb_configs/dir1', (['test1'], ('file'))),
|
||||||
|
('/jjb_configs/dir2', (['test2'], ())),
|
||||||
|
('/jjb_configs/dir3', (['bar'], ())),
|
||||||
|
('/jjb_configs/dir3/bar', ([], ())),
|
||||||
|
('/jjb_configs/test3/bar', None),
|
||||||
|
('/jjb_configs/test3/baz', None)
|
||||||
|
]
|
||||||
|
|
||||||
|
paths = [k for k, v in os_walk_paths if v is not None]
|
||||||
|
|
||||||
|
oswalk_mock.side_effect = fake_os_walk(os_walk_paths)
|
||||||
|
self.assertEqual(paths, cmd.recurse_path('/jjb_configs', ['test*']))
|
||||||
|
|
||||||
|
@mock.patch('jenkins_jobs.cmd.os.walk')
|
||||||
|
def test_recursive_path_option_exclude_absolute(self, oswalk_mock):
|
||||||
|
"""
|
||||||
|
Test paths returned by the recursive processing when using absolute
|
||||||
|
excludes.
|
||||||
|
|
||||||
|
testing paths
|
||||||
|
/jjb_configs/dir1/test1/
|
||||||
|
/jjb_configs/dir1/file
|
||||||
|
/jjb_configs/dir2/test2/
|
||||||
|
/jjb_configs/dir3/bar/
|
||||||
|
/jjb_configs/test3/bar/
|
||||||
|
/jjb_configs/test3/baz/
|
||||||
|
"""
|
||||||
|
|
||||||
|
os_walk_paths = [
|
||||||
|
('/jjb_configs', (['dir1', 'dir2', 'dir3', 'test3'], ())),
|
||||||
|
('/jjb_configs/dir1', None),
|
||||||
|
('/jjb_configs/dir2', (['test2'], ())),
|
||||||
|
('/jjb_configs/dir3', (['bar'], ())),
|
||||||
|
('/jjb_configs/test3', (['bar', 'baz'], ())),
|
||||||
|
('/jjb_configs/dir2/test2', ([], ())),
|
||||||
|
('/jjb_configs/dir3/bar', ([], ())),
|
||||||
|
('/jjb_configs/test3/bar', ([], ())),
|
||||||
|
('/jjb_configs/test3/baz', ([], ()))
|
||||||
|
]
|
||||||
|
|
||||||
|
paths = [k for k, v in os_walk_paths if v is not None]
|
||||||
|
|
||||||
|
oswalk_mock.side_effect = fake_os_walk(os_walk_paths)
|
||||||
|
|
||||||
|
self.assertEqual(paths, cmd.recurse_path('/jjb_configs',
|
||||||
|
['/jjb_configs/dir1']))
|
||||||
|
|
||||||
|
@mock.patch('jenkins_jobs.cmd.os.walk')
|
||||||
|
def test_recursive_path_option_exclude_relative(self, oswalk_mock):
|
||||||
|
"""
|
||||||
|
Test paths returned by the recursive processing when using relative
|
||||||
|
excludes.
|
||||||
|
|
||||||
|
testing paths
|
||||||
|
./jjb_configs/dir1/test/
|
||||||
|
./jjb_configs/dir1/file
|
||||||
|
./jjb_configs/dir2/test/
|
||||||
|
./jjb_configs/dir3/bar/
|
||||||
|
./jjb_configs/test3/bar/
|
||||||
|
./jjb_configs/test3/baz/
|
||||||
|
"""
|
||||||
|
|
||||||
|
os_walk_paths = [
|
||||||
|
('jjb_configs', (['dir1', 'dir2', 'dir3', 'test3'], ())),
|
||||||
|
('jjb_configs/dir1', (['test'], ('file'))),
|
||||||
|
('jjb_configs/dir2', (['test2'], ())),
|
||||||
|
('jjb_configs/dir3', (['bar'], ())),
|
||||||
|
('jjb_configs/test3', (['bar', 'baz'], ())),
|
||||||
|
('jjb_configs/dir1/test', ([], ())),
|
||||||
|
('jjb_configs/dir2/test2', ([], ())),
|
||||||
|
('jjb_configs/dir3/bar', ([], ())),
|
||||||
|
('jjb_configs/test3/bar', None),
|
||||||
|
('jjb_configs/test3/baz', ([], ()))
|
||||||
|
]
|
||||||
|
|
||||||
|
rel_os_walk_paths = [
|
||||||
|
(os.path.abspath(
|
||||||
|
os.path.join(os.path.curdir, k)), v) for k, v in os_walk_paths]
|
||||||
|
|
||||||
|
paths = [k for k, v in rel_os_walk_paths if v is not None]
|
||||||
|
|
||||||
|
oswalk_mock.side_effect = fake_os_walk(rel_os_walk_paths)
|
||||||
|
|
||||||
|
self.assertEqual(paths, cmd.recurse_path('jjb_configs',
|
||||||
|
['jjb_configs/test3/bar']))
|
Loading…
Reference in New Issue
Block a user