Support cross-references based on zuul:attr

Add a new role: :zuul:attr: which will cross reference to a zuul:attr:
directive.

Change-Id: I69a65a9f4a0330f664f6183180872d459d234e72
diff --git a/doc/source/admin/drivers/gerrit.rst b/doc/source/admin/drivers/gerrit.rst
index 91a3510..bc203cb 100644
--- a/doc/source/admin/drivers/gerrit.rst
+++ b/doc/source/admin/drivers/gerrit.rst
@@ -192,7 +192,7 @@
 named *my-gerrit* must have a Code Review vote of +2 in order to be
 enqueued into the pipeline.
 
-.. zuul:attr:: pipeline.require.<source>
+.. zuul:attr:: pipeline.require.<gerrit source>
 
    The dictionary passed to the Gerrit pipeline `require` attribute
    supports the following attributes:
@@ -250,7 +250,7 @@
       A string value that corresponds with the status of the change
       reported by the trigger.
 
-.. zuul:attr:: pipeline.reject.<source>
+.. zuul:attr:: pipeline.reject.<gerrit source>
 
    The `reject` attribute is the mirror of the `require` attribute.  It
    also accepts a dictionary under the connection name.  This
diff --git a/doc/source/admin/drivers/github.rst b/doc/source/admin/drivers/github.rst
index c884be2..97492f6 100644
--- a/doc/source/admin/drivers/github.rst
+++ b/doc/source/admin/drivers/github.rst
@@ -216,7 +216,7 @@
 named *my-github* must have an approved code review in order to be
 enqueued into the pipeline.
 
-.. zuul:attr:: pipeline.require.<source>
+.. zuul:attr:: pipeline.require.<github source>
 
    The dictionary passed to the GitHub pipeline `require` attribute
    supports the following attributes:
@@ -290,7 +290,7 @@
       indicated label (or labels).
 
 
-.. zuul:attr:: pipeline.reject.<source>
+.. zuul:attr:: pipeline.reject.<github source>
 
    The `reject` attribute is the mirror of the `require` attribute.  It
    also accepts a dictionary under the connection name.  This
diff --git a/zuul/sphinx/zuul.py b/zuul/sphinx/zuul.py
index 976d58a..4a44815 100644
--- a/zuul/sphinx/zuul.py
+++ b/zuul/sphinx/zuul.py
@@ -14,7 +14,12 @@
 
 from sphinx import addnodes
 from sphinx.domains import Domain
+from sphinx.roles import XRefRole
 from sphinx.directives import ObjectDescription
+from sphinx.util.nodes import make_refnode
+from docutils import nodes
+
+from typing import Dict # noqa
 
 
 class ZuulConfigObject(ObjectDescription):
@@ -45,6 +50,15 @@
             signode['ids'].append(targetname)
             signode['first'] = (not self.names)
             self.state.document.note_explicit_target(signode)
+            objects = self.env.domaindata['zuul']['objects']
+            if targetname in objects:
+                self.state_machine.reporter.warning(
+                    'duplicate object description of %s, ' % targetname +
+                    'other instance in ' +
+                    self.env.doc2path(objects[targetname][0]) +
+                    ', use :noindex: for one of them',
+                    line=self.lineno)
+            objects[targetname] = (self.env.docname, self.objtype)
 
         objname = self.object_names.get(self.objtype, self.objtype)
         if self.parent_pathname:
@@ -99,6 +113,29 @@
         'value': ZuulValueDirective,
     }
 
+    roles = {
+        'attr': XRefRole(innernodeclass=nodes.inline,  # type: ignore
+                         warn_dangling=True),
+    }
+
+    initial_data = {
+        'objects': {},
+    }  # type: Dict[str, Dict]
+
+    def resolve_xref(self, env, fromdocname, builder, type, target,
+                     node, contnode):
+        objects = self.data['objects']
+        name = type + '-' + target
+        obj = objects.get(name)
+        if obj:
+            return make_refnode(builder, fromdocname, obj[0], name,
+                                contnode, name)
+
+    def clear_doc(self, docname):
+        for fullname, (fn, _l) in list(self.data['objects'].items()):
+            if fn == docname:
+                del self.data['objects'][fullname]
+
 
 def setup(app):
     app.add_domain(ZuulDomain)