diff --git a/doc/source/user/config.rst b/doc/source/user/config.rst index 65bffcfd67..80c9136731 100644 --- a/doc/source/user/config.rst +++ b/doc/source/user/config.rst @@ -668,6 +668,9 @@ Here is an example of two job definitions: affect that branch, and likewise, changes to the master branch only affect it. + See :attr:`pragma.implied-branch-matchers` for how to override + this behavior on a per-file basis. + .. attr:: files This attribute indicates that the job should only run on changes @@ -1275,3 +1278,41 @@ where it is updated is merged. An example follows: :default: 1 The maximum number of running jobs which can use this semaphore. + +.. _pragma: + +Pragma +~~~~~~ + +The `pragma` item does not behave like the others. It can not be +included or excluded from configuration loading by the administrator, +and does not form part of the final configuration itself. It is used +to alter how the configuration is processed while loading. + +A pragma item only affects the current file. The same file in another +branch of the same project will not be affected, nor any other files +or any other projects. The effect is global within that file -- +pragma directives may not be set and then unset within the same file. + +.. code-block:: yaml + + - pragma: + implied-branch-matchers: False + +.. attr:: pragma + + The pragma item currently only supports one attribute: + + .. attr:: implied-branch-matchers + + This is a boolean, which, if set, may be used to enable + (``True``) or disable (``False``) the addition of implied branch + matchers to job definitions. Normally Zuul decides whether to + add these based on heuristics described in :attr:`job.branches`. + This attribute overrides that behavior. + + This can be useful if a project has multiple branches, yet the + jobs defined in the master branch should apply to all branches. + + Note that if a job contains an explicit branch matcher, it will + be used regardless of the value supplied here. diff --git a/tests/fixtures/config/pragma/git/common-config/zuul.yaml b/tests/fixtures/config/pragma/git/common-config/zuul.yaml new file mode 100644 index 0000000000..7a8b45e34a --- /dev/null +++ b/tests/fixtures/config/pragma/git/common-config/zuul.yaml @@ -0,0 +1,53 @@ +- pipeline: + name: check + manager: independent + trigger: + gerrit: + - event: patchset-created + success: + gerrit: + Verified: 1 + failure: + gerrit: + Verified: -1 + +- pipeline: + name: gate + manager: dependent + post-review: True + trigger: + gerrit: + - event: comment-added + approval: + - Approved: 1 + success: + gerrit: + Verified: 2 + submit: true + failure: + gerrit: + Verified: -2 + start: + gerrit: + Verified: 0 + precedence: high + +- job: + name: base + parent: null + +- project: + name: common-config + check: + jobs: [] + gate: + jobs: + - noop + +- project: + name: org/project + check: + jobs: [] + gate: + jobs: + - noop diff --git a/tests/fixtures/config/pragma/git/org_project/README b/tests/fixtures/config/pragma/git/org_project/README new file mode 100644 index 0000000000..9daeafb986 --- /dev/null +++ b/tests/fixtures/config/pragma/git/org_project/README @@ -0,0 +1 @@ +test diff --git a/tests/fixtures/config/pragma/git/org_project/nopragma.yaml b/tests/fixtures/config/pragma/git/org_project/nopragma.yaml new file mode 100644 index 0000000000..95a306b6e0 --- /dev/null +++ b/tests/fixtures/config/pragma/git/org_project/nopragma.yaml @@ -0,0 +1,2 @@ +- job: + name: test-job diff --git a/tests/fixtures/config/pragma/git/org_project/playbooks/test-job.yaml b/tests/fixtures/config/pragma/git/org_project/playbooks/test-job.yaml new file mode 100644 index 0000000000..f679dceaef --- /dev/null +++ b/tests/fixtures/config/pragma/git/org_project/playbooks/test-job.yaml @@ -0,0 +1,2 @@ +- hosts: all + tasks: [] diff --git a/tests/fixtures/config/pragma/git/org_project/pragma.yaml b/tests/fixtures/config/pragma/git/org_project/pragma.yaml new file mode 100644 index 0000000000..89852b0f0a --- /dev/null +++ b/tests/fixtures/config/pragma/git/org_project/pragma.yaml @@ -0,0 +1,5 @@ +- pragma: + implied-branch-matchers: False + +- job: + name: test-job diff --git a/tests/fixtures/config/pragma/main.yaml b/tests/fixtures/config/pragma/main.yaml new file mode 100644 index 0000000000..208e274b13 --- /dev/null +++ b/tests/fixtures/config/pragma/main.yaml @@ -0,0 +1,8 @@ +- tenant: + name: tenant-one + source: + gerrit: + config-projects: + - common-config + untrusted-projects: + - org/project diff --git a/tests/unit/test_v3.py b/tests/unit/test_v3.py index 70b898e2d8..b9ae04bc2f 100755 --- a/tests/unit/test_v3.py +++ b/tests/unit/test_v3.py @@ -1907,6 +1907,57 @@ class TestMaxTimeout(AnsibleZuulTestCase): "B should not fail because of timeout limit") +class TestPragma(ZuulTestCase): + tenant_config_file = 'config/pragma/main.yaml' + + def test_no_pragma(self): + self.create_branch('org/project', 'stable') + with open(os.path.join(FIXTURE_DIR, + 'config/pragma/git/', + 'org_project/nopragma.yaml')) as f: + config = f.read() + file_dict = {'.zuul.yaml': config} + A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A', + files=file_dict) + A.addApproval('Code-Review', 2) + self.fake_gerrit.addEvent(A.addApproval('Approved', 1)) + self.waitUntilSettled() + self.fake_gerrit.addEvent(A.getChangeMergedEvent()) + self.waitUntilSettled() + + # This is an untrusted repo with 2 branches, so it should have + # an implied branch matcher for the job. + tenant = self.sched.abide.tenants.get('tenant-one') + jobs = tenant.layout.getJobs('test-job') + self.assertEqual(len(jobs), 1) + for job in tenant.layout.getJobs('test-job'): + self.assertIsNotNone(job.branch_matcher) + + def test_pragma(self): + self.create_branch('org/project', 'stable') + with open(os.path.join(FIXTURE_DIR, + 'config/pragma/git/', + 'org_project/pragma.yaml')) as f: + config = f.read() + file_dict = {'.zuul.yaml': config} + A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A', + files=file_dict) + A.addApproval('Code-Review', 2) + self.fake_gerrit.addEvent(A.addApproval('Approved', 1)) + self.waitUntilSettled() + self.fake_gerrit.addEvent(A.getChangeMergedEvent()) + self.waitUntilSettled() + + # This is an untrusted repo with 2 branches, so it would + # normally have an implied branch matcher, but our pragma + # overrides it. + tenant = self.sched.abide.tenants.get('tenant-one') + jobs = tenant.layout.getJobs('test-job') + self.assertEqual(len(jobs), 1) + for job in tenant.layout.getJobs('test-job'): + self.assertIsNone(job.branch_matcher) + + class TestBaseJobs(ZuulTestCase): tenant_config_file = 'config/base-jobs/main.yaml' diff --git a/zuul/configloader.py b/zuul/configloader.py index 6ff7dadbfa..2cb23d99ba 100644 --- a/zuul/configloader.py +++ b/zuul/configloader.py @@ -238,7 +238,7 @@ class ZuulMark(object): class ZuulSafeLoader(yaml.SafeLoader): zuul_node_types = frozenset(('job', 'nodeset', 'secret', 'pipeline', 'project', 'project-template', - 'semaphore')) + 'semaphore', 'pragma')) def __init__(self, stream, context): wrapped_stream = io.StringIO(stream) @@ -313,6 +313,30 @@ class EncryptedPKCS1_OAEP(yaml.YAMLObject): private_key).decode('utf8') +class PragmaParser(object): + pragma = { + 'implied-branch-matchers': bool, + '_source_context': model.SourceContext, + '_start_mark': ZuulMark, + } + + schema = vs.Schema(pragma) + + def __init__(self): + self.log = logging.getLogger("zuul.PragmaParser") + + def fromYaml(self, conf): + with configuration_exceptions('project-template', conf): + self.schema(conf) + + bm = conf.get('implied-branch-matchers') + if bm is None: + return + + source_context = conf['_source_context'] + source_context.implied_branch_matchers = bm + + class NodeSetParser(object): @staticmethod def getSchema(anonymous=False): @@ -459,6 +483,13 @@ class JobParser(object): if project_pipeline: return None + # If the user has set a pragma directive for this, use the + # value (if unset, the value is None). + if job.source_context.implied_branch_matchers is True: + return [job.source_context.branch] + elif job.source_context.implied_branch_matchers is False: + return None + # If this is a trusted project, don't create implied branch # matchers. if job.source_context.trusted: @@ -1462,6 +1493,12 @@ class TenantParser(object): @staticmethod def _parseLayoutItems(layout, tenant, data, scheduler, connections, skip_pipelines=False, skip_semaphores=False): + # Handle pragma items first since they modify the source context + # used by other classes. + pragma_parser = PragmaParser() + for config_pragma in data.pragmas: + pragma_parser.fromYaml(config_pragma) + if not skip_pipelines: for config_pipeline in data.pipelines: classes = TenantParser._getLoadClasses( diff --git a/zuul/model.py b/zuul/model.py index ee2ea26ae7..d2ecef4e8d 100644 --- a/zuul/model.py +++ b/zuul/model.py @@ -635,6 +635,7 @@ class SourceContext(object): self.branch = branch self.path = path self.trusted = trusted + self.implied_branch_matchers = None def __str__(self): return '%s/%s@%s' % (self.project, self.path, self.branch) @@ -2338,6 +2339,7 @@ class UnparsedTenantConfig(object): """A collection of yaml lists that has not yet been parsed into objects.""" def __init__(self): + self.pragmas = [] self.pipelines = [] self.jobs = [] self.project_templates = [] @@ -2348,6 +2350,7 @@ class UnparsedTenantConfig(object): def copy(self): r = UnparsedTenantConfig() + r.pragmas = copy.deepcopy(self.pragmas) r.pipelines = copy.deepcopy(self.pipelines) r.jobs = copy.deepcopy(self.jobs) r.project_templates = copy.deepcopy(self.project_templates) @@ -2359,6 +2362,7 @@ class UnparsedTenantConfig(object): def extend(self, conf): if isinstance(conf, UnparsedTenantConfig): + self.pragmas.extend(conf.pragmas) self.pipelines.extend(conf.pipelines) self.jobs.extend(conf.jobs) self.project_templates.extend(conf.project_templates) @@ -2393,6 +2397,8 @@ class UnparsedTenantConfig(object): self.secrets.append(value) elif key == 'semaphore': self.semaphores.append(value) + elif key == 'pragma': + self.pragmas.append(value) else: raise ConfigItemUnknownError()