Merge "Create nodepool.cloud inventory variable" into feature/zuulv3
diff --git a/doc/source/conf.py b/doc/source/conf.py
index fa00593..85fcdc6 100644
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
@@ -27,6 +27,7 @@
 # coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
 extensions = [
     'sphinx.ext.autodoc',
+    'sphinx_autodoc_typehints',
     'sphinx.ext.graphviz',
     'sphinxcontrib.blockdiag',
     'sphinxcontrib.programoutput',
diff --git a/doc/source/user/config.rst b/doc/source/user/config.rst
index 4898e17..7ff7106 100644
--- a/doc/source/user/config.rst
+++ b/doc/source/user/config.rst
@@ -184,19 +184,19 @@
          For more detail on the theory and operation of Zuul's
          dependent pipeline manager, see: :doc:`gating`.
 
-   .. attr:: allow-secrets
+   .. attr:: post-review
       :default: false
 
-      This is a boolean which can be used to prevent jobs which use
-      secrets in the untrusted security context from running in this
-      pipeline.  Some pipelines run on proposed changes and therefore
-      execute code which has not yet been reviewed.  In such a case,
-      allowing a job to use a secret could result in that secret being
-      exposed.  The default is ``false``, meaning that in order to run
-      jobs which use secrets in the untrusted security context, this
-      must be explicitly enabled on each Pipeline where that is safe.
+      This is a boolean which indicates that this pipeline executes
+      code that has been reviewed.  Some jobs perform actions which
+      should not be permitted with unreviewed code.  When this value
+      is ``false`` those jobs will not be permitted to run in the
+      pipeline.  If a pipeline is designed only to be used after
+      changes are reviewed or merged, set this value to ``true`` to
+      permit such jobs.
 
-      For more information, see :ref:`secret`.
+      For more information, see :ref:`secret` and
+      :attr:`job.post-review`.
 
    .. attr:: description
 
@@ -895,16 +895,18 @@
       it should be able to run this job, then it must be explicitly
       listed.  By default, all projects may use the job.
 
-   .. attr:: untrusted-secrets
+   .. attr:: post-review
+      :default: false
 
-      A boolean value which indicates that this job should not be used
-      in a pipeline where allow-secrets is ``false``.  This is
-      automatically set to ``true`` if this job is defined in a
-      :term:`untrusted-project`.  It may be explicitly set to obtain
-      the same behavior for jobs defined in :term:`config projects
-      <config-project>`.  Once this is set to ``true`` anywhere in the
-      inheritance hierarchy for a job, it will remain set for all
-      child jobs and variants (it can not be set to ``false``).
+      A boolean value which indicates whether this job may only be
+      used in pipelines where :attr:`pipeline.post-review` is
+      ``true``.  This is automatically set to ``true`` if this job is
+      defined in a :term:`untrusted-project`.  It may be explicitly
+      set to obtain the same behavior for jobs defined in
+      :term:`config projects <config-project>`.  Once this is set to
+      ``true`` anywhere in the inheritance hierarchy for a job, it
+      will remain set for all child jobs and variants (it can not be
+      set to ``false``).
 
 .. _project:
 
@@ -1078,12 +1080,19 @@
 untrusted project are run in the :term:`untrusted execution context`
 where proposed changes are used in job execution, it is dangerous to
 allow those secrets to be used in pipelines which are used to execute
-proposed but unreviewed changes.  By default, pipelines will refuse to
-run jobs which have playbooks that use secrets in the untrusted
-execution context to protect against someone proposing a change which
-exposes a secret.  To permit this (for instance, in a pipeline which
-only runs after code review), the :attr:`pipeline.allow-secrets`
-attribute may be set.
+proposed but unreviewed changes.  By default, pipelines are considered
+`pre-review` and will refuse to run jobs which have playbooks that use
+secrets in the untrusted execution context to protect against someone
+proposing a change which exposes a secret.  To permit this (for
+instance, in a pipeline which only runs after code review), the
+:attr:`pipeline.post-review` attribute may be explicitly set to
+``true``.
+
+In some cases, it may be desirable to prevent a job which is defined
+in a config project from running in a pre-review pipeline (e.g., a job
+used to publish an artifact).  In these cases, the
+:attr:`job.post-review` attribute may be explicitly set to ``true`` to
+indicate the job should only run in post-review pipelines.
 
 If a job with secrets is unsafe to be used by other projects, the
 `allowed-projects` job attribute can be used to restrict the projects
diff --git a/doc/source/user/jobs.rst b/doc/source/user/jobs.rst
index 5f36c30..4e1880a 100644
--- a/doc/source/user/jobs.rst
+++ b/doc/source/user/jobs.rst
@@ -205,6 +205,13 @@
          The full canonical name of the project including hostname.
          E.g., `git.example.com/org/project`.
 
+      .. var:: src_dir
+
+         The path to the source code on the remote host, relative
+         to the home dir of the remote user.
+         E.g., `src/git.example.com/org/project`.
+
+
    .. var:: tenant
 
       The name of the current Zuul tenant.
@@ -246,6 +253,12 @@
             The full canonical name of the project including hostname.
             E.g., `git.example.com/org/project`.
 
+         .. var:: src_dir
+
+            The path to the source code on the remote host, relative
+            to the home dir of the remote user.
+            E.g., `src/git.example.com/org/project`.
+
       .. var:: branch
 
          The target branch of the change (without the `refs/heads/` prefix).
diff --git a/test-requirements.txt b/test-requirements.txt
index bf8b979..b444297 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -11,6 +11,7 @@
 testrepository>=0.0.17
 testtools>=0.9.32
 sphinxcontrib-programoutput
+sphinx-autodoc-typehints
 mock
 PyMySQL
 mypy
diff --git a/tests/fixtures/config/ansible/git/common-config/playbooks/check-vars.yaml b/tests/fixtures/config/ansible/git/common-config/playbooks/check-vars.yaml
index a63ecbf..9bfeb0e 100644
--- a/tests/fixtures/config/ansible/git/common-config/playbooks/check-vars.yaml
+++ b/tests/fixtures/config/ansible/git/common-config/playbooks/check-vars.yaml
@@ -22,6 +22,7 @@
           - zuul.project.name == 'org/project'
           - zuul.project.canonical_hostname == 'review.example.com'
           - zuul.project.canonical_name == 'review.example.com/org/project'
+          - zuul.project.src_dir == 'src/review.example.com/org/project'
 
     - debug:
         msg: "vartest secret {{ vartest_secret }}"
diff --git a/tests/fixtures/config/ansible/git/common-config/zuul.yaml b/tests/fixtures/config/ansible/git/common-config/zuul.yaml
index ba6227b..d90f5e2 100644
--- a/tests/fixtures/config/ansible/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/ansible/git/common-config/zuul.yaml
@@ -1,7 +1,7 @@
 - pipeline:
     name: check
     manager: independent
-    allow-secrets: true
+    post-review: true
     trigger:
       gerrit:
         - event: patchset-created
diff --git a/tests/fixtures/config/data-return/git/common-config/zuul.yaml b/tests/fixtures/config/data-return/git/common-config/zuul.yaml
index 4db7eb6..906dc5b 100644
--- a/tests/fixtures/config/data-return/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/data-return/git/common-config/zuul.yaml
@@ -1,7 +1,7 @@
 - pipeline:
     name: check
     manager: independent
-    allow-secrets: true
+    post-review: true
     trigger:
       gerrit:
         - event: patchset-created
diff --git a/tests/fixtures/config/disk-accountant/git/common-config/zuul.yaml b/tests/fixtures/config/disk-accountant/git/common-config/zuul.yaml
index 4a13e73..893ea05 100644
--- a/tests/fixtures/config/disk-accountant/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/disk-accountant/git/common-config/zuul.yaml
@@ -1,7 +1,7 @@
 - pipeline:
     name: check
     manager: independent
-    allow-secrets: true
+    post-review: true
     trigger:
       gerrit:
         - event: patchset-created
diff --git a/tests/fixtures/config/inventory/git/common-config/zuul.yaml b/tests/fixtures/config/inventory/git/common-config/zuul.yaml
index d2179b7..7809c5d 100644
--- a/tests/fixtures/config/inventory/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/inventory/git/common-config/zuul.yaml
@@ -1,7 +1,7 @@
 - pipeline:
     name: check
     manager: independent
-    allow-secrets: true
+    post-review: true
     trigger:
       gerrit:
         - event: patchset-created
diff --git a/tests/fixtures/config/pre-playbook/git/common-config/zuul.yaml b/tests/fixtures/config/pre-playbook/git/common-config/zuul.yaml
index 0a6c557..16d1966 100644
--- a/tests/fixtures/config/pre-playbook/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/pre-playbook/git/common-config/zuul.yaml
@@ -1,7 +1,7 @@
 - pipeline:
     name: check
     manager: independent
-    allow-secrets: true
+    post-review: true
     trigger:
       gerrit:
         - event: patchset-created
diff --git a/tests/fixtures/config/secret-leaks/git/common-config/playbooks/secret-file-fail.yaml b/tests/fixtures/config/secret-leaks/git/common-config/playbooks/secret-file-fail.yaml
new file mode 100644
index 0000000..4984411
--- /dev/null
+++ b/tests/fixtures/config/secret-leaks/git/common-config/playbooks/secret-file-fail.yaml
@@ -0,0 +1,6 @@
+- hosts: all
+  tasks:
+    - copy:
+        content: "{{test_secret.username}} {{test_secret.password}}"
+        dest: "{{zuul.executor.work_root}}/failure-file.txt"
+        group: "hopefullythisgroupdoesnotexist"
\ No newline at end of file
diff --git a/tests/fixtures/config/secret-leaks/git/common-config/playbooks/secret-file.yaml b/tests/fixtures/config/secret-leaks/git/common-config/playbooks/secret-file.yaml
new file mode 100644
index 0000000..24bb61f
--- /dev/null
+++ b/tests/fixtures/config/secret-leaks/git/common-config/playbooks/secret-file.yaml
@@ -0,0 +1,5 @@
+- hosts: all
+  tasks:
+    - copy:
+        content: "{{test_secret.username}} {{test_secret.password}}"
+        dest: "{{zuul.executor.work_root}}/secret-file.txt"
diff --git a/tests/fixtures/config/secret-leaks/git/common-config/zuul.yaml b/tests/fixtures/config/secret-leaks/git/common-config/zuul.yaml
new file mode 100644
index 0000000..4ab198f
--- /dev/null
+++ b/tests/fixtures/config/secret-leaks/git/common-config/zuul.yaml
@@ -0,0 +1,70 @@
+- pipeline:
+    name: check
+    manager: independent
+    post-review: true
+    trigger:
+      gerrit:
+        - event: patchset-created
+    success:
+      gerrit:
+        Verified: 1
+    failure:
+      gerrit:
+        Verified: -1
+
+- pipeline:
+    name: gate
+    manager: dependent
+    success-message: Build succeeded (gate).
+    trigger:
+      gerrit:
+        - event: comment-added
+          approval:
+            - Approved: 1
+    success:
+      gerrit:
+        Verified: 2
+        submit: true
+    failure:
+      gerrit:
+        Verified: -2
+    start:
+      gerrit:
+        Verified: 0
+    precedence: high
+
+- secret:
+    name: test_secret
+    data:
+      username: test-username
+      password: !encrypted/pkcs1-oaep |
+        BFhtdnm8uXx7kn79RFL/zJywmzLkT1GY78P3bOtp4WghUFWobkifSu7ZpaV4NeO0s71YUsi1wGZZ
+        L0LveZjUN0t6OU1VZKSG8R5Ly7urjaSo1pPVIq5Rtt/H7W14Lecd+cUeKb4joeusC9drN3AA8a4o
+        ykcVpt1wVqUnTbMGC9ARMCQP6eopcs1l7tzMseprW4RDNhIuz3CRgd0QBMPl6VDoFgBPB8vxtJw+
+        3m0rqBYZCLZgCXekqlny8s2s92nJMuUABbJOEcDRarzibDsSXsfJt1y+5n7yOURsC7lovMg4GF/v
+        Cl/0YMKjBO5bpv9EM5fToeKYyPGSKQoHOnCYceb3cAVcv5UawcCic8XjhEhp4K7WPdYf2HVAC/qt
+        xhbpjTxG4U5Q/SoppOJ60WqEkQvbXs6n5Dvy7xmph6GWmU/bAv3eUK3pdD3xa2Ue1lHWz3U+rsYr
+        aI+AKYsMYx3RBlfAmCeC1ve2BXPrqnOo7G8tnUvfdYPbK4Aakk0ds/AVqFHEZN+S6hRBmBjLaRFW
+        Z3QSO1NjbBxWnaHKZYT7nkrJm8AMCgZU0ZArFLpaufKCeiK5ECSsDxic4FIsY1OkWT42qEUfL0Wd
+        +150AKGNZpPJnnP3QYY4W/MWcKH/zdO400+zWN52WevbSqZy90tqKDJrBkMl1ydqbuw1E4ZHvIs=
+
+- job:
+    name: base
+    parent: null
+
+- job:
+    parent: base
+    name: secret-file
+    secrets:
+      - test_secret
+
+- job:
+    parent: base
+    name: secret-file-fail
+    secrets:
+      - test_secret
+
+- project:
+    name: org/project
+    check:
+      jobs: []
diff --git a/tests/fixtures/config/secret-leaks/git/org_project/README b/tests/fixtures/config/secret-leaks/git/org_project/README
new file mode 100644
index 0000000..9daeafb
--- /dev/null
+++ b/tests/fixtures/config/secret-leaks/git/org_project/README
@@ -0,0 +1 @@
+test
diff --git a/tests/fixtures/config/secret-leaks/main.yaml b/tests/fixtures/config/secret-leaks/main.yaml
new file mode 100644
index 0000000..208e274
--- /dev/null
+++ b/tests/fixtures/config/secret-leaks/main.yaml
@@ -0,0 +1,8 @@
+- tenant:
+    name: tenant-one
+    source:
+      gerrit:
+        config-projects:
+          - common-config
+        untrusted-projects:
+          - org/project
diff --git a/tests/fixtures/layouts/untrusted-secrets.yaml b/tests/fixtures/layouts/untrusted-secrets.yaml
index cfa03e0..b90d3d7 100644
--- a/tests/fixtures/layouts/untrusted-secrets.yaml
+++ b/tests/fixtures/layouts/untrusted-secrets.yaml
@@ -17,7 +17,7 @@
 
 - job:
     name: project1-test
-    untrusted-secrets: true
+    post-review: true
 
 - project:
     name: org/project1
diff --git a/tests/unit/test_model.py b/tests/unit/test_model.py
index 9cc7195..ce30e7c 100644
--- a/tests/unit/test_model.py
+++ b/tests/unit/test_model.py
@@ -461,16 +461,16 @@
                 })
         layout.addJob(untrusted_secrets_untrusted_child_job)
 
-        self.assertIsNone(trusted_secrets_job.untrusted_secrets)
-        self.assertTrue(untrusted_secrets_job.untrusted_secrets)
+        self.assertIsNone(trusted_secrets_job.post_review)
+        self.assertTrue(untrusted_secrets_job.post_review)
         self.assertIsNone(
-            trusted_secrets_trusted_child_job.untrusted_secrets)
+            trusted_secrets_trusted_child_job.post_review)
         self.assertIsNone(
-            trusted_secrets_untrusted_child_job.untrusted_secrets)
+            trusted_secrets_untrusted_child_job.post_review)
         self.assertTrue(
-            untrusted_secrets_trusted_child_job.untrusted_secrets)
+            untrusted_secrets_trusted_child_job.post_review)
         self.assertTrue(
-            untrusted_secrets_untrusted_child_job.untrusted_secrets)
+            untrusted_secrets_untrusted_child_job.post_review)
 
         self.assertEqual(trusted_secrets_job.implied_run[0].secrets[0].name,
                          'trusted-secret')
@@ -697,15 +697,15 @@
                 "Project project2 is not allowed to run job job"):
             item.freezeJobGraph()
 
-    def test_job_pipeline_allow_secrets(self):
-        self.pipeline.allow_secrets = False
+    def test_job_pipeline_allow_untrusted_secrets(self):
+        self.pipeline.post_review = False
         job = configloader.JobParser.fromYaml(self.tenant, self.layout, {
             '_source_context': self.context,
             '_start_mark': self.start_mark,
             'name': 'job',
             'parent': None,
         })
-        job.untrusted_secrets = True
+        job.post_review = True
 
         self.layout.addJob(job)
 
@@ -730,7 +730,7 @@
         item.current_build_set.layout = self.layout
         with testtools.ExpectedException(
                 Exception,
-                "Pipeline gate does not allow jobs with secrets"):
+                "Pre-review pipeline gate does not allow post-review job"):
             item.freezeJobGraph()
 
 
diff --git a/tests/unit/test_scheduler.py b/tests/unit/test_scheduler.py
index 960a922..97d53e0 100755
--- a/tests/unit/test_scheduler.py
+++ b/tests/unit/test_scheduler.py
@@ -2827,7 +2827,7 @@
 
         self.assertHistory([])
         self.assertEqual(A.patchsets[0]['approvals'][0]['value'], "-1")
-        self.assertIn('does not allow jobs with secrets',
+        self.assertIn('does not allow post-review job',
                       A.messages[0])
 
     @simple_layout('layouts/tags.yaml')
diff --git a/tests/unit/test_v3.py b/tests/unit/test_v3.py
index 5e3a6fc..2293ca0 100755
--- a/tests/unit/test_v3.py
+++ b/tests/unit/test_v3.py
@@ -1245,3 +1245,89 @@
         self.assertIn('Base jobs must be defined in config projects',
                       A.messages[0])
         self.assertHistory([])
+
+
+class TestSecretLeaks(AnsibleZuulTestCase):
+    tenant_config_file = 'config/secret-leaks/main.yaml'
+
+    def searchForContent(self, path, content):
+        matches = []
+        for (dirpath, dirnames, filenames) in os.walk(path):
+            for filename in filenames:
+                filepath = os.path.join(dirpath, filename)
+                with open(filepath, 'rb') as f:
+                    if content in f.read():
+                        matches.append(filepath[len(path):])
+        return matches
+
+    def _test_secret_file(self):
+        # Or rather -- test that they *don't* leak.
+        # Keep the jobdir around so we can inspect contents.
+        self.executor_server.keep_jobdir = True
+        conf = textwrap.dedent(
+            """
+            - project:
+                name: org/project
+                check:
+                  jobs:
+                    - secret-file
+            """)
+
+        file_dict = {'.zuul.yaml': conf}
+        A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A',
+                                           files=file_dict)
+        self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
+        self.waitUntilSettled()
+        self.assertHistory([
+            dict(name='secret-file', result='SUCCESS', changes='1,1'),
+        ], ordered=False)
+        matches = self.searchForContent(self.history[0].jobdir.root,
+                                        b'test-password')
+        self.assertEqual(set(['/ansible/playbook_0/secrets.yaml',
+                              '/work/secret-file.txt']),
+                         set(matches))
+
+    def test_secret_file(self):
+        self._test_secret_file()
+
+    def test_secret_file_verbose(self):
+        # Output extra ansible info to exercise alternate logging code
+        # paths.
+        self.executor_server.verbose = True
+        self._test_secret_file()
+
+    def _test_secret_file_fail(self):
+        # Or rather -- test that they *don't* leak.
+        # Keep the jobdir around so we can inspect contents.
+        self.executor_server.keep_jobdir = True
+        conf = textwrap.dedent(
+            """
+            - project:
+                name: org/project
+                check:
+                  jobs:
+                    - secret-file-fail
+            """)
+
+        file_dict = {'.zuul.yaml': conf}
+        A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A',
+                                           files=file_dict)
+        self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
+        self.waitUntilSettled()
+        self.assertHistory([
+            dict(name='secret-file-fail', result='FAILURE', changes='1,1'),
+        ], ordered=False)
+        matches = self.searchForContent(self.history[0].jobdir.root,
+                                        b'test-password')
+        self.assertEqual(set(['/ansible/playbook_0/secrets.yaml',
+                              '/work/failure-file.txt']),
+                         set(matches))
+
+    def test_secret_file_fail(self):
+        self._test_secret_file_fail()
+
+    def test_secret_file_fail_verbose(self):
+        # Output extra ansible info to exercise alternate logging code
+        # paths.
+        self.executor_server.verbose = True
+        self._test_secret_file_fail()
diff --git a/zuul/configloader.py b/zuul/configloader.py
index 86459b0..8b459b3 100644
--- a/zuul/configloader.py
+++ b/zuul/configloader.py
@@ -369,7 +369,7 @@
                'allowed-projects': to_list(str),
                'override-branch': str,
                'description': str,
-               'untrusted-secrets': bool
+               'post-review': bool
                }
 
         return vs.Schema(job)
@@ -465,14 +465,14 @@
         # through inheritance to ensure that we don't run this job in
         # an unsafe check pipeline.
         if secrets and not conf['_source_context'].trusted:
-            job.untrusted_secrets = True
+            job.post_review = True
 
-        if 'untrusted-secrets' in conf:
-            if conf['untrusted-secrets']:
-                job.untrusted_secrets = True
+        if 'post-review' in conf:
+            if conf['post-review']:
+                job.post_review = True
             else:
-                raise Exception("Once set, the untrusted_secrets "
-                                "attribute may not be unset")
+                raise Exception("Once set, the post-review attribute "
+                                "may not be unset")
 
         # Roles are part of the playbook context so we must establish
         # them earlier than playbooks.
@@ -836,7 +836,7 @@
                     'footer-message': str,
                     'dequeue-on-new-patchset': bool,
                     'ignore-dependencies': bool,
-                    'allow-secrets': bool,
+                    'post-review': bool,
                     'disable-after-consecutive-failures':
                         vs.All(int, vs.Range(min=1)),
                     'window': window,
@@ -886,7 +886,8 @@
             'dequeue-on-new-patchset', True)
         pipeline.ignore_dependencies = conf.get(
             'ignore-dependencies', False)
-        pipeline.allow_secrets = conf.get('allow-secrets', False)
+        pipeline.post_review = conf.get(
+            'post-review', False)
 
         for conf_key, action in PipelineParser.reporter_actions.items():
             reporter_set = []
diff --git a/zuul/driver/sql/sqlconnection.py b/zuul/driver/sql/sqlconnection.py
index 1187c2d..d76fafd 100644
--- a/zuul/driver/sql/sqlconnection.py
+++ b/zuul/driver/sql/sqlconnection.py
@@ -17,6 +17,7 @@
 import alembic
 import alembic.config
 import sqlalchemy as sa
+import sqlalchemy.pool
 import voluptuous as v
 
 from zuul.connection import BaseConnection
@@ -40,7 +41,14 @@
         self.tables_established = False
         try:
             self.dburi = self.connection_config.get('dburi')
-            self.engine = sa.create_engine(self.dburi)
+            # Recycle connections if they've been idle for more than 1 second.
+            # MySQL connections are lightweight and thus keeping long-lived
+            # connections around is not valuable.
+            # TODO(mordred) Add a config paramter
+            self.engine = sa.create_engine(
+                self.dburi,
+                poolclass=sqlalchemy.pool.QueuePool,
+                pool_recycle=1)
             self._migrate()
             self._setup_tables()
             self.zuul_buildset_table, self.zuul_build_table \
diff --git a/zuul/executor/client.py b/zuul/executor/client.py
index 1c3693d..40ad860 100644
--- a/zuul/executor/client.py
+++ b/zuul/executor/client.py
@@ -16,6 +16,7 @@
 import gear
 import json
 import logging
+import os
 import time
 import threading
 from uuid import uuid4
@@ -154,7 +155,9 @@
             name=item.change.project.name,
             short_name=item.change.project.name.split('/')[-1],
             canonical_hostname=item.change.project.canonical_hostname,
-            canonical_name=item.change.project.canonical_name)
+            canonical_name=item.change.project.canonical_name,
+            src_dir=os.path.join('src', item.change.project.canonical_name),
+        )
 
         zuul_params = dict(build=uuid,
                            buildset=item.current_build_set.uuid,
@@ -186,7 +189,9 @@
                 name=i.change.project.name,
                 short_name=i.change.project.name.split('/')[-1],
                 canonical_hostname=i.change.project.canonical_hostname,
-                canonical_name=i.change.project.canonical_name)
+                canonical_name=i.change.project.canonical_name,
+                src_dir=os.path.join('src', i.change.project.canonical_name),
+            )
             if hasattr(i.change, 'number'):
                 d['change'] = str(i.change.number)
             if hasattr(i.change, 'patchset'):
diff --git a/zuul/executor/server.py b/zuul/executor/server.py
index be12812..45937ef 100644
--- a/zuul/executor/server.py
+++ b/zuul/executor/server.py
@@ -29,6 +29,10 @@
 from zuul.lib.yamlutil import yaml
 from zuul.lib.config import get_default
 
+try:
+    import ara.plugins.callbacks as ara_callbacks
+except ImportError:
+    ara_callbacks = None
 import gear
 
 import zuul.merger.merger
@@ -1395,6 +1399,17 @@
                     yaml.safe_dump(secrets, default_flow_style=False))
             jobdir_playbook.has_secrets = True
 
+        # TODO(mordred) This should likely be extracted into a more generalized
+        #               mechanism for deployers being able to add callback
+        #               plugins.
+        if ara_callbacks:
+            callback_path = '%s:%s' % (
+                self.executor_server.callback_dir,
+                os.path.dirname(ara_callbacks.__file__))
+            callback_whitelist = 'zuul_json,ara'
+        else:
+            callback_path = self.executor_server.callback_dir
+            callback_whitelist = 'zuul_json'
         with open(jobdir_playbook.ansible_config, 'w') as config:
             config.write('[defaults]\n')
             config.write('hostfile = %s\n' % self.jobdir.inventory)
@@ -1410,10 +1425,9 @@
             config.write('library = %s\n'
                          % self.executor_server.library_dir)
             config.write('command_warnings = False\n')
-            config.write('callback_plugins = %s\n'
-                         % self.executor_server.callback_dir)
+            config.write('callback_plugins = %s\n' % callback_path)
             config.write('stdout_callback = zuul_stream\n')
-            config.write('callback_whitelist = zuul_json\n')
+            config.write('callback_whitelist = %s\n' % callback_whitelist)
             # bump the timeout because busy nodes may take more than
             # 10s to respond
             config.write('timeout = 30\n')
diff --git a/zuul/model.py b/zuul/model.py
index cf57851..5a157bc 100644
--- a/zuul/model.py
+++ b/zuul/model.py
@@ -98,7 +98,7 @@
         self.success_message = None
         self.footer_message = None
         self.start_message = None
-        self.allow_secrets = False
+        self.post_review = False
         self.dequeue_on_new_patchset = True
         self.ignore_dependencies = False
         self.manager = None
@@ -801,7 +801,7 @@
             required_projects={},
             allowed_projects=None,
             override_branch=None,
-            untrusted_secrets=None,
+            post_review=None,
         )
 
         # These are generally internal attributes which are not
@@ -2322,9 +2322,9 @@
                 change.project.name not in frozen_job.allowed_projects):
                 raise Exception("Project %s is not allowed to run job %s" %
                                 (change.project.name, frozen_job.name))
-            if ((not pipeline.allow_secrets) and frozen_job.untrusted_secrets):
-                raise Exception("Pipeline %s does not allow jobs with "
-                                "secrets (job %s)" % (
+            if ((not pipeline.post_review) and frozen_job.post_review):
+                raise Exception("Pre-review pipeline %s does not allow "
+                                "post-review job %s" % (
                                     pipeline.name, frozen_job.name))
             job_graph.addJob(frozen_job)