Use -dot- subdomain notation in MapReduce console links

appspot.com is not provisioned with a multi-level wildcard SSL certificate, so
URLs of the form https://service.projectid.appspot.com/path need to be rewritten
as https://service-dot-projectid.appspot.com/path (and same for version names).

This is a follow-up to []

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=231418795
This commit is contained in:
mcilwain 2019-01-29 09:43:30 -08:00 committed by Michael Muller
parent 6e0b8f2cfd
commit 3812c2ceda
7 changed files with 85 additions and 6 deletions

View file

@ -284,7 +284,10 @@ public class MapreduceRunner {
private String renderMapreduceConsoleLink(String jobId) {
return String.format(
MAPREDUCE_CONSOLE_LINK_FORMAT, appEngineServiceUtils.getServiceHostname("backend"), jobId);
MAPREDUCE_CONSOLE_LINK_FORMAT,
appEngineServiceUtils.convertToSingleSubdomain(
appEngineServiceUtils.getServiceHostname("backend")),
jobId);
}
/**
@ -301,7 +304,7 @@ public class MapreduceRunner {
}
public void sendLinkToMapreduceConsole(Response response) {
response.setPayload(getLinkToMapreduceConsole());
response.setPayload(getLinkToMapreduceConsole() + "\n");
}
public String getLinkToMapreduceConsole() {

View file

@ -38,6 +38,25 @@ public interface AppEngineServiceUtils {
/** Returns a host name to use for the given service and version. */
String getVersionHostname(String service, String version);
/**
* Converts a multi-level App Engine host name (not URL) to the -dot- single subdomain format.
*
* <p>This is needed because appspot.com only has a single wildcard SSL certificate, so the native
* App Engine URLs of the form service.projectid.appspot.com or
* version.service.projectid.appspot.com won't work over HTTPS when being fetched from outside of
* GCP. The work-around is to change all of the "." subdomain markers to "-dot-". E.g.:
*
* <ul>
* <li>tools.projectid.appspot.com --> tools-dot-projectid.appspot.com
* <li>version.backend.projectid.appspot.com --> version-dot-backend-dot-projectid.appspot.com
* </ul>
*
* @see <a
* href="https://cloud.google.com/appengine/docs/standard/java/how-requests-are-routed">How
* App Engine requests are routed</a>
*/
String convertToSingleSubdomain(String hostname);
/** Set the number of instances at runtime for a given service and version. */
void setNumInstances(String service, String version, long numInstances);
}

View file

@ -18,11 +18,18 @@ import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import com.google.appengine.api.modules.ModulesService;
import com.google.common.flogger.FluentLogger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.inject.Inject;
/** A wrapper for {@link ModulesService} that provides a saner API. */
public class AppEngineServiceUtilsImpl implements AppEngineServiceUtils {
private static final Pattern APPSPOT_HOSTNAME_PATTERN =
Pattern.compile("^(.*)\\.appspot\\.com$");
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private final ModulesService modulesService;
@Inject
@ -56,4 +63,14 @@ public class AppEngineServiceUtilsImpl implements AppEngineServiceUtils {
checkArgument(numInstances > 0, "Number of instances must be greater than 0");
modulesService.setNumInstances(service, version, numInstances);
}
@Override
public String convertToSingleSubdomain(String hostname) {
Matcher matcher = APPSPOT_HOSTNAME_PATTERN.matcher(hostname);
if (!matcher.matches()) {
logger.atWarning().log("Skipping conversion because hostname can't be parsed: %s", hostname);
return hostname;
}
return matcher.group(1).replace(".", "-dot-") + ".appspot.com";
}
}

View file

@ -94,7 +94,8 @@ public class ExportDomainListsActionTest extends MapreduceTestCase<ExportDomainL
runMapreduce();
assertThat(response.getPayload())
.startsWith(
"Mapreduce console: https://backend.hostname.tld/_ah/pipeline/status.html?root=");
"Mapreduce console: https://backend-dot-projectid.appspot.com"
+ "/_ah/pipeline/status.html?root=");
}
@Test

View file

@ -20,6 +20,7 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import com.google.appengine.api.blobstore.dev.LocalBlobstoreService;
import com.google.appengine.api.modules.ModulesService;
import com.google.appengine.api.taskqueue.dev.LocalTaskQueue;
import com.google.appengine.api.taskqueue.dev.QueueStateInfo;
import com.google.appengine.api.taskqueue.dev.QueueStateInfo.HeaderWrapper;
@ -38,6 +39,7 @@ import google.registry.testing.FakeClock;
import google.registry.testing.MockitoJUnitRule;
import google.registry.testing.ShardableTestCase;
import google.registry.util.AppEngineServiceUtils;
import google.registry.util.AppEngineServiceUtilsImpl;
import java.io.ByteArrayInputStream;
import java.io.ObjectInputStream;
import java.io.UnsupportedEncodingException;
@ -84,16 +86,19 @@ public abstract class MapreduceTestCase<T> extends ShardableTestCase {
@Rule public final MockitoJUnitRule mocks = MockitoJUnitRule.create();
@Mock
AppEngineServiceUtils appEngineServiceUtils;
@Mock ModulesService modulesService;
@Before
public void setUp() {
taskQueue = LocalTaskQueueTestConfig.getLocalTaskQueue();
ApiProxyLocal proxy = (ApiProxyLocal) ApiProxy.getDelegate();
// Creating files is not allowed in some test execution environments, so don't.
proxy.setProperty(LocalBlobstoreService.NO_STORAGE_PROPERTY, "true");
when(appEngineServiceUtils.getServiceHostname("backend")).thenReturn("backend.hostname.tld");
appEngineServiceUtils = new AppEngineServiceUtilsImpl(modulesService);
when(modulesService.getVersionHostname("backend", null))
.thenReturn("version.backend.projectid.appspot.com");
}
protected MapreduceRunner makeDefaultRunner() {

View file

@ -132,7 +132,8 @@ public class GenerateZoneFilesActionTest extends MapreduceTestCase<GenerateZoneF
assertThat(response).containsKey("mapreduceConsoleLink");
assertThat(response.get("mapreduceConsoleLink").toString())
.startsWith(
"Mapreduce console: https://backend.hostname.tld/_ah/pipeline/status.html?root=");
"Mapreduce console: https://backend-dot-projectid.appspot.com"
+ "/_ah/pipeline/status.html?root=");
executeTasksUntilEmpty("mapreduce");

View file

@ -110,4 +110,37 @@ public class AppEngineServiceUtilsImplTest {
() -> appEngineServiceUtils.setNumInstances("service", "version", -10L));
assertThat(thrown).hasMessageThat().isEqualTo("Number of instances must be greater than 0");
}
@Test
public void test_convertToSingleSubdomain_doesNothingWithoutServiceOrHostname() {
assertThat(appEngineServiceUtils.convertToSingleSubdomain("projectid.appspot.com"))
.isEqualTo("projectid.appspot.com");
}
@Test
public void test_convertToSingleSubdomain_doesNothingWhenItCannotParseCorrectly() {
assertThat(appEngineServiceUtils.convertToSingleSubdomain("garbage.notrealhost.example"))
.isEqualTo("garbage.notrealhost.example");
}
@Test
public void test_convertToSingleSubdomain_convertsWithServiceName() {
assertThat(appEngineServiceUtils.convertToSingleSubdomain("service.projectid.appspot.com"))
.isEqualTo("service-dot-projectid.appspot.com");
}
@Test
public void test_convertToSingleSubdomain_convertsWithVersionAndServiceName() {
assertThat(
appEngineServiceUtils.convertToSingleSubdomain("version.service.projectid.appspot.com"))
.isEqualTo("version-dot-service-dot-projectid.appspot.com");
}
@Test
public void test_convertToSingleSubdomain_convertsWithInstanceAndVersionAndServiceName() {
assertThat(
appEngineServiceUtils.convertToSingleSubdomain(
"instanceid.version.service.projectid.appspot.com"))
.isEqualTo("instanceid-dot-version-dot-service-dot-projectid.appspot.com");
}
}