Merge branch 'stable-3.3'
* stable-3.3:
Initial implementation
Change-Id: I1696933a2c1a870ba71eb6835e0fa281d0581eb2
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..659c9e1
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,18 @@
+# Compiled class file
+*.class
+
+# Log file
+*.log
+
+# IJ
+.idea
+.ijwb/
+
+# Package Files #
+*.jar
+*.war
+*.nar
+*.ear
+*.zip
+*.tar.gz
+*.rar
\ No newline at end of file
diff --git a/BUILD b/BUILD
new file mode 100644
index 0000000..7bd22d4
--- /dev/null
+++ b/BUILD
@@ -0,0 +1,50 @@
+load(
+ "//tools/bzl:plugin.bzl",
+ "PLUGIN_DEPS",
+ "PLUGIN_TEST_DEPS",
+ "gerrit_plugin",
+)
+load("//tools/bzl:junit.bzl", "junit_tests")
+
+gerrit_plugin(
+ name = "replication-status",
+ srcs = glob(["src/main/java/**/*.java"]),
+ manifest_entries = [
+ "Gerrit-PluginName: replication-status",
+ "Gerrit-Module: com.googlesource.gerrit.plugins.replicationstatus.Module",
+ "Implementation-Title: Replication Status",
+ "Implementation-URL: https://gerrit.googlesource.com/plugins/replication-status",
+ ],
+ resources = glob(["src/main/resources/**/*"]),
+ deps = [
+ ":replication-neverlink",
+ "//java/com/google/gerrit/proto",
+ "//plugins/replication-status/proto:replication_status_cache_java_proto",
+ "@error-prone-annotations//jar",
+ ],
+)
+
+junit_tests(
+ name = "replicationstatus_tests",
+ srcs = glob(["src/test/java/**/*.java"]),
+ resources = glob(["src/test/resources/**/*"]),
+ deps = [
+ ":replicationstatus__plugin_test_deps",
+ ],
+)
+
+java_library(
+ name = "replication-neverlink",
+ neverlink = 1,
+ exports = ["//plugins/replication"],
+)
+
+java_library(
+ name = "replicationstatus__plugin_test_deps",
+ testonly = 1,
+ visibility = ["//visibility:public"],
+ exports = PLUGIN_DEPS + PLUGIN_TEST_DEPS + [
+ ":replication-status__plugin",
+ "//plugins/replication",
+ ],
+)
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..261eeb9
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/README.md b/README.md
index 5f59fa5..a5c70e0 100644
--- a/README.md
+++ b/README.md
@@ -2,3 +2,85 @@
Record and display the repository's replication status without having to dig
into the Gerrit replication_log
+
+Consumes replication events and updates a cache with the latest replication
+status of specific refs to specific remotes.
+
+The cache information is then exposed via a project's resource REST endpoint:
+
+```bash
+curl -v --user <user> '<gerrit-server>/a/projects/<project-name>/remotes/<remote-url>/replication-status'
+```
+
+* <project-name>: an (url-encoded) project repository
+* <remote-url>: an (url-encoded) remote URL for the replication
+
+For instance, to assess the replication status of the project `some/project` to
+the
+`https://github.com/some/project.git` URL, the following endpoint should be
+called:
+
+```bash
+curl -v --user <user> '<gerrit-server>/a/projects/some%2Fproject/remotes/https%3A%2F%2Fgithub.com%2Fsome%2Fproject.git/replication-status'
+```
+
+A payload, similar to this may be returned:
+
+```
+{
+ "remotes": {
+ "https://github.com/some/project.git": {
+ "status": {
+ "refs/changes/01/1/meta": {
+ "status": "SUCCEEDED",
+ "when": 1626688830
+ },
+ "refs/changes/03/3/meta": {
+ "status": "SUCCEEDED",
+ "when": 1626688854
+ },
+ "refs/changes/03/3/1": {
+ "status": "SUCCEEDED",
+ "when": 1626688854
+ },
+ "refs/changes/02/2/1": {
+ "status": "SUCCEEDED",
+ "when": 1626688844
+ },
+ "refs/changes/02/2/meta": {
+ "status": "SUCCEEDED",
+ "when": 1626688844
+ },
+ "refs/changes/01/1/1": {
+ "status": "SUCCEEDED",
+ "when": 1626688830
+ }
+ }
+ }
+ },
+ "status": "OK",
+ "project": "some/project"
+}
+```
+
+### HTTP status
+
+The endpoint returns different HTTP response code depending on the result:
+
+* 200 OK - The endpoint was called successfully, and a payload returned
+* 404 Not Found - Project was not found
+* 500 Failure - An unexpected server error occurred
+* 403 Forbidden - The user has no permission to query the endpoint. Only
+ Administrators and project owners are allowed
+
+### Overall status
+
+The REST-API response shows a `status` field, which shows the overall
+replication-status of the projects for the specified remote.
+
+- `OK` - all the refs have successfully replicated
+- `FAILED` - Some refs have not replicated successfully
+
+### TODO
+
+* Does not consume pull-replication events.
diff --git a/proto/BUILD b/proto/BUILD
new file mode 100644
index 0000000..c8e0c74
--- /dev/null
+++ b/proto/BUILD
@@ -0,0 +1,13 @@
+load("@rules_java//java:defs.bzl", "java_proto_library")
+load("@rules_proto//proto:defs.bzl", "proto_library")
+
+proto_library(
+ name = "replication_status_cache_proto",
+ srcs = ["cache.proto"],
+)
+
+java_proto_library(
+ name = "replication_status_cache_java_proto",
+ visibility = ["//visibility:public"],
+ deps = [":replication_status_cache_proto"],
+)
diff --git a/proto/cache.proto b/proto/cache.proto
new file mode 100644
index 0000000..6206ac0
--- /dev/null
+++ b/proto/cache.proto
@@ -0,0 +1,36 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+syntax = "proto3";
+
+package replicationstatus.cache;
+
+option java_package = "com.googlesource.gerrit.plugins.replicationstatus.proto";
+
+// Serialized form of com.googlesource.gerrit.plugins.replicationstatus.ReplicationStatus.Key.
+// Next ID: 4
+message ReplicationStatusKeyProto {
+ string project = 1;
+ string remote = 2;
+ string ref = 3;
+}
+
+// Serialized form of com.googlesource.gerrit.plugins.replicationstatus.ReplicationStatus.
+// Next ID: 3
+message ReplicationStatusProto {
+ int64 when = 1;
+ string status = 2;
+}
+
+
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replicationstatus/EventHandler.java b/src/main/java/com/googlesource/gerrit/plugins/replicationstatus/EventHandler.java
new file mode 100644
index 0000000..b197d7f
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replicationstatus/EventHandler.java
@@ -0,0 +1,64 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.replicationstatus;
+
+import static com.googlesource.gerrit.plugins.replicationstatus.ReplicationStatus.ReplicationStatusResult;
+
+import com.google.common.cache.Cache;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.server.config.GerritInstanceId;
+import com.google.gerrit.server.events.Event;
+import com.google.gerrit.server.events.EventListener;
+import com.google.inject.Inject;
+import com.google.inject.name.Named;
+import com.googlesource.gerrit.plugins.replication.events.RefReplicatedEvent;
+
+class EventHandler implements EventListener {
+ private final Cache<ReplicationStatus.Key, ReplicationStatus> replicationStatusCache;
+ private final String nodeInstanceId;
+
+ @Inject
+ EventHandler(
+ @Named(ReplicationStatus.CACHE_NAME)
+ Cache<ReplicationStatus.Key, ReplicationStatus> replicationStatusCache,
+ @Nullable @GerritInstanceId String nodeInstanceId) {
+ this.replicationStatusCache = replicationStatusCache;
+ this.nodeInstanceId = nodeInstanceId;
+ }
+
+ @Override
+ public void onEvent(Event event) {
+ if (shouldConsume(event)) {
+ if (event instanceof RefReplicatedEvent) {
+ RefReplicatedEvent replEvent = (RefReplicatedEvent) event;
+
+ ReplicationStatus.Key cacheKey =
+ ReplicationStatus.Key.create(
+ Project.nameKey(replEvent.project), replEvent.targetUri, replEvent.ref);
+
+ replicationStatusCache.put(
+ cacheKey,
+ ReplicationStatus.create(
+ ReplicationStatusResult.fromString(replEvent.status), replEvent.eventCreatedOn));
+ }
+ }
+ }
+
+ private boolean shouldConsume(Event event) {
+ return nodeInstanceId == null && event.instanceId == null
+ || nodeInstanceId != null && nodeInstanceId.equals(event.instanceId);
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replicationstatus/Module.java b/src/main/java/com/googlesource/gerrit/plugins/replicationstatus/Module.java
new file mode 100644
index 0000000..9109e55
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replicationstatus/Module.java
@@ -0,0 +1,28 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.replicationstatus;
+
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.server.events.EventListener;
+
+class Module extends LifecycleModule {
+ @Override
+ protected void configure() {
+ DynamicSet.bind(binder(), EventListener.class).to(EventHandler.class);
+ install(new ReplicationStatusApiModule());
+ install(new ReplicationStatusCacheModule());
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replicationstatus/ProjectReplicationStatus.java b/src/main/java/com/googlesource/gerrit/plugins/replicationstatus/ProjectReplicationStatus.java
new file mode 100644
index 0000000..a2c35eb
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replicationstatus/ProjectReplicationStatus.java
@@ -0,0 +1,39 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.replicationstatus;
+
+import com.google.auto.value.AutoValue;
+import java.util.Map;
+
+@AutoValue
+public abstract class ProjectReplicationStatus {
+ static ProjectReplicationStatus create(
+ Map<String, RemoteReplicationStatus> remotes,
+ ProjectReplicationStatusResult status,
+ String project) {
+ return new AutoValue_ProjectReplicationStatus(remotes, status, project);
+ }
+
+ public abstract Map<String, RemoteReplicationStatus> remotes();
+
+ public abstract ProjectReplicationStatusResult status();
+
+ public abstract String project();
+
+ enum ProjectReplicationStatusResult {
+ FAILED,
+ OK;
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replicationstatus/RemoteReplicationStatus.java b/src/main/java/com/googlesource/gerrit/plugins/replicationstatus/RemoteReplicationStatus.java
new file mode 100644
index 0000000..f567e5e
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replicationstatus/RemoteReplicationStatus.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.replicationstatus;
+
+import com.google.auto.value.AutoValue;
+import java.util.Map;
+
+@AutoValue
+public abstract class RemoteReplicationStatus {
+ static RemoteReplicationStatus create(Map<String, ReplicationStatus> status) {
+ return new AutoValue_RemoteReplicationStatus(status);
+ }
+
+ public abstract Map<String, ReplicationStatus> status();
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replicationstatus/ReplicationStatus.java b/src/main/java/com/googlesource/gerrit/plugins/replicationstatus/ReplicationStatus.java
new file mode 100644
index 0000000..438edc2
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replicationstatus/ReplicationStatus.java
@@ -0,0 +1,123 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.replicationstatus;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.proto.Protos;
+import com.google.gerrit.server.cache.serialize.CacheSerializer;
+import com.googlesource.gerrit.plugins.replicationstatus.proto.Cache;
+
+@AutoValue
+public abstract class ReplicationStatus {
+ static final String CACHE_NAME = "replication_status";
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ static ReplicationStatus create(ReplicationStatusResult status, long when) {
+ return new AutoValue_ReplicationStatus(status, when);
+ }
+
+ public abstract ReplicationStatusResult status();
+
+ public abstract long when();
+
+ public boolean isFailure() {
+ return status().isFailure();
+ }
+
+ @AutoValue
+ public abstract static class Key {
+ static ReplicationStatus.Key create(Project.NameKey projectName, String remote, String ref) {
+ return new AutoValue_ReplicationStatus_Key(projectName, remote, ref);
+ }
+
+ abstract Project.NameKey projectName();
+
+ abstract String remote();
+
+ abstract String ref();
+
+ enum Serializer implements CacheSerializer<ReplicationStatus.Key> {
+ INSTANCE;
+
+ @Override
+ public byte[] serialize(ReplicationStatus.Key object) {
+ return Protos.toByteArray(
+ Cache.ReplicationStatusKeyProto.newBuilder()
+ .setProject(object.projectName().get())
+ .setRemote(object.remote())
+ .setRef(object.ref())
+ .build());
+ }
+
+ @Override
+ public ReplicationStatus.Key deserialize(byte[] in) {
+ Cache.ReplicationStatusKeyProto proto =
+ Protos.parseUnchecked(Cache.ReplicationStatusKeyProto.parser(), in);
+ return ReplicationStatus.Key.create(
+ Project.nameKey(proto.getProject()), proto.getRemote(), proto.getRef());
+ }
+ }
+ }
+
+ enum Serializer implements CacheSerializer<ReplicationStatus> {
+ INSTANCE;
+
+ @Override
+ public byte[] serialize(ReplicationStatus object) {
+ return Protos.toByteArray(
+ Cache.ReplicationStatusProto.newBuilder()
+ .setWhen(object.when())
+ .setStatus(object.status().name())
+ .build());
+ }
+
+ @Override
+ public ReplicationStatus deserialize(byte[] in) {
+ Cache.ReplicationStatusProto proto =
+ Protos.parseUnchecked(Cache.ReplicationStatusProto.parser(), in);
+
+ return ReplicationStatus.create(
+ ReplicationStatus.ReplicationStatusResult.valueOf(proto.getStatus()), proto.getWhen());
+ }
+ }
+
+ enum ReplicationStatusResult {
+ FAILED,
+ NOT_ATTEMPTED,
+ SUCCEEDED,
+ UNKNOWN;
+
+ static ReplicationStatusResult fromString(String result) {
+ switch (result.toLowerCase()) {
+ case "succeeded":
+ return SUCCEEDED;
+ case "not_attempted":
+ return NOT_ATTEMPTED;
+ case "failed":
+ return FAILED;
+ default:
+ logger.atSevere().log(
+ "Could not parse result into a valid replication status: %", result);
+ return UNKNOWN;
+ }
+ }
+
+ public boolean isFailure() {
+ return this == FAILED || this == UNKNOWN;
+ }
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replicationstatus/ReplicationStatusAction.java b/src/main/java/com/googlesource/gerrit/plugins/replicationstatus/ReplicationStatusAction.java
new file mode 100644
index 0000000..7691897
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replicationstatus/ReplicationStatusAction.java
@@ -0,0 +1,105 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.replicationstatus;
+
+import static com.googlesource.gerrit.plugins.replicationstatus.ReplicationStatus.CACHE_NAME;
+
+import com.google.common.cache.Cache;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.permissions.ProjectPermission;
+import com.google.inject.Inject;
+import com.google.inject.name.Named;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+
+class ReplicationStatusAction implements RestReadView<ReplicationStatusProjectRemoteResource> {
+ private final PermissionBackend permissionBackend;
+ private final GitRepositoryManager repoManager;
+ private final Cache<ReplicationStatus.Key, ReplicationStatus> replicationStatusCache;
+
+ @Inject
+ ReplicationStatusAction(
+ PermissionBackend permissionBackend,
+ GitRepositoryManager repoManager,
+ @Named(CACHE_NAME) Cache<ReplicationStatus.Key, ReplicationStatus> replicationStatusCache) {
+ this.permissionBackend = permissionBackend;
+ this.repoManager = repoManager;
+ this.replicationStatusCache = replicationStatusCache;
+ }
+
+ @Override
+ public Response<ProjectReplicationStatus> apply(ReplicationStatusProjectRemoteResource resource)
+ throws AuthException, PermissionBackendException, BadRequestException,
+ ResourceConflictException, IOException {
+
+ Project.NameKey projectNameKey = resource.getProjectNameKey();
+ String remoteURL = resource.getRemoteUrl();
+
+ checkIsOwnerOrAdmin(projectNameKey);
+
+ ProjectReplicationStatus.ProjectReplicationStatusResult overallStatus =
+ ProjectReplicationStatus.ProjectReplicationStatusResult.OK;
+ Map<String, RemoteReplicationStatus> remoteStatuses = new HashMap<>();
+ try (Repository git = repoManager.openRepository(projectNameKey)) {
+
+ Map<String, ReplicationStatus> refStatuses = new HashMap<>();
+ for (Ref r : git.getRefDatabase().getRefs()) {
+ ReplicationStatus replicationStatus =
+ replicationStatusCache.getIfPresent(
+ ReplicationStatus.Key.create(projectNameKey, remoteURL, r.getName()));
+
+ if (replicationStatus != null) {
+ refStatuses.put(r.getName(), replicationStatus);
+ if (replicationStatus.isFailure()) {
+ overallStatus = ProjectReplicationStatus.ProjectReplicationStatusResult.FAILED;
+ }
+ }
+ }
+ remoteStatuses.put(remoteURL, RemoteReplicationStatus.create(refStatuses));
+
+ ProjectReplicationStatus projectStatus =
+ ProjectReplicationStatus.create(remoteStatuses, overallStatus, projectNameKey.get());
+
+ return Response.ok(projectStatus);
+
+ } catch (RepositoryNotFoundException e) {
+ throw new BadRequestException(
+ String.format("Project %s does not exist", projectNameKey.get()));
+ }
+ }
+
+ private void checkIsOwnerOrAdmin(Project.NameKey project) throws AuthException {
+ if (!permissionBackend.currentUser().testOrFalse(GlobalPermission.ADMINISTRATE_SERVER)
+ && !permissionBackend
+ .currentUser()
+ .project(project)
+ .testOrFalse(ProjectPermission.WRITE_CONFIG)) {
+ throw new AuthException("Administrate Server or Project owner required");
+ }
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replicationstatus/ReplicationStatusApiModule.java b/src/main/java/com/googlesource/gerrit/plugins/replicationstatus/ReplicationStatusApiModule.java
new file mode 100644
index 0000000..adecc6d
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replicationstatus/ReplicationStatusApiModule.java
@@ -0,0 +1,33 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.replicationstatus;
+
+import static com.google.gerrit.server.project.ProjectResource.PROJECT_KIND;
+import static com.googlesource.gerrit.plugins.replicationstatus.ReplicationStatusProjectRemoteResource.REPLICATION_STATUS_PROJECT_REMOTE_KIND;
+
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.RestApiModule;
+import com.google.inject.Scopes;
+
+class ReplicationStatusApiModule extends RestApiModule {
+ @Override
+ protected void configure() {
+ bind(ReplicationStatusAction.class).in(Scopes.SINGLETON);
+ DynamicMap.mapOf(binder(), REPLICATION_STATUS_PROJECT_REMOTE_KIND);
+ child(PROJECT_KIND, "remotes").to(ReplicationStatusProjectRemoteCollection.class);
+ get(REPLICATION_STATUS_PROJECT_REMOTE_KIND, "replication-status")
+ .to(ReplicationStatusAction.class);
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replicationstatus/ReplicationStatusCacheModule.java b/src/main/java/com/googlesource/gerrit/plugins/replicationstatus/ReplicationStatusCacheModule.java
new file mode 100644
index 0000000..fa5a472
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replicationstatus/ReplicationStatusCacheModule.java
@@ -0,0 +1,29 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.replicationstatus;
+
+import com.google.gerrit.server.cache.CacheModule;
+
+public class ReplicationStatusCacheModule extends CacheModule {
+
+ @Override
+ protected void configure() {
+ persist(ReplicationStatus.CACHE_NAME, ReplicationStatus.Key.class, ReplicationStatus.class)
+ .version(1)
+ .diskLimit(-1)
+ .keySerializer(ReplicationStatus.Key.Serializer.INSTANCE)
+ .valueSerializer(ReplicationStatus.Serializer.INSTANCE);
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replicationstatus/ReplicationStatusProjectRemoteCollection.java b/src/main/java/com/googlesource/gerrit/plugins/replicationstatus/ReplicationStatusProjectRemoteCollection.java
new file mode 100644
index 0000000..a8c0b0c
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replicationstatus/ReplicationStatusProjectRemoteCollection.java
@@ -0,0 +1,58 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.replicationstatus;
+
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.ChildCollection;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.NotImplementedException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.server.project.ProjectResource;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+@Singleton
+public class ReplicationStatusProjectRemoteCollection
+ implements ChildCollection<ProjectResource, ReplicationStatusProjectRemoteResource> {
+ private final DynamicMap<RestView<ReplicationStatusProjectRemoteResource>> views;
+
+ @Inject
+ ReplicationStatusProjectRemoteCollection(
+ DynamicMap<RestView<ReplicationStatusProjectRemoteResource>> views) {
+ this.views = views;
+ }
+
+ @Override
+ public RestView<ProjectResource> list() throws RestApiException {
+ throw new NotImplementedException();
+ }
+
+ @Override
+ public ReplicationStatusProjectRemoteResource parse(ProjectResource parent, IdString id)
+ throws ResourceNotFoundException, Exception {
+ Project.NameKey projectNameKey = parent.getNameKey();
+ String remoteURL = id.get();
+
+ return new ReplicationStatusProjectRemoteResource(projectNameKey, remoteURL, remoteURL);
+ }
+
+ @Override
+ public DynamicMap<RestView<ReplicationStatusProjectRemoteResource>> views() {
+ return views;
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replicationstatus/ReplicationStatusProjectRemoteResource.java b/src/main/java/com/googlesource/gerrit/plugins/replicationstatus/ReplicationStatusProjectRemoteResource.java
new file mode 100644
index 0000000..77b81cd
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replicationstatus/ReplicationStatusProjectRemoteResource.java
@@ -0,0 +1,49 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.replicationstatus;
+
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.restapi.RestResource;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.inject.TypeLiteral;
+
+public class ReplicationStatusProjectRemoteResource implements RestResource {
+ public static final TypeLiteral<RestView<ReplicationStatusProjectRemoteResource>>
+ REPLICATION_STATUS_PROJECT_REMOTE_KIND =
+ new TypeLiteral<RestView<ReplicationStatusProjectRemoteResource>>() {};
+
+ private final Project.NameKey projectNameKey;
+ private final String remote;
+ private final String remoteURL;
+
+ public ReplicationStatusProjectRemoteResource(
+ Project.NameKey projectNameKey, String remote, String remoteURL) {
+ this.projectNameKey = projectNameKey;
+ this.remote = remote;
+ this.remoteURL = remoteURL;
+ }
+
+ public Project.NameKey getProjectNameKey() {
+ return projectNameKey;
+ }
+
+ public String getRemote() {
+ return remote;
+ }
+
+ public String getRemoteUrl() {
+ return remoteURL;
+ }
+}
diff --git a/src/main/resources/Documentation/build.md b/src/main/resources/Documentation/build.md
new file mode 100644
index 0000000..9d5193b
--- /dev/null
+++ b/src/main/resources/Documentation/build.md
@@ -0,0 +1,33 @@
+# Build
+
+This plugin is built with Bazel in-tree build.
+
+## Build in Gerrit tree
+
+Create a symbolic link of the repository source to the Gerrit source tree
+/plugins/replication-status directory.
+
+Example:
+
+```shell
+git clone https://gerrit.googlesource.com/gerrit
+git clone https://gerrit.googlesource.com/plugins/replication-status
+cd gerrit/plugins ln -s ../../replication-status replication-status
+```
+
+From the Gerrit source tree issue the command
+
+```shell
+bazelisk build plugins/replication-status
+```
+
+The jar file is created
+under `basel-bin/plugins/replication-status/replication-status.jar`
+
+To execute the tests run
+
+```shell
+bazelisk test plugins/replication-status/...
+```
+
+from the Gerrit source tree.
\ No newline at end of file
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
new file mode 100644
index 0000000..aa01313
--- /dev/null
+++ b/src/main/resources/Documentation/config.md
@@ -0,0 +1,45 @@
+# Config
+
+The plugin itself has no specific configuration, however some Gerrit specific
+settings are relevant.
+
+## Cache
+
+This plugin relies on a cache to store replication status information, the
+global cache configuration settings apply.
+
+Please look at
+the [gerrit cache documentation](https://gerrit-review.googlesource.com/Documentation/config-gerrit.html#cache)
+for more information on this.
+
+In particular:
+
+* To define the lifespan of replication-status entries in the cache, look at
+ the [maxAge](https://gerrit-review.googlesource.com/Documentation/config-gerrit.html#cache.name.maxAge)
+ documentation. *Default*: store forever with no expire
+
+* To define how much disk space the replication-status cache can take, look at
+ the [diskLimit](https://gerrit-review.googlesource.com/Documentation/config-gerrit.html#cache.name.diskLimit)
+ documentation. *Default*: disk storage for the cache is disabled
+
+To modify this cache behaviour add the following stanza to the `gerrit.config`,
+for example:
+
+```
+[cache "replication-status.replication_status"]
+ diskLimit = 52428800 # 50 Mb
+ maxAge = 1 day
+```
+
+## Gerrit instanceId
+
+This plugin will try to discriminate among events produced by the current
+instances versus events produced by different instances.
+
+If the gerrit node running this plugin has
+the [gerrit.instanceId]([gerrit.instanceId](https://gerrit-review.googlesource.com/Documentation/config-gerrit.html#gerrit.instanceId))
+set to a specific value, it will only consume events that match it.
+
+On the other hand, if the `gerrit.instanceId` is not defined, only events that _
+do not_ have instanceId set will be consumed, as events exhibiting different
+instanceId values must have been generated by different gerrit nodes.
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replicationstatus/ReplicationStatusIT.java b/src/test/java/com/googlesource/gerrit/plugins/replicationstatus/ReplicationStatusIT.java
new file mode 100644
index 0000000..68508f3
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/replicationstatus/ReplicationStatusIT.java
@@ -0,0 +1,292 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.replicationstatus;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.extensions.restapi.Url.encode;
+import static com.googlesource.gerrit.plugins.replication.ReplicationState.RefPushResult;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.TestPlugin;
+import com.google.gerrit.acceptance.config.GerritConfig;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.AccessSection;
+import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.Permission;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.api.groups.GroupInput;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.httpd.restapi.RestApiServlet;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gson.Gson;
+import com.google.inject.Inject;
+import com.googlesource.gerrit.plugins.replication.events.RefReplicatedEvent;
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.util.Collections;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.transport.RemoteRefUpdate;
+import org.eclipse.jgit.transport.URIish;
+import org.junit.Before;
+import org.junit.Test;
+
+@TestPlugin(
+ name = "replication-status",
+ sysModule = "com.googlesource.gerrit.plugins.replicationstatus.Module")
+public class ReplicationStatusIT extends LightweightPluginDaemonTest {
+ private static final String REF_MASTER = Constants.R_HEADS + Constants.MASTER;
+ private static final String REMOTE = "ssh://some.remote.host";
+
+ private static final Gson gson = newGson();
+
+ @Inject protected SitePaths sitePaths;
+ @Inject private ProjectOperations projectOperations;
+
+ private EventHandler eventHandler;
+
+ @Before
+ public void setUp() throws IOException {
+ eventHandler = plugin.getSysInjector().getInstance(EventHandler.class);
+ }
+
+ @Test
+ @GerritConfig(name = "gerrit.instanceId", value = "testInstanceId-1")
+ public void shouldBeOKSuccessForAdminUsers() throws Exception {
+ RestResponse result = adminRestSession.get(endpoint(project, REMOTE));
+ result.assertOK();
+
+ assertThat(contentWithoutMagicJson(result)).isEqualTo(emptyReplicationStatus(project, REMOTE));
+ }
+
+ @Test
+ @GerritConfig(name = "gerrit.instanceId", value = "testInstanceId-1")
+ public void shouldBeOKSuccessForProjectOwners() throws Exception {
+ makeProjectOwner(user, project);
+ RestResponse result = userRestSession.get(endpoint(project, REMOTE));
+ result.assertOK();
+
+ assertThat(contentWithoutMagicJson(result)).isEqualTo(emptyReplicationStatus(project, REMOTE));
+ }
+
+ @Test
+ @GerritConfig(name = "gerrit.instanceId", value = "testInstanceId-1")
+ public void shouldBeForbiddenForNonProjectOwners() throws Exception {
+ RestResponse result = userRestSession.get(endpoint(project, REMOTE));
+ result.assertForbidden();
+
+ assertThat(result.getEntityContent()).contains("Administrate Server or Project owner required");
+ }
+
+ @Test
+ @GerritConfig(name = "gerrit.instanceId", value = "testInstanceId-1")
+ public void shouldBeForbiddenForAnonymousUsers() throws Exception {
+ RestResponse result = anonymousRestSession.get(endpoint(project, REMOTE));
+ result.assertForbidden();
+
+ assertThat(result.getEntityContent()).contains("Administrate Server or Project owner required");
+ }
+
+ @Test
+ @GerritConfig(name = "gerrit.instanceId", value = "testInstanceId-1")
+ public void shouldNotReportStatusOfReplicationsGeneratedOnDifferentNodes() throws Exception {
+ eventHandler.onEvent(
+ successReplicatedEvent("testInstanceId-2", System.currentTimeMillis(), REMOTE));
+
+ RestResponse result = adminRestSession.get(endpoint(project, REMOTE));
+ result.assertOK();
+
+ assertThat(contentWithoutMagicJson(result)).isEqualTo(emptyReplicationStatus(project, REMOTE));
+ }
+
+ @Test
+ @GerritConfig(name = "gerrit.instanceId", value = "testInstanceId-1")
+ public void shouldReturnSuccessfulProjectReplicationStatus() throws Exception {
+ long eventCreatedOn = System.currentTimeMillis();
+
+ eventHandler.onEvent(successReplicatedEvent("testInstanceId-1", eventCreatedOn, REMOTE));
+ RestResponse result = adminRestSession.get(endpoint(project, REMOTE));
+
+ result.assertOK();
+ assertThat(contentWithoutMagicJson(result))
+ .isEqualTo(successReplicationStatus(REMOTE, project, eventCreatedOn));
+ }
+
+ @Test
+ public void shouldConsumeEventsThatHaveNoInstanceId() throws Exception {
+ long eventCreatedOn = System.currentTimeMillis();
+
+ eventHandler.onEvent(successReplicatedEvent(null, eventCreatedOn, REMOTE));
+ RestResponse result = adminRestSession.get(endpoint(project, REMOTE));
+
+ result.assertOK();
+ assertThat(contentWithoutMagicJson(result))
+ .isEqualTo(successReplicationStatus(REMOTE, project, eventCreatedOn));
+ }
+
+ @Test
+ public void shouldNotConsumeEventsWhenNodeInstanceIdIsNullButEventHasIt() throws Exception {
+ eventHandler.onEvent(
+ successReplicatedEvent("testInstanceId-2", System.currentTimeMillis(), REMOTE));
+
+ RestResponse result = adminRestSession.get(endpoint(project, REMOTE));
+ result.assertOK();
+
+ assertThat(contentWithoutMagicJson(result)).isEqualTo(emptyReplicationStatus(project, REMOTE));
+ }
+
+ @Test
+ public void shouldConsumeEventsWhenBothNodeAndEventHaveNoInstanceId() throws Exception {
+ long eventCreatedOn = System.currentTimeMillis();
+
+ eventHandler.onEvent(successReplicatedEvent(null, eventCreatedOn, REMOTE));
+ RestResponse result = adminRestSession.get(endpoint(project, REMOTE));
+
+ result.assertOK();
+ assertThat(contentWithoutMagicJson(result))
+ .isEqualTo(successReplicationStatus(REMOTE, project, eventCreatedOn));
+ }
+
+ @Test
+ @GerritConfig(name = "gerrit.instanceId", value = "testInstanceId-1")
+ public void shouldShowFailedInPayloadWhenRefCouldntBeReplicated() throws Exception {
+ long eventCreatedOn = System.currentTimeMillis();
+
+ eventHandler.onEvent(failedReplicatedEvent("testInstanceId-1", eventCreatedOn, REMOTE));
+ RestResponse result = adminRestSession.get(endpoint(project, REMOTE));
+
+ result.assertOK();
+ assertThat(contentWithoutMagicJson(result))
+ .isEqualTo(failedReplicationStatus(REMOTE, project, eventCreatedOn));
+ }
+
+ private String contentWithoutMagicJson(RestResponse response) throws IOException {
+ return response.getEntityContent().substring(RestApiServlet.JSON_MAGIC.length);
+ }
+
+ private AccountGroup.UUID createGroup(String name, TestAccount member) throws RestApiException {
+ GroupInput groupInput = new GroupInput();
+ groupInput.name = name(name);
+ groupInput.members = Collections.singletonList(String.valueOf(member.id().get()));
+ return AccountGroup.uuid(gApi.groups().create(groupInput).get().id);
+ }
+
+ private void makeProjectOwner(TestAccount user, Project.NameKey project) throws RestApiException {
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(
+ allow(Permission.OWNER)
+ .ref(AccessSection.ALL)
+ .group(createGroup("projectOwners", user)))
+ .update();
+ }
+
+ private RefReplicatedEvent replicatedEvent(
+ @Nullable String instanceId,
+ long when,
+ String ref,
+ String remote,
+ RefPushResult status,
+ RemoteRefUpdate.Status refStatus)
+ throws URISyntaxException {
+ RefReplicatedEvent replicatedEvent =
+ new RefReplicatedEvent(project.get(), ref, new URIish(remote), status, refStatus);
+ replicatedEvent.instanceId = instanceId;
+ replicatedEvent.eventCreatedOn = when;
+
+ return replicatedEvent;
+ }
+
+ private RefReplicatedEvent successReplicatedEvent(
+ @Nullable String instanceId, long when, String remoteUrl) throws URISyntaxException {
+
+ return replicatedEvent(
+ instanceId,
+ when,
+ REF_MASTER,
+ remoteUrl,
+ RefPushResult.SUCCEEDED,
+ RemoteRefUpdate.Status.OK);
+ }
+
+ private RefReplicatedEvent failedReplicatedEvent(
+ @Nullable String instanceId, long when, String remoteUrl) throws URISyntaxException {
+
+ return replicatedEvent(
+ instanceId,
+ when,
+ REF_MASTER,
+ remoteUrl,
+ RefPushResult.FAILED,
+ RemoteRefUpdate.Status.REJECTED_NONFASTFORWARD);
+ }
+
+ private static String endpoint(Project.NameKey project, String remote) {
+ return String.format(
+ "/projects/%s/remotes/%s/replication-status", project.get(), encode(remote));
+ }
+
+ private String emptyReplicationStatus(Project.NameKey project, String remoteUrl)
+ throws URISyntaxException {
+ return gson.toJson(
+ ProjectReplicationStatus.create(
+ ImmutableMap.of(remoteUrl, RemoteReplicationStatus.create(Collections.emptyMap())),
+ ProjectReplicationStatus.ProjectReplicationStatusResult.OK,
+ project.get()));
+ }
+
+ private String successReplicationStatus(String remote, Project.NameKey project, long when)
+ throws URISyntaxException {
+ return projectReplicationStatus(
+ remote,
+ project,
+ when,
+ ProjectReplicationStatus.ProjectReplicationStatusResult.OK,
+ ReplicationStatus.ReplicationStatusResult.SUCCEEDED);
+ }
+
+ private String failedReplicationStatus(String remote, Project.NameKey project, long when)
+ throws URISyntaxException {
+ return projectReplicationStatus(
+ remote,
+ project,
+ when,
+ ProjectReplicationStatus.ProjectReplicationStatusResult.FAILED,
+ ReplicationStatus.ReplicationStatusResult.FAILED);
+ }
+
+ private String projectReplicationStatus(
+ String remoteUrl,
+ Project.NameKey project,
+ long when,
+ ProjectReplicationStatus.ProjectReplicationStatusResult projectReplicationStatusResult,
+ ReplicationStatus.ReplicationStatusResult replicationStatusResult)
+ throws URISyntaxException {
+ return gson.toJson(
+ ProjectReplicationStatus.create(
+ ImmutableMap.of(
+ remoteUrl,
+ RemoteReplicationStatus.create(
+ ImmutableMap.of(
+ REF_MASTER, ReplicationStatus.create(replicationStatusResult, when)))),
+ projectReplicationStatusResult,
+ project.get()));
+ }
+}