From 68e738d88c20ee64f6b480f27824564950bafbb8 Mon Sep 17 00:00:00 2001 From: Weimin Yu Date: Thu, 22 Jun 2023 11:20:41 -0400 Subject: [PATCH] Add CurlCommand option to connect to canary (#2060) Add a --canary option (default to false) to the CurlCommand that allows connection to the canary endpoints. During canary analysis, only the DEFAULT-canary receives traffic. This new flag allows use to test other canary services manually using the curl command. --- .../google/registry/tools/CurlCommand.java | 7 +++- .../registry/tools/ServiceConnection.java | 31 ++++++++++++--- .../registry/tools/CurlCommandTest.java | 31 +++++++++++---- .../registry/tools/ServiceConnectionTest.java | 38 +++++++++++++++++++ 4 files changed, 93 insertions(+), 14 deletions(-) create mode 100644 core/src/test/java/google/registry/tools/ServiceConnectionTest.java diff --git a/core/src/main/java/google/registry/tools/CurlCommand.java b/core/src/main/java/google/registry/tools/CurlCommand.java index 467b8827e..7a66ccb19 100644 --- a/core/src/main/java/google/registry/tools/CurlCommand.java +++ b/core/src/main/java/google/registry/tools/CurlCommand.java @@ -75,6 +75,11 @@ class CurlCommand implements CommandWithConnection { required = true) private Service service; + @Parameter( + names = {"--canary"}, + description = "If set, use the canary end-point; otherwise use the regular end-point.") + private Boolean canary = Boolean.FALSE; + @Override public void setConnection(ServiceConnection connection) { this.connection = connection; @@ -90,7 +95,7 @@ class CurlCommand implements CommandWithConnection { throw new IllegalArgumentException("You may not specify a body for a get method."); } - ServiceConnection connectionToService = connection.withService(service); + ServiceConnection connectionToService = connection.withService(service, canary); String response = (method == Method.GET) ? connectionToService.sendGetRequest(path, ImmutableMap.of()) diff --git a/core/src/main/java/google/registry/tools/ServiceConnection.java b/core/src/main/java/google/registry/tools/ServiceConnection.java index e35f3f12a..d7c5104a6 100644 --- a/core/src/main/java/google/registry/tools/ServiceConnection.java +++ b/core/src/main/java/google/registry/tools/ServiceConnection.java @@ -15,6 +15,8 @@ package google.registry.tools; import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Strings.isNullOrEmpty; +import static com.google.common.base.Verify.verify; import static com.google.common.net.HttpHeaders.X_REQUESTED_WITH; import static com.google.common.net.MediaType.JSON_UTF_8; import static google.registry.security.JsonHttp.JSON_SAFETY_PREFIX; @@ -26,6 +28,7 @@ import com.google.api.client.http.HttpHeaders; import com.google.api.client.http.HttpRequest; import com.google.api.client.http.HttpRequestFactory; import com.google.api.client.http.HttpResponse; +import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.io.CharStreams; @@ -36,6 +39,7 @@ import google.registry.config.RegistryConfig; import google.registry.request.Action.Service; import java.io.IOException; import java.io.InputStreamReader; +import java.net.MalformedURLException; import java.net.URL; import java.util.Map; import javax.annotation.Nullable; @@ -55,20 +59,23 @@ public class ServiceConnection { @Inject HttpRequestFactory requestFactory; private final Service service; + private final boolean useCanary; @Inject ServiceConnection() { service = Service.TOOLS; + useCanary = false; } - private ServiceConnection(Service service, HttpRequestFactory requestFactory) { + private ServiceConnection(Service service, HttpRequestFactory requestFactory, boolean useCanary) { this.service = service; this.requestFactory = requestFactory; + this.useCanary = useCanary; } - /** Returns a copy of this connection that talks to a different service. */ - public ServiceConnection withService(Service service) { - return new ServiceConnection(service, requestFactory); + /** Returns a copy of this connection that talks to a different service endpoint. */ + public ServiceConnection withService(Service service, boolean isCanary) { + return new ServiceConnection(service, requestFactory, isCanary); } /** Returns the contents of the title tag in the given HTML, or null if not found. */ @@ -85,7 +92,7 @@ public class ServiceConnection { private String internalSend( String endpoint, Map params, MediaType contentType, @Nullable byte[] payload) throws IOException { - GenericUrl url = new GenericUrl(String.format("%s%s", getServer(service), endpoint)); + GenericUrl url = new GenericUrl(String.format("%s%s", getServer(), endpoint)); url.putAll(params); HttpRequest request = (payload != null) @@ -120,6 +127,20 @@ public class ServiceConnection { } } + @VisibleForTesting + URL getServer() { + URL url = getServer(service); + if (useCanary) { + verify(!isNullOrEmpty(url.getHost()), "Null host in url"); + try { + return new URL(url.getProtocol(), "nomulus-dot-" + url.getHost(), url.getFile()); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + } + return url; + } + public String sendPostRequest( String endpoint, Map params, MediaType contentType, byte[] payload) throws IOException { diff --git a/core/src/test/java/google/registry/tools/CurlCommandTest.java b/core/src/test/java/google/registry/tools/CurlCommandTest.java index df3265751..d32e34665 100644 --- a/core/src/test/java/google/registry/tools/CurlCommandTest.java +++ b/core/src/test/java/google/registry/tools/CurlCommandTest.java @@ -22,6 +22,7 @@ import static google.registry.request.Action.Service.TOOLS; import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; @@ -29,6 +30,7 @@ import static org.mockito.Mockito.when; import com.google.common.collect.ImmutableMap; import com.google.common.net.MediaType; +import google.registry.request.Action.Service; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; @@ -46,7 +48,7 @@ class CurlCommandTest extends CommandTestCase { @BeforeEach void beforeEach() { command.setConnection(connection); - when(connection.withService(any())).thenReturn(connectionForService); + when(connection.withService(any(Service.class), anyBoolean())).thenReturn(connectionForService); } @Captor ArgumentCaptor> urlParamCaptor; @@ -54,7 +56,7 @@ class CurlCommandTest extends CommandTestCase { @Test void testGetInvocation() throws Exception { runCommand("--path=/foo/bar?a=1&b=2", "--service=TOOLS"); - verify(connection).withService(TOOLS); + verify(connection).withService(eq(TOOLS), eq(false)); verifyNoMoreInteractions(connection); verify(connectionForService) .sendGetRequest(eq("/foo/bar?a=1&b=2"), eq(ImmutableMap.of())); @@ -63,7 +65,7 @@ class CurlCommandTest extends CommandTestCase { @Test void testExplicitGetInvocation() throws Exception { runCommand("--path=/foo/bar?a=1&b=2", "--request=GET", "--service=BACKEND"); - verify(connection).withService(BACKEND); + verify(connection).withService(eq(BACKEND), eq(false)); verifyNoMoreInteractions(connection); verify(connectionForService) .sendGetRequest(eq("/foo/bar?a=1&b=2"), eq(ImmutableMap.of())); @@ -72,7 +74,7 @@ class CurlCommandTest extends CommandTestCase { @Test void testPostInvocation() throws Exception { runCommand("--path=/foo/bar?a=1&b=2", "--data=some data", "--service=DEFAULT"); - verify(connection).withService(DEFAULT); + verify(connection).withService(eq(DEFAULT), eq(false)); verifyNoMoreInteractions(connection); verify(connectionForService) .sendPostRequest( @@ -89,7 +91,7 @@ class CurlCommandTest extends CommandTestCase { "--data=some data", "--service=DEFAULT", "--content-type=application/json"); - verify(connection).withService(DEFAULT); + verify(connection).withService(eq(DEFAULT), eq(false)); verifyNoMoreInteractions(connection); verify(connectionForService) .sendPostRequest( @@ -118,7 +120,7 @@ class CurlCommandTest extends CommandTestCase { void testMultiDataPost() throws Exception { runCommand( "--path=/foo/bar?a=1&b=2", "--data=first=100", "-d", "second=200", "--service=PUBAPI"); - verify(connection).withService(PUBAPI); + verify(connection).withService(eq(PUBAPI), eq(false)); verifyNoMoreInteractions(connection); verify(connectionForService) .sendPostRequest( @@ -132,7 +134,7 @@ class CurlCommandTest extends CommandTestCase { void testDataDoesntSplit() throws Exception { runCommand( "--path=/foo/bar?a=1&b=2", "--data=one,two", "--service=PUBAPI"); - verify(connection).withService(PUBAPI); + verify(connection).withService(eq(PUBAPI), eq(false)); verifyNoMoreInteractions(connection); verify(connectionForService) .sendPostRequest( @@ -145,7 +147,20 @@ class CurlCommandTest extends CommandTestCase { @Test void testExplicitPostInvocation() throws Exception { runCommand("--path=/foo/bar?a=1&b=2", "--request=POST", "--service=TOOLS"); - verify(connection).withService(TOOLS); + verify(connection).withService(eq(TOOLS), eq(false)); + verifyNoMoreInteractions(connection); + verify(connectionForService) + .sendPostRequest( + eq("/foo/bar?a=1&b=2"), + eq(ImmutableMap.of()), + eq(MediaType.PLAIN_TEXT_UTF_8), + eq("".getBytes(UTF_8))); + } + + @Test + void testCanaryInvocation() throws Exception { + runCommand("--path=/foo/bar?a=1&b=2", "--request=POST", "--service=TOOLS", "--canary"); + verify(connection).withService(eq(TOOLS), eq(true)); verifyNoMoreInteractions(connection); verify(connectionForService) .sendPostRequest( diff --git a/core/src/test/java/google/registry/tools/ServiceConnectionTest.java b/core/src/test/java/google/registry/tools/ServiceConnectionTest.java new file mode 100644 index 000000000..e465de813 --- /dev/null +++ b/core/src/test/java/google/registry/tools/ServiceConnectionTest.java @@ -0,0 +1,38 @@ +// Copyright 2023 The Nomulus Authors. All Rights Reserved. +// +// 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 google.registry.tools; + +import static com.google.common.truth.Truth.assertThat; +import static google.registry.request.Action.Service.DEFAULT; + +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link google.registry.tools.ServiceConnection}. */ +public class ServiceConnectionTest { + + @Test + void testServerUrl_notCanary() { + ServiceConnection connection = new ServiceConnection().withService(DEFAULT, false); + String serverUrl = connection.getServer().toString(); + assertThat(serverUrl).isEqualTo("https://localhost"); // See default-config.yaml + } + + @Test + void testServerUrl_canary() { + ServiceConnection connection = new ServiceConnection().withService(DEFAULT, true); + String serverUrl = connection.getServer().toString(); + assertThat(serverUrl).isEqualTo("https://nomulus-dot-localhost"); + } +}