Use per-project settings for Zuul v3 support

In Zuul v3, the whole API is split by the tenant name; there is no
possibility to request status of a change without knowing the tenant
name.

This patch adapts Tricium's code for per-project setting retrieval. The
top-level global variable in site-path/etc/gerrit.config is no longer
used. Given that this is a fresh plugin, I think it's safe to make that
change now without any extra compatibility code. The value can be set
just as easily in All-Project's refs/meta/config.

It was also necessary to remove that extra JSON Content-Type header.
Without that, Firefox would complain about Access-Control-Allow-Headers
not allowing the `content-type`. It seems that there's no need to send
these headers in the first place.

The curret version is able to show the status dashboard for the ucurrent
job in both Zuul v2 and Zuul v3. In v2, links point to individual job's
logs. For v3, they go to a not-that-useful page with job description.
That will be fixed later.

I also plan to expand this a bit for Zuul v3 because it supports
querying build results even after all jobs have concluded. I hope to
offer a nice and simple but persistent indication of job results so that
one could configure PolyGerrit to hide bot's comments by default, only
relying on the zuul-status' output as a result matrix. That's future
work, though.

Change-Id: I93b4d6fcd7c312ddc543bff42522c82be77d7a76
diff --git a/src/main/java/com/googlesource/gerrit/plugins/zuulstatus/GetConfig.java b/src/main/java/com/googlesource/gerrit/plugins/zuulstatus/GetConfig.java
index dae676d..1622755 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/zuulstatus/GetConfig.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/zuulstatus/GetConfig.java
@@ -16,28 +16,38 @@
 
 import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.server.config.ConfigResource;
 import com.google.gerrit.server.config.PluginConfig;
 import com.google.gerrit.server.config.PluginConfigFactory;
+import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.gerrit.server.project.ProjectResource;
 import com.google.inject.Inject;
+import com.google.inject.Singleton;
 
-public class GetConfig implements RestReadView<ConfigResource> {
+@Singleton
+public class GetConfig implements RestReadView<ProjectResource> {
 
-  private final PluginConfig cfg;
+  private final PluginConfigFactory config;
+  private final String pluginName;
 
   @Inject
   public GetConfig(PluginConfigFactory cfgFactory, @PluginName String pluginName) {
-    this.cfg = cfgFactory.getFromGerritConfig(pluginName);
+    this.config = cfgFactory;
+    this.pluginName = pluginName;
   }
 
   @Override
-  public ConfigInfo apply(ConfigResource resource) {
+  public ConfigInfo apply(ProjectResource project) throws NoSuchProjectException {
+    PluginConfig cfg = config.getFromProjectConfigWithInheritance(
+        project.getNameKey(), pluginName);
+
     ConfigInfo info = new ConfigInfo();
-    info.zuulUrl = cfg.getString("zuulUrl", null);
+    info.zuulUrl = cfg.getString("url", null);
+    info.zuulTenant = cfg.getString("tenant", null);
     return info;
   }
 
   public static class ConfigInfo {
     String zuulUrl;
+    String zuulTenant;
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/zuulstatus/Module.java b/src/main/java/com/googlesource/gerrit/plugins/zuulstatus/Module.java
index e2e3d91..59dcff6 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/zuulstatus/Module.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/zuulstatus/Module.java
@@ -14,26 +14,30 @@
 
 package com.googlesource.gerrit.plugins.zuulstatus;
 
-import static com.google.gerrit.server.config.ConfigResource.CONFIG_KIND;
+import static com.google.gerrit.server.project.ProjectResource.PROJECT_KIND;
 
+import com.google.gerrit.extensions.annotations.Exports;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.restapi.RestApiModule;
 import com.google.gerrit.extensions.webui.JavaScriptPlugin;
 import com.google.gerrit.extensions.webui.WebUiPlugin;
-import com.google.inject.AbstractModule;
+import com.google.gerrit.server.config.ProjectConfigEntry;
 
-public class Module extends AbstractModule {
+public class Module extends RestApiModule {
   @Override
   protected void configure() {
-    install(
-        new RestApiModule() {
-          @Override
-          protected void configure() {
-            get(CONFIG_KIND, "config").to(GetConfig.class);
-          }
-        });
-
     DynamicSet.bind(binder(), WebUiPlugin.class)
         .toInstance(new JavaScriptPlugin("zuul-status.html"));
+
+    get(PROJECT_KIND, "config").to(GetConfig.class);
+
+    // TODO: these annotations only apply to GWT UIs...
+    bind(ProjectConfigEntry.class)
+	.annotatedWith(Exports.named("url"))
+	.toInstance(new ProjectConfigEntry("Top-level Zuul URL", null));
+
+    bind(ProjectConfigEntry.class)
+	.annotatedWith(Exports.named("tenant"))
+	.toInstance(new ProjectConfigEntry("Zuul v3 tenant name -- leave empty on Zuul v2", null));
   }
 }
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index 3dacbe1..2babffe 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -1,15 +1,39 @@
 Configuration
 =============
 
-The configuration of the @PLUGIN@ plugin is done in the `gerrit.config`
-file.
+The configuration of the @PLUGIN@ plugin is done via the `project.config`
+stored in each project's `refs/meta/config` branch. Configuration is subject to
+inheritance from parent projects as per usual rules.
+
+Zuul v2:
+--------
+
+Specify path including the API subdirectory:
 
 ```
 [plugin "@PLUGIN@"]
-zuulUrl = https://example.org/zuul/status/change/
+url = https://example.org/zuul-v2/status/change/
+```
+
+Zuul v3:
+--------
+
+Zuul v3 added support for multi-tenancy. The `url` contains just the root path
+of the Zuul web application. Use the `tenant` to set the tenant name:
+
+```
+[plugin "@PLUGIN@"]
+url = https://zuul.example.org/
+tenant = public
 ```
 
 <a id="show-zuul-url">
-`plugin.@PLUGIN@.zuulUrl`
-:    Url to zuul api.
+`plugin.@PLUGIN@.url`
+:    URL to Zuul's root (for Zuul v3), or an URL to Zuul v2's API.
+By default not set.
+
+<a id="show-zuul-tenant">
+`plugin.@PLUGIN@.tenant`
+:    Tenant name to be used within Zuul.
+Leave this unset on Zuul v2. Set this to a valid tenant name if using Zuul v3.
 By default not set.
diff --git a/src/main/resources/static/zuul-status-view.js b/src/main/resources/static/zuul-status-view.js
index de873ae..b10dec7 100644
--- a/src/main/resources/static/zuul-status-view.js
+++ b/src/main/resources/static/zuul-status-view.js
@@ -62,6 +62,10 @@
     is: 'zuul-status-view',
     properties: {
       zuulUrl: String,
+      zuulTenant: {
+        type: String,
+        value: null,
+      },
       plugin: Object,
       change: Object,
       revision: {
@@ -118,12 +122,19 @@
         this.set('zuulDisable', false);
       }
 
-      const config = await this.getConfig();
+      const project = this.change.project;
+      const plugin = this.plugin.getPluginName();
+      const config = await this.getConfig(project, plugin);
       if (config && config.zuul_url) {
         this.zuulUrl = config.zuul_url;
+        if (config.zuul_tenant) {
+          this.zuulTenant = config.zuul_tenant;
+          console.info(`zuul-status: Zuul v3 at ${this.zuulUrl}, tenant ${this.zuulTenant}`);
+        } else {
+          console.info(`zuul-status: Zuul v2 at ${this.zuulUrl}`);
+        }
       } else {
-        console.info("No config found for plugin zuul-status at endpoint " +
-            "/config/server/info");
+        console.info("No config found for plugin zuul-status");
       }
       if (this.zuulUrl) {
         await this._update();
@@ -136,8 +147,10 @@
      * @return {Promise} Resolves to the fetched config object,
      *     or rejects if the response is non-OK.
      */
-    async getConfig() {
-      return await this.plugin.restApi().get('/config/server/config');
+    async getConfig(project, plugin) {
+      return await this.plugin.restApi().get(
+              `/projects/${encodeURIComponent(project)}` +
+              `/${encodeURIComponent(plugin)}~config`);
     },
 
     /**
@@ -191,10 +204,10 @@
      * @return {Promise} Resolves to a fetch Response object.
      */
     async _getReponse(change, revision) {
-      const url = `${this.zuulUrl}${change._number},${revision._number}`
+      const url = this.zuulTenant === null ?
+        `${this.zuulUrl}${change._number},${revision._number}` :
+        `${this.zuulUrl}/api/tenant/${this.zuulTenant}/status/change/${change._number},${revision._number}`;
       const options = {method: 'GET'};
-      options.headers = new Headers();
-      options.headers.set('Content-Type', 'application/json');
 
       return await fetch(url, options);
     },