Merge "Migrate docs-draft jobs to emit to logs/html" into feature/zuulv3
diff --git a/tests/unit/test_v3.py b/tests/unit/test_v3.py
index 9a10e9d..94f169a 100755
--- a/tests/unit/test_v3.py
+++ b/tests/unit/test_v3.py
@@ -748,6 +748,48 @@
         self.assertIn('appears multiple times', A.messages[0],
                       "A should have a syntax error reported")
 
+    def test_secret_not_found_error(self):
+        in_repo_conf = textwrap.dedent(
+            """
+            - job:
+                name: test
+                secrets: does-not-exist
+            """)
+
+        file_dict = {'.zuul.yaml': in_repo_conf}
+        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.assertEqual(A.data['status'], 'NEW')
+        self.assertEqual(A.reported, 1,
+                         "A should report failure")
+        self.assertIn('secret "does-not-exist" was not found', A.messages[0],
+                      "A should have a syntax error reported")
+
+    def test_nodeset_not_found_error(self):
+        in_repo_conf = textwrap.dedent(
+            """
+            - job:
+                name: test
+                nodeset: does-not-exist
+            """)
+
+        file_dict = {'.zuul.yaml': in_repo_conf}
+        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.assertEqual(A.data['status'], 'NEW')
+        self.assertEqual(A.reported, 1,
+                         "A should report failure")
+        self.assertIn('nodeset "does-not-exist" was not found', A.messages[0],
+                      "A should have a syntax error reported")
+
     def test_multi_repo(self):
         downstream_repo_conf = textwrap.dedent(
             """
diff --git a/zuul/configloader.py b/zuul/configloader.py
index 62439c4..b70ea59 100644
--- a/zuul/configloader.py
+++ b/zuul/configloader.py
@@ -106,6 +106,24 @@
         super(ProjectNotFoundError, self).__init__(message)
 
 
+class SecretNotFoundError(Exception):
+    def __init__(self, secret):
+        message = textwrap.dedent("""\
+        The secret "{secret}" was not found.
+        """)
+        message = textwrap.fill(message.format(secret=secret))
+        super(SecretNotFoundError, self).__init__(message)
+
+
+class NodesetNotFoundError(Exception):
+    def __init__(self, nodeset):
+        message = textwrap.dedent("""\
+        The nodeset "{nodeset}" was not found.
+        """)
+        message = textwrap.fill(message.format(nodeset=nodeset))
+        super(NodesetNotFoundError, self).__init__(message)
+
+
 class PipelineNotPermittedError(Exception):
     def __init__(self):
         message = textwrap.dedent("""\
@@ -358,10 +376,6 @@
 
     @staticmethod
     def getSchema():
-        node = {vs.Required('name'): str,
-                vs.Required('label'): str,
-                }
-
         zuul_role = {vs.Required('zuul'): str,
                      'name': str}
 
@@ -391,7 +405,6 @@
                'files': to_list(str),
                'secrets': to_list(vs.Any(secret, str)),
                'irrelevant-files': to_list(str),
-               'nodes': vs.Any([node], str),
                # validation happens in NodeSetParser
                'nodeset': vs.Any(dict, str),
                'timeout': int,
@@ -489,13 +502,15 @@
         # Secrets are part of the playbook context so we must establish
         # them earlier than playbooks.
         secrets = []
-        for secret_config in conf.get('secrets', []):
+        for secret_config in as_list(conf.get('secrets', [])):
             if isinstance(secret_config, str):
                 secret_name = secret_config
-                secret = layout.secrets[secret_name]
+                secret = layout.secrets.get(secret_name)
             else:
                 secret_name = secret_config['name']
-                secret = layout.secrets[secret_config['secret']]
+                secret = layout.secrets.get(secret_config['secret'])
+            if secret is None:
+                raise SecretNotFoundError(secret_name)
             if secret_name == 'zuul':
                 raise Exception("Secrets named 'zuul' are not allowed.")
             if secret.source_context != job.source_context:
@@ -576,27 +591,15 @@
             conf_nodeset = conf['nodeset']
             if isinstance(conf_nodeset, str):
                 # This references an existing named nodeset in the layout.
-                ns = layout.nodesets[conf_nodeset]
+                ns = layout.nodesets.get(conf_nodeset)
+                if ns is None:
+                    raise NodesetNotFoundError(conf_nodeset)
             else:
                 ns = NodeSetParser.fromYaml(conf_nodeset, anonymous=True)
             if tenant.max_nodes_per_job != -1 and \
                len(ns) > tenant.max_nodes_per_job:
                 raise MaxNodeError(job, tenant)
             job.nodeset = ns
-        elif 'nodes' in conf:
-            conf_nodes = conf['nodes']
-            if isinstance(conf_nodes, str):
-                # This references an existing named nodeset in the layout.
-                ns = layout.nodesets[conf_nodes]
-            else:
-                ns = model.NodeSet()
-                for conf_node in conf_nodes:
-                    node = model.Node(conf_node['name'], conf_node['label'])
-                    ns.addNode(node)
-            if tenant.max_nodes_per_job != -1 and \
-               len(ns) > tenant.max_nodes_per_job:
-                raise MaxNodeError(job, tenant)
-            job.nodeset = ns
 
         if 'required-projects' in conf:
             new_projects = {}