Fully qualify project configuration names

The layout stores the configuration of a project in the ProjectConfig
class (not the Project class -- that represents the abstract idea
of a project independent of Zuul, the ProjectConfig represents a
particular Zuul configuration operating on that project).  Therefore,
in the continuing effort to fully qualify project names, index
ProjectConfig objects by their canonical project name.  Use that
name when looking for a ProjectConfig to find the jobs to run for
a given change.

Story: 2000953
Change-Id: I733a66369c969770e57c2fa8b30822bd15e1aca7
diff --git a/tests/base.py b/tests/base.py
index 16332bf..78b2ea0 100755
--- a/tests/base.py
+++ b/tests/base.py
@@ -1927,7 +1927,9 @@
     def getPipeline(self, name):
         return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
 
-    def updateConfigLayout(self, path):
+    def updateConfigLayout(self, path, project_repos=None):
+        if project_repos is None:
+            project_repos = []
         root = os.path.join(self.test_root, "config")
         if not os.path.exists(root):
             os.makedirs(root)
@@ -1939,7 +1941,26 @@
       gerrit:
         config-repos:
           - %s
-        """ % path)
+        project-repos:
+          - org/project
+          - org/project1
+          - org/project2
+          - org/project3
+          - org/project4
+          - org/project5
+          - org/project6
+          - org/one-job-project
+          - org/nonvoting-project
+          - org/templated-project
+          - org/layered-project
+          - org/node-project
+          - org/conflict-project
+          - org/noop-project
+          - org/experimental-project
+          - org/no-jobs-project\n""" % path)
+
+        for repo in project_repos:
+            f.write("          - %s\n" % repo)
         f.close()
         self.config.set('zuul', 'tenant_config',
                         os.path.join(FIXTURE_DIR, f.name))
diff --git a/tests/fixtures/config/duplicate-pipeline/main.yaml b/tests/fixtures/config/duplicate-pipeline/main.yaml
index ba2d8f5..5e1bc6e 100755
--- a/tests/fixtures/config/duplicate-pipeline/main.yaml
+++ b/tests/fixtures/config/duplicate-pipeline/main.yaml
@@ -4,3 +4,5 @@
       gerrit:
         config-repos:
           - common-config
+        project-repos:
+          - org/project
diff --git a/tests/fixtures/config/merges/main.yaml b/tests/fixtures/config/merges/main.yaml
index a22ed5c..039706f 100644
--- a/tests/fixtures/config/merges/main.yaml
+++ b/tests/fixtures/config/merges/main.yaml
@@ -4,3 +4,9 @@
       gerrit:
         config-repos:
           - common-config
+        project-repos:
+          - org/project-cherry-pick
+          - org/project-merge
+          - org/project-merge-branches
+          - org/project-merge-resolve
+
diff --git a/tests/fixtures/config/multi-tenant-semaphore/main.yaml b/tests/fixtures/config/multi-tenant-semaphore/main.yaml
index b1c47b1..7e05d13 100644
--- a/tests/fixtures/config/multi-tenant-semaphore/main.yaml
+++ b/tests/fixtures/config/multi-tenant-semaphore/main.yaml
@@ -5,6 +5,9 @@
         config-repos:
           - common-config
           - tenant-one-config
+        project-repos:
+          - org/project1
+          - org/project2
 
 - tenant:
     name: tenant-two
@@ -13,3 +16,6 @@
         config-repos:
           - common-config
           - tenant-two-config
+        project-repos:
+          - org/project1
+          - org/project2
diff --git a/tests/fixtures/config/multi-tenant/main.yaml b/tests/fixtures/config/multi-tenant/main.yaml
index b1c47b1..4ce2510 100644
--- a/tests/fixtures/config/multi-tenant/main.yaml
+++ b/tests/fixtures/config/multi-tenant/main.yaml
@@ -5,6 +5,8 @@
         config-repos:
           - common-config
           - tenant-one-config
+        project-repos:
+          - org/project1
 
 - tenant:
     name: tenant-two
@@ -13,3 +15,5 @@
         config-repos:
           - common-config
           - tenant-two-config
+        project-repos:
+          - org/project2
diff --git a/tests/fixtures/config/one-job-project/main.yaml b/tests/fixtures/config/one-job-project/main.yaml
index a22ed5c..2211390 100644
--- a/tests/fixtures/config/one-job-project/main.yaml
+++ b/tests/fixtures/config/one-job-project/main.yaml
@@ -4,3 +4,5 @@
       gerrit:
         config-repos:
           - common-config
+        project-repos:
+          - org/one-job-project
diff --git a/tests/fixtures/config/openstack/main.yaml b/tests/fixtures/config/openstack/main.yaml
index 95a0952..aa2615d 100644
--- a/tests/fixtures/config/openstack/main.yaml
+++ b/tests/fixtures/config/openstack/main.yaml
@@ -4,3 +4,6 @@
       gerrit:
         config-repos:
           - project-config
+        project-repos:
+          - openstack/nova
+          - openstack/keystone
\ No newline at end of file
diff --git a/tests/fixtures/config/requirements/email/main.yaml b/tests/fixtures/config/requirements/email/main.yaml
index a22ed5c..c388705 100644
--- a/tests/fixtures/config/requirements/email/main.yaml
+++ b/tests/fixtures/config/requirements/email/main.yaml
@@ -4,3 +4,6 @@
       gerrit:
         config-repos:
           - common-config
+        project-repos:
+          - org/project1
+          - org/project2
diff --git a/tests/fixtures/config/requirements/newer-than/main.yaml b/tests/fixtures/config/requirements/newer-than/main.yaml
index a22ed5c..c388705 100644
--- a/tests/fixtures/config/requirements/newer-than/main.yaml
+++ b/tests/fixtures/config/requirements/newer-than/main.yaml
@@ -4,3 +4,6 @@
       gerrit:
         config-repos:
           - common-config
+        project-repos:
+          - org/project1
+          - org/project2
diff --git a/tests/fixtures/config/requirements/older-than/main.yaml b/tests/fixtures/config/requirements/older-than/main.yaml
index a22ed5c..c388705 100644
--- a/tests/fixtures/config/requirements/older-than/main.yaml
+++ b/tests/fixtures/config/requirements/older-than/main.yaml
@@ -4,3 +4,6 @@
       gerrit:
         config-repos:
           - common-config
+        project-repos:
+          - org/project1
+          - org/project2
diff --git a/tests/fixtures/config/requirements/reject-username/main.yaml b/tests/fixtures/config/requirements/reject-username/main.yaml
index a22ed5c..c388705 100644
--- a/tests/fixtures/config/requirements/reject-username/main.yaml
+++ b/tests/fixtures/config/requirements/reject-username/main.yaml
@@ -4,3 +4,6 @@
       gerrit:
         config-repos:
           - common-config
+        project-repos:
+          - org/project1
+          - org/project2
diff --git a/tests/fixtures/config/requirements/reject/main.yaml b/tests/fixtures/config/requirements/reject/main.yaml
index a22ed5c..c388705 100644
--- a/tests/fixtures/config/requirements/reject/main.yaml
+++ b/tests/fixtures/config/requirements/reject/main.yaml
@@ -4,3 +4,6 @@
       gerrit:
         config-repos:
           - common-config
+        project-repos:
+          - org/project1
+          - org/project2
diff --git a/tests/fixtures/config/requirements/state/main.yaml b/tests/fixtures/config/requirements/state/main.yaml
index a22ed5c..70af14b 100644
--- a/tests/fixtures/config/requirements/state/main.yaml
+++ b/tests/fixtures/config/requirements/state/main.yaml
@@ -4,3 +4,7 @@
       gerrit:
         config-repos:
           - common-config
+        project-repos:
+          - current-project
+          - open-project
+          - status-project
diff --git a/tests/fixtures/config/requirements/username/main.yaml b/tests/fixtures/config/requirements/username/main.yaml
index a22ed5c..c388705 100644
--- a/tests/fixtures/config/requirements/username/main.yaml
+++ b/tests/fixtures/config/requirements/username/main.yaml
@@ -4,3 +4,6 @@
       gerrit:
         config-repos:
           - common-config
+        project-repos:
+          - org/project1
+          - org/project2
diff --git a/tests/fixtures/config/requirements/vote1/main.yaml b/tests/fixtures/config/requirements/vote1/main.yaml
index a22ed5c..c388705 100644
--- a/tests/fixtures/config/requirements/vote1/main.yaml
+++ b/tests/fixtures/config/requirements/vote1/main.yaml
@@ -4,3 +4,6 @@
       gerrit:
         config-repos:
           - common-config
+        project-repos:
+          - org/project1
+          - org/project2
diff --git a/tests/fixtures/config/requirements/vote2/main.yaml b/tests/fixtures/config/requirements/vote2/main.yaml
index a22ed5c..c388705 100644
--- a/tests/fixtures/config/requirements/vote2/main.yaml
+++ b/tests/fixtures/config/requirements/vote2/main.yaml
@@ -4,3 +4,6 @@
       gerrit:
         config-repos:
           - common-config
+        project-repos:
+          - org/project1
+          - org/project2
diff --git a/tests/fixtures/config/single-tenant/main.yaml b/tests/fixtures/config/single-tenant/main.yaml
index d9868fa..096e34e 100644
--- a/tests/fixtures/config/single-tenant/main.yaml
+++ b/tests/fixtures/config/single-tenant/main.yaml
@@ -6,3 +6,18 @@
           - common-config
         project-repos:
           - org/project
+          - org/project1
+          - org/project2
+          - org/project3
+          - org/project4
+          - org/project5
+          - org/project6
+          - org/one-job-project
+          - org/nonvoting-project
+          - org/templated-project
+          - org/layered-project
+          - org/node-project
+          - org/conflict-project
+          - org/noop-project
+          - org/experimental-project
+          - org/no-jobs-project
diff --git a/tests/fixtures/config/success-url/main.yaml b/tests/fixtures/config/success-url/main.yaml
index a22ed5c..841f74d 100644
--- a/tests/fixtures/config/success-url/main.yaml
+++ b/tests/fixtures/config/success-url/main.yaml
@@ -4,3 +4,5 @@
       gerrit:
         config-repos:
           - common-config
+        project-repos:
+          - org/docs
diff --git a/tests/fixtures/config/templated-project/main.yaml b/tests/fixtures/config/templated-project/main.yaml
index a22ed5c..3b297a7 100644
--- a/tests/fixtures/config/templated-project/main.yaml
+++ b/tests/fixtures/config/templated-project/main.yaml
@@ -4,3 +4,6 @@
       gerrit:
         config-repos:
           - common-config
+        project-repos:
+          - org/templated-project
+          - org/layered-project
diff --git a/tests/fixtures/config/zuul-connections-multiple-gerrits/git/common-config/zuul.yaml b/tests/fixtures/config/zuul-connections-multiple-gerrits/git/common-config/zuul.yaml
index 302dfcf..8353732 100644
--- a/tests/fixtures/config/zuul-connections-multiple-gerrits/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/zuul-connections-multiple-gerrits/git/common-config/zuul.yaml
@@ -33,10 +33,13 @@
     name: project-test2
 
 - project:
-    name: org/project1
+    name: review.example.com/org/project1
     review_check:
       jobs:
         - project-test1
+
+- project:
+    name: another.example.com/org/project1
     another_check:
       jobs:
         - project-test2
diff --git a/tests/fixtures/config/zuul-connections-multiple-gerrits/main.yaml b/tests/fixtures/config/zuul-connections-multiple-gerrits/main.yaml
index 730cc7e..72e43f5 100644
--- a/tests/fixtures/config/zuul-connections-multiple-gerrits/main.yaml
+++ b/tests/fixtures/config/zuul-connections-multiple-gerrits/main.yaml
@@ -4,3 +4,8 @@
       review_gerrit:
         config-repos:
           - common-config
+        project-repos:
+          - org/project1
+      another_gerrit:
+        project-repos:
+          - org/project1
diff --git a/tests/fixtures/config/zuultrigger/parent-change-enqueued/main.yaml b/tests/fixtures/config/zuultrigger/parent-change-enqueued/main.yaml
index a22ed5c..d9868fa 100644
--- a/tests/fixtures/config/zuultrigger/parent-change-enqueued/main.yaml
+++ b/tests/fixtures/config/zuultrigger/parent-change-enqueued/main.yaml
@@ -4,3 +4,5 @@
       gerrit:
         config-repos:
           - common-config
+        project-repos:
+          - org/project
diff --git a/tests/unit/test_model.py b/tests/unit/test_model.py
index a40054f..04b0e0a 100644
--- a/tests/unit/test_model.py
+++ b/tests/unit/test_model.py
@@ -165,6 +165,7 @@
         layout.addPipeline(pipeline)
         queue = model.ChangeQueue(pipeline)
         project = model.Project('project', self.source)
+        tenant.addProjectRepo(project)
 
         base = configloader.JobParser.fromYaml(tenant, layout, {
             '_source_context': self.context,
@@ -431,6 +432,7 @@
     def test_job_inheritance_job_tree(self):
         tenant = model.Tenant('tenant')
         layout = model.Layout()
+        tenant.addProjectRepo(self.project)
 
         pipeline = model.Pipeline('gate', layout)
         layout.addPipeline(pipeline)
@@ -511,6 +513,7 @@
         layout.addPipeline(pipeline)
         queue = model.ChangeQueue(pipeline)
         project = model.Project('project', self.source)
+        tenant.addProjectRepo(project)
 
         base = configloader.JobParser.fromYaml(tenant, layout, {
             '_source_context': self.context,
@@ -591,6 +594,7 @@
         self.layout.addJob(job)
 
         project2 = model.Project('project2', self.source)
+        self.tenant.addProjectRepo(project2)
         context2 = model.SourceContext(project2, 'master',
                                        'test', True)
 
diff --git a/tests/unit/test_scheduler.py b/tests/unit/test_scheduler.py
index 41e8463..e1d00f6 100755
--- a/tests/unit/test_scheduler.py
+++ b/tests/unit/test_scheduler.py
@@ -1498,8 +1498,8 @@
         # https://bugs.executepad.net/zuul/+bug/1078946
         # This test assumes the repo is already cloned; make sure it is
         tenant = self.sched.abide.tenants.get('tenant-one')
-        url = self.fake_gerrit.getGitUrl(
-            tenant.layout.project_configs.get('org/project1'))
+        trusted, project = tenant.getProject('org/project1')
+        url = self.fake_gerrit.getGitUrl(project)
         self.merge_server.merger.addProject('org/project1', url)
         A = self.fake_gerrit.addFakeChange('org/project1', 'master', 'A')
         A.addPatchset(large=True)
@@ -2881,7 +2881,7 @@
         self.assertEqual(A.reported, 2)
 
     def test_repo_deleted(self):
-        self.updateConfigLayout('layout-repo-deleted')
+        self.updateConfigLayout('layout-repo-deleted', ['org/delete-project'])
         self.sched.reconfigure(self.config)
 
         self.init_repo("org/delete-project")
diff --git a/zuul/configloader.py b/zuul/configloader.py
index 4e08c13..a6a2281 100644
--- a/zuul/configloader.py
+++ b/zuul/configloader.py
@@ -46,6 +46,17 @@
     pass
 
 
+class ProjectNotFoundError(Exception):
+    def __init__(self, project):
+        message = textwrap.dedent("""\
+        The project {project} was not found.  All projects
+        referenced within a Zuul configuration must first be
+        added to the main configuration file by the Zuul
+        administrator.""")
+        message = textwrap.fill(message.format(project=project))
+        super(ProjectNotFoundError, self).__init__(message)
+
+
 def indent(s):
     return '\n'.join(['  ' + x for x in s.split('\n')])
 
@@ -54,7 +65,7 @@
 def configuration_exceptions(stanza, conf):
     try:
         yield
-    except vs.Invalid as e:
+    except Exception as e:
         conf = copy.deepcopy(conf)
         context = conf.pop('_source_context')
         start_mark = conf.pop('_start_mark')
@@ -488,7 +499,13 @@
         for conf in conf_list:
             with configuration_exceptions('project', conf):
                 ProjectParser.getSchema(layout)(conf)
-        project = model.ProjectConfig(conf_list[0]['name'])
+
+        with configuration_exceptions('project', conf_list[0]):
+            project_name = conf_list[0]['name']
+            (trusted, project) = tenant.getProject(project_name)
+            if project is None:
+                raise ProjectNotFoundError(project_name)
+            project_config = model.ProjectConfig(project.canonical_name)
 
         configs = []
         for conf in conf_list:
@@ -504,14 +521,14 @@
                             for name in conf_templates])
             configs.append(project_template)
             mode = conf.get('merge-mode')
-            if mode and project.merge_mode is None:
+            if mode and project_config.merge_mode is None:
                 # Set the merge mode to the first one that we find and
                 # ignore subsequent settings.
-                project.merge_mode = model.MERGER_MAP[mode]
-        if project.merge_mode is None:
+                project_config.merge_mode = model.MERGER_MAP[mode]
+        if project_config.merge_mode is None:
             # If merge mode was not specified in any project stanza,
             # set it to the default.
-            project.merge_mode = model.MERGER_MAP['merge-resolve']
+            project_config.merge_mode = model.MERGER_MAP['merge-resolve']
         for pipeline in layout.pipelines.values():
             project_pipeline = model.ProjectPipelineConfig()
             queue_name = None
@@ -532,9 +549,8 @@
             if queue_name:
                 project_pipeline.queue_name = queue_name
             if pipeline_defined:
-                project.pipelines[pipeline.name] = project_pipeline
-
-        return project
+                project_config.pipelines[pipeline.name] = project_pipeline
+        return project_config
 
 
 class PipelineParser(object):
@@ -787,7 +803,6 @@
                                                   unparsed_config,
                                                   scheduler,
                                                   connections)
-        tenant.layout.tenant = tenant
         return tenant
 
     @staticmethod
@@ -992,6 +1007,8 @@
             layout.addProjectConfig(ProjectParser.fromYaml(
                 tenant, layout, config_project))
 
+        layout.tenant = tenant
+
         for pipeline in layout.pipelines.values():
             pipeline.manager._postConfig(layout)
 
diff --git a/zuul/driver/timer/__init__.py b/zuul/driver/timer/__init__.py
index 7ad8756..00ddb26 100644
--- a/zuul/driver/timer/__init__.py
+++ b/zuul/driver/timer/__init__.py
@@ -76,12 +76,12 @@
 
     def _onTrigger(self, tenant, pipeline_name, timespec):
         for project_name in tenant.layout.project_configs.keys():
+            project_hostname, project_name = project_name.split('/', 1)
             event = TriggerEvent()
             event.type = 'timer'
             event.timespec = timespec
             event.forced_pipeline = pipeline_name
-            # TODOv3(jeblair): add project hostname in future change
-            event.project_hostname = ''
+            event.project_hostname = project_hostname
             event.project_name = project_name
             self.log.debug("Adding event %s" % event)
             self.sched.addEvent(event)
diff --git a/zuul/executor/client.py b/zuul/executor/client.py
index 461af0b..0439126 100644
--- a/zuul/executor/client.py
+++ b/zuul/executor/client.py
@@ -52,7 +52,7 @@
                 url=item.pipeline.source.getGitUrl(
                     item.change.project),
                 connection_name=connection_name,
-                merge_mode=item.current_build_set.getMergeMode(project),
+                merge_mode=item.current_build_set.getMergeMode(),
                 refspec=refspec,
                 branch=branch,
                 ref=item.current_build_set.ref,
diff --git a/zuul/manager/__init__.py b/zuul/manager/__init__.py
index 9507d15..9661f54 100644
--- a/zuul/manager/__init__.py
+++ b/zuul/manager/__init__.py
@@ -461,7 +461,7 @@
                     url=self.pipeline.source.getGitUrl(
                         item.change.project),
                     connection_name=connection_name,
-                    merge_mode=item.current_build_set.getMergeMode(project),
+                    merge_mode=item.current_build_set.getMergeMode(),
                     refspec=refspec,
                     branch=branch,
                     ref=item.current_build_set.ref,
diff --git a/zuul/manager/dependent.py b/zuul/manager/dependent.py
index 4c48568..abba8da 100644
--- a/zuul/manager/dependent.py
+++ b/zuul/manager/dependent.py
@@ -38,13 +38,14 @@
         self.log.debug("Building shared change queues")
         change_queues = {}
         project_configs = self.pipeline.layout.project_configs
+        tenant = self.pipeline.layout.tenant
 
         for project_config in project_configs.values():
             project_pipeline_config = project_config.pipelines.get(
                 self.pipeline.name)
             if project_pipeline_config is None:
                 continue
-            project = self.pipeline.source.getProject(project_config.name)
+            (trusted, project) = tenant.getProject(project_config.name)
             queue_name = project_pipeline_config.queue_name
             if queue_name and queue_name in change_queues:
                 change_queue = change_queues[queue_name]
diff --git a/zuul/model.py b/zuul/model.py
index 77c5990..7dfd10f 100644
--- a/zuul/model.py
+++ b/zuul/model.py
@@ -1239,10 +1239,14 @@
     def getTries(self, job_name):
         return self.tries.get(job_name)
 
-    def getMergeMode(self, job_name):
-        if not self.layout or job_name not in self.layout.project_configs:
-            return MERGER_MERGE_RESOLVE
-        return self.layout.project_configs[job_name].merge_mode
+    def getMergeMode(self):
+        if self.layout:
+            project = self.item.change.project
+            project_config = self.layout.project_configs.get(
+                project.canonical_name)
+            if project_config:
+                return project_config.merge_mode
+        return MERGER_MERGE_RESOLVE
 
 
 class QueueItem(object):
@@ -1843,7 +1847,7 @@
         return self.project_hostname + '/' + self.project_name
 
     def __repr__(self):
-        ret = '<TriggerEvent %s %s' % (self.type, self.project_name)
+        ret = '<TriggerEvent %s %s' % (self.type, self.canonical_project_name)
 
         if self.branch:
             ret += " %s" % self.branch
@@ -2401,7 +2405,7 @@
 
     def createJobGraph(self, item):
         project_config = self.project_configs.get(
-            item.change.project.name, None)
+            item.change.project.canonical_name, None)
         ret = JobGraph()
         # NOTE(pabelanger): It is possible for a foreign project not to have a
         # configured pipeline, if so return an empty JobGraph.
diff --git a/zuul/scheduler.py b/zuul/scheduler.py
index 0fa1763..0f937e8 100644
--- a/zuul/scheduler.py
+++ b/zuul/scheduler.py
@@ -611,7 +611,7 @@
 
     def _doEnqueueEvent(self, event):
         tenant = self.abide.tenants.get(event.tenant_name)
-        project = tenant.layout.project_configs.get(event.project_name)
+        (trusted, project) = tenant.getProject(event.project_name)
         pipeline = tenant.layout.pipelines[event.forced_pipeline]
         change = pipeline.source.getChange(event, project)
         self.log.debug("Event %s for change %s was directly assigned "