Clone: configure pushUrl if sshdAdvertisedPrimaryAddress is set
The new option `plugin.download-commands.sshdAdvertisedPrimaryAddress`
specifies the address clients should be configured to reach a primary
Gerrit instance.
This may differ from `sshd.listenAddress` if fetch is served from
another address. An example is a setup where upload-pack requests
are served by a Gerrit replica and receive-pack by a Gerrit primary.
Since ssh cannot be load balanced on layer 7 the addresses of the
primary and replica need to be different.
Configure `remote.origin.pushUrl` in the clone command installing the
commit-msg hook which is typically used if the client is used to push
new changes.
Change-Id: I69a271c55fe53a4d1c01b5f24f1a6fd8313e7ba8
diff --git a/src/main/java/com/googlesource/gerrit/plugins/download/command/CloneWithCommitMsgHook.java b/src/main/java/com/googlesource/gerrit/plugins/download/command/CloneWithCommitMsgHook.java
index 431c8b9..f7bc005 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/download/command/CloneWithCommitMsgHook.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/download/command/CloneWithCommitMsgHook.java
@@ -91,6 +91,22 @@
.append(extraCommand)
.append(")");
}
+
+ if (scheme instanceof SshScheme) {
+ String sshPushAddress = ((SshScheme) scheme).getPushUrl(project);
+ if (sshPushAddress != null) {
+ command
+ .append(" && ")
+ .append("(cd ")
+ .append(QuoteUtil.quote(projectName))
+ .append(" && ")
+ .append(
+ "git remote set-url --push \"$(git config --default origin --get clone.defaultRemoteName)\"")
+ .append(" ")
+ .append(QuoteUtil.quote(sshPushAddress))
+ .append(")");
+ }
+ }
return command != null ? command.toString() : null;
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/download/scheme/SshScheme.java b/src/main/java/com/googlesource/gerrit/plugins/download/scheme/SshScheme.java
index 4628f73..6e23ef9 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/download/scheme/SshScheme.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/download/scheme/SshScheme.java
@@ -18,10 +18,13 @@
import com.google.common.annotations.VisibleForTesting;
import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.annotations.PluginName;
import com.google.gerrit.extensions.config.DownloadScheme;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.config.CanonicalWebUrl;
import com.google.gerrit.server.config.DownloadConfig;
+import com.google.gerrit.server.config.PluginConfig;
+import com.google.gerrit.server.config.PluginConfigFactory;
import com.google.gerrit.server.ssh.SshAdvertisedAddresses;
import com.google.inject.Inject;
import com.google.inject.Provider;
@@ -35,6 +38,7 @@
public class SshScheme extends DownloadScheme {
private final String sshdAddress;
+ private final String sshdPrimaryAddress;
private final String sshdHost;
private final int sshdPort;
private final Provider<CurrentUser> userProvider;
@@ -45,6 +49,8 @@
@VisibleForTesting
public SshScheme(
@SshAdvertisedAddresses List<String> sshAddresses,
+ @PluginName String pluginName,
+ PluginConfigFactory configFactory,
@CanonicalWebUrl @Nullable Provider<String> urlProvider,
Provider<CurrentUser> userProvider,
DownloadConfig downloadConfig) {
@@ -81,6 +87,17 @@
this.sshdHost = host;
this.sshdPort = port;
+ PluginConfig config = configFactory.getFromGerritConfig(pluginName);
+ String sshdPrimaryAddress = config.getString("sshdAdvertisedPrimaryAddress");
+ if (sshdPrimaryAddress != null && sshdPrimaryAddress.startsWith("*:") && urlProvider != null) {
+ try {
+ sshdPrimaryAddress = new URL(urlProvider.get()).getHost() + sshdPrimaryAddress.substring(1);
+ } catch (MalformedURLException e) {
+ // ignore, then this scheme will be disabled
+ }
+ }
+ this.sshdPrimaryAddress = sshdPrimaryAddress;
+
this.userProvider = userProvider;
this.schemeAllowed = downloadConfig.getDownloadSchemes().contains(SSH);
this.schemeHidden = downloadConfig.getHiddenSchemes().contains(SSH);
@@ -89,7 +106,17 @@
@Nullable
@Override
public String getUrl(String project) {
- if (!isEnabled() || !userProvider.get().isIdentifiedUser()) {
+ return buildSshUrl(sshdAddress, project);
+ }
+
+ @Nullable
+ public String getPushUrl(String project) {
+ return buildSshUrl(sshdPrimaryAddress, project);
+ }
+
+ @Nullable
+ private String buildSshUrl(String address, String project) {
+ if (!isEnabled() || address == null || !userProvider.get().isIdentifiedUser()) {
return null;
}
@@ -106,7 +133,7 @@
throw new IllegalStateException("No UTF-8 support", e);
}
r.append("@");
- r.append(ensureSlash(sshdAddress));
+ r.append(ensureSlash(address));
r.append(project);
return r.toString();
}
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index 526f298..3386747 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -138,3 +138,29 @@
Optional command to complete the commit-msg hook. For example:
`git submodule update --init --recursive && git review -s`
would initialize the submodules and setup git review.
+
+### <a id="plugin.@PLUGIN@">Section plugin.@PLUGIN@</a>
+
+```
+[plugin "@PLUGIN@"]
+ sshdAdvertisedPrimaryAddress = host:port
+```
+
+<a id="plugin.@PLUGIN@.sshdAdvertisedPrimaryAddress">plugin.@PLUGIN@.sshdAdvertisedPrimaryAddress</a>
+Specifies the address where clients can reach a Gerrit primary
+instance via ssh protocol.
+
+This may differ from sshd.listenAddress if fetch is served from
+another address. An example is a setup where upload-pack requests
+are served by a Gerrit replica and receive-pack by a Gerrit primary.
+Since ssh cannot be load balanced on layer 7 the addresses of the
+primary and replica need to be different.
+
+The following forms may be used to specify an address. In any
+form, `:'port'` may be omitted to use the default SSH port of 22.
+
+* `'hostname':'port'` (for example `review.example.com:22`)
+* `'IPv4':'port'` (for example `10.0.0.1:29418`)
+* `['IPv6']:'port'` (for example `[ff02::1]:29418`)
+
+By default unset.
diff --git a/src/test/java/com/googlesource/gerrit/plugins/download/DownloadCommandTest.java b/src/test/java/com/googlesource/gerrit/plugins/download/DownloadCommandTest.java
index bf7a49d..c39bf25 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/download/DownloadCommandTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/download/DownloadCommandTest.java
@@ -18,6 +18,8 @@
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.account.GroupMembership;
import com.google.gerrit.server.config.DownloadConfig;
+import com.google.gerrit.server.config.PluginConfig;
+import com.google.gerrit.server.config.PluginConfigFactory;
import com.google.inject.Provider;
import com.google.inject.util.Providers;
import com.googlesource.gerrit.plugins.download.scheme.HttpScheme;
@@ -29,6 +31,7 @@
import org.eclipse.jgit.lib.Config;
import org.junit.Before;
import org.junit.Ignore;
+import org.mockito.Mockito;
@Ignore
public class DownloadCommandTest {
@@ -67,6 +70,7 @@
public final String projectName = "my/project";
public final String userName = "john-doe@company.com";
public final int sshPort = 29418;
+ public final int sshdAdvertisedPrimaryAddress = 39418;
public String urlEncodedUserName() throws UnsupportedEncodingException {
return URLEncoder.encode(userName, StandardCharsets.UTF_8.name());
@@ -90,14 +94,25 @@
@Before
public void setUp() {
- Config cfg = new Config();
+ final String pluginName = "download-commands";
+
+ PluginConfigFactory configFactory = Mockito.mock(PluginConfigFactory.class);
+ Mockito.when(configFactory.getFromGerritConfig(pluginName))
+ .thenReturn(PluginConfig.createFromGerritConfig(pluginName, new Config()));
+
urlProvider = Providers.of(ENV.canonicalUrl());
+
+ Config cfg = new Config();
DownloadConfig downloadConfig = new DownloadConfig(cfg);
+
userProvider = Providers.of(fakeUser());
+
httpScheme = new HttpScheme(cfg, urlProvider, userProvider, downloadConfig);
sshScheme =
new SshScheme(
ImmutableList.of(String.format("%s:%d", ENV.fqdn, ENV.sshPort)),
+ pluginName,
+ configFactory,
urlProvider,
userProvider,
downloadConfig);
diff --git a/src/test/java/com/googlesource/gerrit/plugins/download/command/CloneWithCommitMsgHookTest.java b/src/test/java/com/googlesource/gerrit/plugins/download/command/CloneWithCommitMsgHookTest.java
index 24b521e..f9d62ba 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/download/command/CloneWithCommitMsgHookTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/download/command/CloneWithCommitMsgHookTest.java
@@ -18,15 +18,21 @@
import static com.googlesource.gerrit.plugins.download.command.CloneWithCommitMsgHook.HOOKS_DIR;
import static com.googlesource.gerrit.plugins.download.command.CloneWithCommitMsgHook.HOOK_COMMAND_KEY;
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.server.config.DownloadConfig;
+import com.google.gerrit.server.config.PluginConfig;
+import com.google.gerrit.server.config.PluginConfigFactory;
import com.googlesource.gerrit.plugins.download.DownloadCommandTest;
+import com.googlesource.gerrit.plugins.download.scheme.SshScheme;
import org.eclipse.jgit.lib.Config;
import org.junit.Test;
+import org.mockito.Mockito;
public class CloneWithCommitMsgHookTest extends DownloadCommandTest {
@Test
public void testSshNoConfiguredCommands() throws Exception {
- String command = getCloneCommand(null, null).getCommand(sshScheme, ENV.projectName);
+ String command = getCloneCommand(null, null, null).getCommand(sshScheme, ENV.projectName);
assertThat(command)
.isEqualTo(
String.format(
@@ -37,7 +43,8 @@
@Test
public void testSshConfiguredHookCommand() throws Exception {
String hookCommand = "my hook command";
- String command = getCloneCommand(hookCommand, null).getCommand(sshScheme, ENV.projectName);
+ String command =
+ getCloneCommand(hookCommand, null, null).getCommand(sshScheme, ENV.projectName);
assertThat(command)
.isEqualTo(
String.format(
@@ -46,9 +53,28 @@
}
@Test
+ public void testSshConfiguredHookCommandAndPrimaryAddress() throws Exception {
+ String hookCommand = "my hook command";
+ String primaryAddress = getSshdAdvertisedPrimaryAddress();
+ String command =
+ getCloneCommand(hookCommand, null, primaryAddress).getCommand(sshScheme, ENV.projectName);
+ assertThat(command)
+ .isEqualTo(
+ String.format(
+ "git clone \"%s\" && (cd %s && %s) && (cd %s && git remote set-url --push "
+ + "\"$(git config --default origin --get clone.defaultRemoteName)\" \"%s\")",
+ sshScheme.getUrl(ENV.projectName),
+ baseName(ENV.projectName),
+ hookCommand,
+ baseName(ENV.projectName),
+ sshScheme.getPushUrl(ENV.projectName)));
+ }
+
+ @Test
public void testSshConfiguredExtraCommand() throws Exception {
String extraCommand = "my extra command";
- String command = getCloneCommand(extraCommand, null).getCommand(sshScheme, ENV.projectName);
+ String command =
+ getCloneCommand(extraCommand, null, null).getCommand(sshScheme, ENV.projectName);
assertThat(command)
.isEqualTo(
String.format(
@@ -61,7 +87,7 @@
String hookCommand = "my hook command";
String extraCommand = "my extra command";
String command =
- getCloneCommand(hookCommand, extraCommand).getCommand(sshScheme, ENV.projectName);
+ getCloneCommand(hookCommand, extraCommand, null).getCommand(sshScheme, ENV.projectName);
assertThat(command)
.isEqualTo(
String.format(
@@ -74,8 +100,30 @@
}
@Test
+ public void testSshConfiguredHookExtraCommandAndAdvertisedPrimary() throws Exception {
+ String hookCommand = "my hook command";
+ String extraCommand = "my extra command";
+ String primaryAddress = getSshdAdvertisedPrimaryAddress();
+ String command =
+ getCloneCommand(hookCommand, extraCommand, primaryAddress)
+ .getCommand(sshScheme, ENV.projectName);
+ assertThat(command)
+ .isEqualTo(
+ String.format(
+ "git clone \"%s\" && (cd %s && %s) && (cd %s && %s) && (cd %s && git remote set-url --push "
+ + "\"$(git config --default origin --get clone.defaultRemoteName)\" \"%s\")",
+ sshScheme.getUrl(ENV.projectName),
+ baseName(ENV.projectName),
+ hookCommand,
+ baseName(ENV.projectName),
+ extraCommand,
+ baseName(ENV.projectName),
+ sshScheme.getPushUrl(ENV.projectName)));
+ }
+
+ @Test
public void testHttpNoConfiguredCommands() throws Exception {
- String command = getCloneCommand(null, null).getCommand(httpScheme, ENV.projectName);
+ String command = getCloneCommand(null, null, null).getCommand(httpScheme, ENV.projectName);
assertThat(command)
.isEqualTo(
String.format(
@@ -86,7 +134,8 @@
@Test
public void testHttpConfiguredExtraCommand() throws Exception {
String extraCommand = "my extra command";
- String command = getCloneCommand(extraCommand, null).getCommand(httpScheme, ENV.projectName);
+ String command =
+ getCloneCommand(extraCommand, null, null).getCommand(httpScheme, ENV.projectName);
assertThat(command)
.isEqualTo(
String.format(
@@ -99,7 +148,7 @@
String hookCommand = "my hook command";
String extraCommand = "my extra command";
String command =
- getCloneCommand(hookCommand, extraCommand).getCommand(httpScheme, ENV.projectName);
+ getCloneCommand(hookCommand, extraCommand, null).getCommand(httpScheme, ENV.projectName);
assertThat(command)
.isEqualTo(
String.format(
@@ -121,10 +170,31 @@
baseName(ENV.projectName), HOOKS_DIR, HOOKS_DIR, ENV.fqdn, HOOKS_DIR);
}
- private CloneCommand getCloneCommand(String hookCommand, String extraCommand) {
+ private String getSshdAdvertisedPrimaryAddress() {
+ return String.format("%s:%d", ENV.fqdn, ENV.sshdAdvertisedPrimaryAddress);
+ }
+
+ private CloneCommand getCloneCommand(
+ String hookCommand, String extraCommand, String sshdAdvertisedPrimaryAddress) {
+ final String pluginName = "download-commands";
Config cfg = new Config();
cfg.setString("gerrit", null, HOOK_COMMAND_KEY, hookCommand);
cfg.setString("gerrit", null, EXTRA_COMMAND_KEY, extraCommand);
+ cfg.setString(
+ "plugin", pluginName, "sshdadvertisedprimaryaddress", sshdAdvertisedPrimaryAddress);
+
+ PluginConfigFactory configFactory = Mockito.mock(PluginConfigFactory.class);
+ Mockito.when(configFactory.getFromGerritConfig(pluginName))
+ .thenReturn(PluginConfig.createFromGerritConfig(pluginName, cfg));
+
+ sshScheme =
+ new SshScheme(
+ ImmutableList.of(String.format("%s:%d", ENV.fqdn, ENV.sshPort)),
+ pluginName,
+ configFactory,
+ urlProvider,
+ userProvider,
+ new DownloadConfig(cfg));
return new CloneWithCommitMsgHook(cfg, urlProvider);
}
}