1
0
mirror of https://github.com/google/nomulus synced 2026-07-08 17:16:54 +00:00

Compare commits

...

11 Commits

Author SHA1 Message Date
Lai Jiang 92ebd0dedb Build different console versions for different environments (#2715)
TESTED=deployed to alpha
2025-03-11 23:39:28 +00:00
Lai Jiang b49e37feee Add a GCB job to delete GAE canary versions (#2714)
We've seen this issue happen more often than not recently, where GAE
canary deployment is stuck for about 10 min and the failed. The reason
is not clear, but delete the canary version prior to a deployment always
fixes the issue.
2025-03-11 14:14:11 +00:00
Lai Jiang bede56598c Fix console build for GKE (#2713)
We use the $environment property to set the console config. If it is not
given, 'alpha' is used, which has the same effect as 'production'.

TESTED=ran :jetty:copyConsole with
-Penvironment=(sandbox|production|alpha) and checked the resulting js
file.
2025-03-11 00:03:12 +00:00
Lai Jiang 467d9c7bf1 Fix cookie logging logic (#2711)
Make the logic more robust by using regex capture groups.
2025-03-10 23:10:03 +00:00
gbrodman e5ebe96c74 Add SQL code for simplified console update history table (#2710)
We'll remove the old ones, but this one adds the new simplified version
2025-03-07 19:40:19 +00:00
gbrodman 2ff4d97b0a Refactor console bulk domain action types (#2708)
This makes the action types a bit simpler -- this is possible because
we've reduced the scope of domain actions that we want to natively
support
2025-03-07 18:12:32 +00:00
gbrodman 6b0beeb477 Add BSA label to rdap-domain 404 responses for BSA domains (#2706) 2025-03-07 13:58:18 +00:00
Lai Jiang d2d43f4115 Fix a Cloud Scheduler deployment bug (#2707)
For GKE all tasks should be on backend, BSA was on its own service
because of egress IP constraint.

Also made it possible to specify a timeout for the Cloud Scheduler job,
with the default (3m) suitable for most tasks.
2025-03-06 16:25:52 +00:00
Lai Jiang 12fd206c35 Update README.md (#2705) 2025-03-05 16:55:04 +00:00
Lai Jiang a3f510d0db Log session cookies in metadata (#2703)
There are two session cookies, JSESSIONID, which is set by Jetty, and
GCLB, which is set by the Gateway.

In one session, every request other than the first one (the <hello>)
should have the same GCLB value, and every request after a successful
<login> should have the same JSESSIONID.

With these two metadata, we should be able to trace all requests that
*should* belong to the same session and debug issues with session
mismatch (if any).
2025-03-04 20:18:15 +00:00
gbrodman fa54c26ee2 Log transaction durations (#2682)
There can be delays in releasing predicate locks when we have
transactions that are long-lived -- even delays in releasing predicate
locks acquired by shorter-lived transactions. Logging the transaction
duration will allow us to get a sense as to transaction durations during
busy times.
2025-03-04 13:15:15 +00:00
45 changed files with 5356 additions and 4721 deletions
+37 -41
View File
@@ -12,16 +12,16 @@ Nomulus is an open source, scalable, cloud-based service for operating
[top-level domains](https://en.wikipedia.org/wiki/Top-level_domain) (TLDs). It
is the authoritative source for the TLDs that it runs, meaning that it is
responsible for tracking domain name ownership and handling registrations,
renewals, availability checks, and WHOIS requests. End-user registrants (i.e.
renewals, availability checks, and WHOIS requests. End-user registrants (i.e.,
people or companies that want to register a domain name) use an intermediate
domain name registrar acting on their behalf to interact with the registry.
Nomulus runs on [Google App Engine][gae] and is written primarily in Java. It is
the software that [Google Registry](https://www.registry.google/) uses to
operate TLDs such as .google, .app, .how, .soy, and .みんな. It can run any
number of TLDs in a single shared registry system using horizontal scaling. Its
source code is publicly available in this repository under the [Apache 2.0 free
and open source license](https://www.apache.org/licenses/LICENSE-2.0).
Nomulus runs on [Google Kubernetes Engine](https://cloud.google.com/kubernetes-engine)
and is written primarily in Java. It is the software that
[Google Registry](https://www.registry.google/) uses to operate TLDs such as .google,
.app, .how, .soy, and .みんな. It can run any number of TLDs in a single shared registry
system using horizontal scaling. Its source code is publicly available in this
repository under the [Apache 2.0 free and open source license](https://www.apache.org/licenses/LICENSE-2.0).
## Getting started
@@ -54,9 +54,11 @@ Nomulus has the following capabilities:
checking, updating, and transferring domain names.
* **[DNS](https://en.wikipedia.org/wiki/Domain_Name_System) interface**: The
registry provides a pluggable interface that can be implemented to handle
different DNS providers. It includes a sample implementation using Google
Cloud DNS as well as an RFC 2136 compliant implementation that works with
BIND.
different DNS providers. It includes a sample implementation using [Google
Cloud DNS](https://cloud.google.com/dns/), as well as an RFC 2136 compliant
implementation that works with BIND. If you are using Google Cloud DNS, you
may need to understand its capabilities and provide your own
multi-[AS](https://en.wikipedia.org/wiki/Autonomous_system_\(Internet\)) solution.
* **[WHOIS](https://en.wikipedia.org/wiki/WHOIS)**: A text-based protocol that
returns ownership and contact information on registered domain names.
* **[Registration Data Access Protocol
@@ -68,7 +70,7 @@ Nomulus has the following capabilities:
provider to allow take-over by another registry operator in the event of
serious failure. This is required by ICANN for all [new
gTLDs](https://newgtlds.icann.org/).
* **Premium pricing**: Communicates prices for premium domain names (i.e.
* **Premium pricing**: Communicates prices for premium domain names (i.e.,
those that are highly desirable) and supports configurable premium
registration and renewal prices. An extensible interface allows fully
programmatic pricing.
@@ -91,56 +93,50 @@ Nomulus has the following capabilities:
* **Administrative tool**: Performs the full range of administrative tasks
needed to manage a running registry system, including creating and
configuring new TLDs.
* **DNS interface**: An interface for DNS operations is provided so you can
write an implementation for your chosen provider, along with a sample
implementation that uses [Google Cloud DNS](https://cloud.google.com/dns/).
If you are using Google Cloud DNS you may need to understand its
capabilities and provide your own
multi-[AS](https://en.wikipedia.org/wiki/Autonomous_system_\(Internet\))
solution.
* **GAE Proxy**: App Engine Standard only serves HTTP/S traffic. A proxy to
forward traffic on EPP and WHOIS ports to App Engine via HTTPS is provided.
Instructions on setting up the proxy on
[Google Kubernetes Engine](https://cloud.google.com/kubernetes-engine/)
is [available](https://github.com/google/nomulus/blob/master/docs/proxy-setup.md).
Running the proxy on GKE supports IPv4 and IPv6 access, per ICANN's
requirements for gTLDs. The proxy can also run as a single jar file, or on
other Kubernetes providers, with modifications.
* **Secure storage of cryptographic keys**: A keyring interface is
provided for plugging in your own implementation (see [configuration
doc](https://github.com/google/nomulus/blob/master/docs/configuration.md)
for details), and an implementation based on
[Google Cloud Secret Manager](https://cloud.google.com/security/products/secret-manager) is
available.
* **TPC Proxy**: Nomulus is built on top of the [Jetty](https://jetty.org/)
container that implements the [Jakarta Servlet](https://jakarta.ee/specifications/servlet/)
specification and only serves HTTP/S traffic. A proxy to translate raw TCP traffic (e.g., EPP)
to and from HTTP is provided.
Instructions on setting up the proxy
are [available](https://github.com/google/nomulus/blob/master/docs/proxy-setup.md).
The proxy can either run in a separate cluster and communicate to Nomulus public HTTP
endpoints via the Internet, or as a sidecar with the Nomulus image in the same pod and
communicate to it via loopback.
## Additional components
Registry operators interested in deploying Nomulus will likely require some
additional components that are need to be configured separately.
additional components that need to be configured separately.
* A way to invoice registrars for domain name registrations and accept
payments. Nomulus records the information required to generate invoices in
[billing
events](https://github.com/google/nomulus/blob/master/docs/code-structure.md#billing-events).
* Fully automated reporting to meet ICANN's requirements for gTLDs. Nomulus
includes substantial reporting functionality but some additional work will
includes substantial reporting functionality, but some additional work will
be required by the operator in this area.
* A secure method for storing cryptographic keys. A keyring interface is
provided for plugging in your own implementation (see [configuration
doc](https://github.com/google/nomulus/blob/master/docs/configuration.md)
for details).
* System status and uptime monitoring.
## Outside references
* [Donuts](http://donuts.domains) Registry has helped review the code and
provided valuable feedback
* [Identity Digital](http://identity.digital) has helped review the code and
provided valuable feedback.
* [CoCCa](http://cocca.org.nz) and [FRED](https://fred.nic.cz) are other
open-source registry platforms in use by many TLDs
open-source registry platforms in use by many TLDs.
* We are not aware of any fully open source domain registrar projects, but
open source EPP Toolkits (not yet tested with Nomulus; may require
integration work) include:
* [EPP RTK Project](http://epp-rtk.sourceforge.net/)
* [CentralNic](https://www.centralnic.com/registry/labs)
* [Universal Registry/Registrar Toolkit](https://sourceforge.net/projects/epp-rtk/)
* [ari-toolkit](https://github.com/AusRegistry/ari-toolkit)
* [Net::DRI](https://metacpan.org/pod/Net::DRI)
* Some Open Source DNS Projects that may be useful, but which we have not
tested:
* [AtomiaDNS](http://atomiadns.com/)
* [PowerDNS](https://doc.powerdns.com/md/)
[gae]:https://cloud.google.com/appengine/docs/about-the-standard-environment
* [AtomiaDNS](https://github.com/atomia/atomiadns)
* [PowerDNS](https://github.com/PowerDNS/pdns)
+3 -7
View File
@@ -95,26 +95,22 @@ task stage {
description = 'Generates application directories for all services.'
}
// App-engine environment configuration. We set up all of the variables in
// the root project.
def environments = ['production', 'sandbox', 'alpha', 'crash', 'qa']
def gcpProject = null
apply from: "${rootDir.path}/projects.gradle"
if (environment == '') {
// Keep the project null, this will prevent deployment. Set the
// Keep the project null, this will prevent deployment. Set the
// environment to "alpha" because other code needs this property to
// explode the war file.
environment = 'alpha'
} else if (environment != 'production' && environment != 'sandbox') {
} else {
gcpProject = projects[environment]
if (gcpProject == null) {
throw new GradleException("-Penvironment must be one of " +
"${projects.keySet()}.")
}
project(':console-webapp').setProperty('configuration', environment)
}
rootProject.ext.environment = environment
+1 -1
View File
@@ -25,7 +25,7 @@ import textwrap
import re
# We should never analyze any generated files
UNIVERSALLY_SKIPPED_PATTERNS = {"/build/", "cloudbuild-caches", "/out/", ".git/", ".gradle/", "/dist/", "karma.conf.js", "polyfills.ts", "test.ts", "/docs/console-endpoints/"}
UNIVERSALLY_SKIPPED_PATTERNS = {"/build/", "cloudbuild-caches", "/out/", ".git/", ".gradle/", "/dist/", "/console-alpha/", "/console-crash/", "/console-qa", "/console-sandbox", "/console-production", "karma.conf.js", "polyfills.ts", "test.ts", "/docs/console-endpoints/"}
# We can't rely on CI to have the Enum package installed so we do this instead.
FORBIDDEN = 1
REQUIRED = 2
+1
View File
@@ -44,3 +44,4 @@ Thumbs.db
# Build artifact
/staged/dist
/staged/console-*
+10 -1
View File
@@ -96,11 +96,17 @@
"sourceMap": true,
"namedChunks": true
},
"development": {
"qa": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true,
"namedChunks": true
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true,
"namedChunks": true,
}
},
"defaultConfiguration": "production"
@@ -120,6 +126,9 @@
"sandbox": {
"buildTarget": "console-webapp:build:sandbox"
},
"qa": {
"buildTarget": "console-webapp:build:qa"
},
"development": {
"buildTarget": "console-webapp:build:development"
}
+43 -3
View File
@@ -40,15 +40,55 @@ task runConsoleWebappUnitTests(type: Exec) {
task buildConsoleWebapp(type: Exec) {
workingDir "${consoleDir}/"
executable 'npm'
def configuration = project.hasProperty('configuration') ?
project.getProperty('configuration') :
'production'
def configuration = project.getProperty('configuration')
args 'run', "build", "--configuration=${configuration}"
doFirst {
println "Building console for environment: ${configuration}"
}
}
task buildConsoleForAll() {}
def createConsoleTask = { env ->
project.tasks.register("buildConsoleFor${env.capitalize()}", Exec) {
workingDir "${consoleDir}/"
executable 'npm'
args 'run', 'build', "--configuration=${env}"
doFirst {
println "Building console for environment: ${env}"
}
doLast {
copy {
from "${consoleDir}/staged/dist/"
into "${consoleDir}/staged/console-${env}"
}
delete "${consoleDir}/staged/dist"
}
dependsOn(tasks.npmInstallDeps)
}
project.tasks.register("deleteConsoleFor${env.capitalize()}", Delete) {
delete "${consoleDir}/staged/console-${env}"
}
tasks.named('clean') {
dependsOn(tasks.named("deleteConsoleFor${env.capitalize()}"))
}
tasks.named('buildConsoleForAll') {
dependsOn(tasks.named("buildConsoleFor${env.capitalize()}"))
}
}
['alpha', 'crash', 'qa', 'sandbox', 'production'].forEach {env ->
createConsoleTask(env)
}
// Force an order so we don't run these tasks in parallel.
tasks.buildConsoleForCrash.mustRunAfter(tasks.buildConsoleForAlpha)
tasks.buildConsoleForQa.mustRunAfter(tasks.buildConsoleForCrash)
tasks.buildConsoleForSandbox.mustRunAfter(tasks.buildConsoleForQa)
tasks.buildConsoleForProduction.mustRunAfter(tasks.buildConsoleForSandbox)
// This task must run last, otherwise the previous tasks will have deleted the "dist" folder.
tasks.buildConsoleWebapp.mustRunAfter(tasks.buildConsoleForProduction)
task applyFormatting(type: Exec) {
workingDir "${consoleDir}/"
executable 'npm'
+1
View File
@@ -0,0 +1 @@
configuration=production
+1
View File
@@ -687,6 +687,7 @@ artifacts {
task runTestServer(type: JavaExec) {
main = 'google.registry.server.RegistryTestServerMain'
classpath = sourceSets.test.runtimeClasspath
dependsOn(rootProject.project('console-webapp').tasks.named('buildConsoleWebapp'))
}
/**
@@ -301,6 +301,7 @@
<url><![CDATA[/_dr/task/bsaValidate]]></url>
<name>bsaValidate</name>
<service>bsa</service>
<timeout>10m</timeout>
<description>
Validates the processed BSA data in the database against the original
block lists.
@@ -199,16 +199,16 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
/** Returns the TLD entities for the given TLD strings, throwing if any don't exist. */
public static ImmutableSet<Tld> get(Set<String> tlds) {
Map<String, Optional<Tld>> registries = CACHE.getAll(tlds);
ImmutableSet<String> missingRegistries =
registries.entrySet().stream()
Map<String, Optional<Tld>> tldObjects = CACHE.getAll(tlds);
ImmutableSet<String> missingTlds =
tldObjects.entrySet().stream()
.filter(e -> e.getValue().isEmpty())
.map(Map.Entry::getKey)
.collect(toImmutableSet());
if (missingRegistries.isEmpty()) {
return registries.values().stream().map(Optional::get).collect(toImmutableSet());
if (missingTlds.isEmpty()) {
return tldObjects.values().stream().map(Optional::get).collect(toImmutableSet());
} else {
throw new TldNotFoundException(missingRegistries);
throw new TldNotFoundException(missingTlds);
}
}
@@ -14,10 +14,10 @@
package google.registry.model.tld.label;
import static com.google.common.base.Charsets.US_ASCII;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static com.google.common.hash.Funnels.stringFunnel;
import static java.nio.charset.StandardCharsets.US_ASCII;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableMap;
@@ -16,7 +16,9 @@ package google.registry.module;
import static google.registry.util.GcpJsonFormatter.setCurrentRequest;
import static google.registry.util.GcpJsonFormatter.setCurrentTraceId;
import static google.registry.util.GcpJsonFormatter.setLabel;
import static google.registry.util.GcpJsonFormatter.unsetCurrentRequest;
import static google.registry.util.GcpJsonFormatter.unsetLabels;
import static google.registry.util.RandomStringGenerator.insecureRandomStringGenerator;
import static google.registry.util.StringGenerator.Alphabets.HEX_DIGITS_ONLY;
@@ -30,9 +32,12 @@ import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Arrays;
import java.util.Optional;
import java.util.logging.ConsoleHandler;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/** Servlet that handles all requests. */
public class RegistryServlet extends ServletBase {
@@ -48,6 +53,10 @@ public class RegistryServlet extends ServletBase {
private static final RequestHandler<RequestComponent> requestHandler = component.requestHandler();
private static final Lazy<MetricReporter> metricReporter = component.metricReporter();
// The regex pattern to capture the cookies that we want to log.
private static final Pattern COOKIE_REGEX_PATTERN =
Pattern.compile("(JSESSIONID|GCLB)=[\"]?([^;\"\\s]+)[\"]?");
private final String projectId;
static {
@@ -74,11 +83,20 @@ public class RegistryServlet extends ServletBase {
String userAgent = String.valueOf(req.getHeader("User-Agent"));
String protocol = req.getProtocol();
setCurrentRequest(requestMethod, requestUrl, userAgent, protocol);
Optional.ofNullable(req.getHeader("Cookie"))
.ifPresent(
cookie -> {
Matcher matcher = COOKIE_REGEX_PATTERN.matcher(cookie);
while (matcher.find()) {
setLabel(matcher.group(1), matcher.group(2));
}
});
try {
super.service(req, rsp);
} finally {
setCurrentTraceId(null);
unsetCurrentRequest();
unsetLabels();
}
}
@@ -13,8 +13,8 @@
// limitations under the License.
package google.registry.persistence.converter;
import static com.google.common.base.Charsets.US_ASCII;
import static com.google.common.hash.Funnels.stringFunnel;
import static java.nio.charset.StandardCharsets.US_ASCII;
import com.google.common.hash.BloomFilter;
import jakarta.persistence.AttributeConverter;
@@ -276,6 +276,10 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
}
T result = work.call();
txn.commit();
long duration = clock.nowUtc().getMillis() - txnInfo.transactionTime.getMillis();
if (duration >= 100) {
logger.atInfo().log("Transaction duration: %d milliseconds", duration);
}
return result;
} catch (Throwable e) {
// Catch a Throwable here so even Errors would lead to a rollback.
@@ -14,14 +14,15 @@
package google.registry.rdap;
import static com.google.common.base.Charsets.UTF_8;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN;
import static google.registry.request.Actions.getPathForAction;
import static google.registry.util.DomainNameUtils.canonicalizeHostname;
import static jakarta.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
import static jakarta.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
import static jakarta.servlet.http.HttpServletResponse.SC_NOT_FOUND;
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.flogger.FluentLogger;
import com.google.common.net.MediaType;
@@ -41,6 +42,7 @@ import google.registry.request.Parameter;
import google.registry.request.RequestMethod;
import google.registry.request.RequestPath;
import google.registry.request.Response;
import google.registry.util.Clock;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Optional;
@@ -60,6 +62,10 @@ public abstract class RdapActionBase implements Runnable {
private static final MediaType RESPONSE_MEDIA_TYPE =
MediaType.create("application", "rdap+json").withCharset(UTF_8);
private static final Gson GSON = new GsonBuilder().disableHtmlEscaping().create();
private static final Gson FORMATTED_OUTPUT_GSON =
new GsonBuilder().disableHtmlEscaping().setPrettyPrinting().create();
/** Whether to include or exclude deleted items from a query. */
protected enum DeletedItemHandling {
EXCLUDE,
@@ -75,6 +81,7 @@ public abstract class RdapActionBase implements Runnable {
@Inject @Parameter("formatOutput") Optional<Boolean> formatOutputParam;
@Inject @Config("rdapResultSetMaxSize") int rdapResultSetMaxSize;
@Inject RdapMetrics rdapMetrics;
@Inject Clock clock;
/** Builder for metric recording. */
final RdapMetrics.RdapMetricInformation.Builder metricInformationBuilder =
@@ -152,6 +159,10 @@ public abstract class RdapActionBase implements Runnable {
response.setStatus(SC_OK);
setPayload(replyObject);
metricInformationBuilder.setStatusCode(SC_OK);
} catch (RdapDomainAction.DomainBlockedByBsaException e) {
logger.atInfo().withCause(e).log("Domain blocked by BSA");
setErrorCodes(SC_NOT_FOUND);
setPayload(new RdapObjectClasses.DomainBlockedByBsaErrorResponse(e.getMessage()));
} catch (HttpException e) {
logger.atInfo().withCause(e).log("Error in RDAP.");
setError(e.getResponseCode(), e.getResponseCodeString(), e.getMessage());
@@ -166,8 +177,7 @@ public abstract class RdapActionBase implements Runnable {
}
void setError(int status, String title, String description) {
metricInformationBuilder.setStatusCode(status);
response.setStatus(status);
setErrorCodes(status);
try {
setPayload(ErrorResponse.create(status, title, description));
} catch (Exception ex) {
@@ -176,21 +186,18 @@ public abstract class RdapActionBase implements Runnable {
}
}
void setErrorCodes(int status) {
metricInformationBuilder.setStatusCode(status);
response.setStatus(status);
}
void setPayload(ReplyPayloadBase replyObject) {
if (requestMethod == Action.Method.HEAD) {
return;
}
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.disableHtmlEscaping();
if (formatOutputParam.orElse(false)) {
gsonBuilder.setPrettyPrinting();
}
Gson gson = gsonBuilder.create();
TopLevelReplyObject topLevelObject =
TopLevelReplyObject.create(replyObject, rdapJsonFormatter.createTosNotice());
Gson gson = formatOutputParam.orElse(false) ? FORMATTED_OUTPUT_GSON : GSON;
response.setPayload(gson.toJson(topLevelObject.toJson()));
}
@@ -119,7 +119,7 @@ final class RdapDataStructures {
*/
@AutoValue
@RestrictJsonNames("notices[]")
abstract static class Notice extends NoticeOrRemark {
public abstract static class Notice extends NoticeOrRemark {
/**
* Notice and Remark Type are defined in 10.2.1 of RFC 9083.
@@ -20,8 +20,11 @@ import static google.registry.request.Action.Method.GET;
import static google.registry.request.Action.Method.HEAD;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import com.google.common.net.InternetDomainName;
import google.registry.flows.EppException;
import google.registry.flows.domain.DomainFlowUtils;
import google.registry.model.domain.Domain;
import google.registry.model.tld.Tld;
import google.registry.rdap.RdapJsonFormatter.OutputDataType;
import google.registry.rdap.RdapMetrics.EndpointType;
import google.registry.rdap.RdapObjectClasses.RdapDomain;
@@ -51,8 +54,9 @@ public class RdapDomainAction extends RdapActionBase {
// RDAP Technical Implementation Guide 2.1.1 - we must support A-label (Punycode) and U-label
// (Unicode) formats. canonicalizeName will transform Unicode to Punycode so we support both.
pathSearchString = canonicalizeName(pathSearchString);
InternetDomainName domainName;
try {
validateDomainName(pathSearchString);
domainName = validateDomainName(pathSearchString);
} catch (EppException e) {
throw new BadRequestException(
String.format(
@@ -66,6 +70,7 @@ public class RdapDomainAction extends RdapActionBase {
pathSearchString,
shouldIncludeDeleted() ? START_OF_TIME : rdapJsonFormatter.getRequestTime());
if (domain.isEmpty() || !isAuthorized(domain.get())) {
handlePossibleBsaBlock(domainName);
// RFC7480 5.3 - if the server wishes to respond that it doesn't have data satisfying the
// query, it MUST reply with 404 response code.
//
@@ -75,4 +80,17 @@ public class RdapDomainAction extends RdapActionBase {
}
return rdapJsonFormatter.createRdapDomain(domain.get(), OutputDataType.FULL);
}
private void handlePossibleBsaBlock(InternetDomainName domainName) {
Tld tld = Tld.get(domainName.parent().toString());
if (DomainFlowUtils.isBlockedByBsa(domainName.parts().getFirst(), tld, clock.nowUtc())) {
throw new DomainBlockedByBsaException(domainName + " blocked by BSA");
}
}
static class DomainBlockedByBsaException extends RuntimeException {
DomainBlockedByBsaException(String message) {
super(message);
}
}
}
@@ -63,8 +63,21 @@ public class RdapIcannStandardInformation {
.build())
.build();
/** Not required, but provided when a domain is blocked by BSA. */
private static final Notice DOMAIN_BLOCKED_BY_BSA_NOTICE =
Notice.builder()
.setTitle("Blocked Domain")
.setDescription("This name has been blocked by a GlobalBlock service")
.addLink(
Link.builder()
.setRel("alternate")
.setHref("https://brandsafetyalliance.co")
.setType("text/html")
.build())
.build();
/** Boilerplate notices required by domain responses. */
static final ImmutableList<Notice> domainBoilerplateNotices =
static final ImmutableList<Notice> DOMAIN_BOILERPLATE_NOTICES =
ImmutableList.of(
CONFORMANCE_NOTICE,
// RDAP Response Profile 2.6.3
@@ -72,8 +85,12 @@ public class RdapIcannStandardInformation {
// RDAP Response Profile 2.11
INACCURACY_COMPLAINT_FORM_NOTICE);
/** Boilerplate notice for when a domain is blocked by BSA. */
static final ImmutableList<Notice> DOMAIN_BLOCKED_BY_BSA_BOILERPLATE_NOTICES =
ImmutableList.of(DOMAIN_BLOCKED_BY_BSA_NOTICE);
/** Boilerplate remarks required by nameserver and entity responses. */
static final ImmutableList<Notice> nameserverAndEntityBoilerplateNotices =
static final ImmutableList<Notice> NAMESERVER_AND_ENTITY_BOILERPLATE_NOTICES =
ImmutableList.of(CONFORMANCE_NOTICE);
/**
@@ -39,6 +39,7 @@ import google.registry.rdap.RdapDataStructures.RdapConformance;
import google.registry.rdap.RdapDataStructures.RdapStatus;
import google.registry.rdap.RdapDataStructures.Remark;
import google.registry.util.Idn;
import jakarta.servlet.http.HttpServletResponse;
import java.util.Optional;
/** Object Classes defined in RFC 9083 section 5. */
@@ -137,10 +138,22 @@ final class RdapObjectClasses {
* suppress them for other types of responses (e.g. help).
*/
public enum BoilerplateType {
DOMAIN,
NAMESERVER,
ENTITY,
OTHER
DOMAIN(RdapIcannStandardInformation.DOMAIN_BOILERPLATE_NOTICES),
DOMAIN_BLOCKED_BY_BSA(RdapIcannStandardInformation.DOMAIN_BLOCKED_BY_BSA_BOILERPLATE_NOTICES),
NAMESERVER(RdapIcannStandardInformation.NAMESERVER_AND_ENTITY_BOILERPLATE_NOTICES),
ENTITY(RdapIcannStandardInformation.NAMESERVER_AND_ENTITY_BOILERPLATE_NOTICES),
OTHER(ImmutableList.of());
@SuppressWarnings("ImmutableEnumChecker") // immutable lists are, in fact, immutable
private final ImmutableList<Notice> notices;
BoilerplateType(ImmutableList<Notice> notices) {
this.notices = notices;
}
public ImmutableList<Notice> getNotices() {
return notices;
}
}
/**
@@ -173,14 +186,7 @@ final class RdapObjectClasses {
@JsonableElement("notices[]") abstract Notice aTosNotice();
@JsonableElement("notices") ImmutableList<Notice> boilerplateNotices() {
return switch (aAreplyObject().boilerplateType) {
case DOMAIN -> RdapIcannStandardInformation.domainBoilerplateNotices;
case NAMESERVER, ENTITY ->
RdapIcannStandardInformation.nameserverAndEntityBoilerplateNotices;
default -> // things other than domains, nameservers and entities do not yet have
// boilerplate
ImmutableList.of();
};
return aAreplyObject().boilerplateType.getNotices();
}
static TopLevelReplyObject create(ReplyPayloadBase replyObject, Notice tosNotice) {
@@ -532,6 +538,25 @@ final class RdapObjectClasses {
}
}
/** Specialized error response body for when a domain is blocked by BSA. */
@RestrictJsonNames({})
@SuppressWarnings("UnusedVariable")
public static class DomainBlockedByBsaErrorResponse extends ReplyPayloadBase {
@JsonableElement private static final LanguageIdentifier lang = LanguageIdentifier.EN;
@JsonableElement private static final int errorCode = HttpServletResponse.SC_NOT_FOUND;
@JsonableElement private static final String title = "Not Found";
@JsonableElement private final ImmutableList<String> description;
DomainBlockedByBsaErrorResponse(String message) {
super(BoilerplateType.DOMAIN_BLOCKED_BY_BSA);
this.description = ImmutableList.of(message);
}
}
/** Error Response Body defined in 6 of RFC 9083. */
@RestrictJsonNames({})
@AutoValue
@@ -27,7 +27,7 @@ import java.util.List;
* @param numResourcesRetrieved Number of resources retrieved from the database in the process of
* assembling the data set.
*/
record RdapResultSet<T extends EppResource>(
public record RdapResultSet<T extends EppResource>(
ImmutableList<T> resources,
IncompletenessWarningType incompletenessWarningType,
int numResourcesRetrieved) {
@@ -14,9 +14,9 @@
package google.registry.rdap;
import static com.google.common.base.Charsets.UTF_8;
import static google.registry.persistence.transaction.TransactionManagerFactory.replicaTm;
import static google.registry.util.DateTimeUtils.END_OF_TIME;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
@@ -69,7 +69,7 @@ public abstract class RdapSearchActionBase extends RdapActionBase {
public final BaseSearchResponse getJsonObjectForResource(
String pathSearchString, boolean isHeadRequest) {
// The pathSearchString is not used by search commands.
if (pathSearchString.length() > 0) {
if (!pathSearchString.isEmpty()) {
throw new BadRequestException("Unexpected path");
}
decodeCursorToken();
@@ -323,7 +323,8 @@ public abstract class RdapSearchActionBase extends RdapActionBase {
if (partialStringQuery.getHasWildcard()) {
builder =
builder.where(
filterField, criteriaBuilder::like,
filterField,
criteriaBuilder::like,
String.format("%s%%", partialStringQuery.getInitialString()));
} else {
// no wildcard means we use a standard equals query
@@ -40,7 +40,7 @@ import java.util.Optional;
abstract class RdapSearchResults {
/** Responding To Searches defined in 8 of RFC 9083. */
abstract static class BaseSearchResponse extends ReplyPayloadBase {
public abstract static class BaseSearchResponse extends ReplyPayloadBase {
abstract IncompletenessWarningType incompletenessWarningType();
abstract ImmutableMap<String, URI> navigationLinks();
@@ -20,6 +20,7 @@ import com.google.common.flogger.FluentLogger;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.logging.Level;
import javax.annotation.Nullable;
/** Base for exceptions that cause an HTTP error response. */
public abstract class HttpException extends RuntimeException {
@@ -33,13 +34,14 @@ public abstract class HttpException extends RuntimeException {
private final int responseCode;
protected HttpException(int responseCode, String message, Throwable cause, Level logLevel) {
protected HttpException(
int responseCode, String message, @Nullable Throwable cause, Level logLevel) {
super(message, cause);
this.responseCode = responseCode;
this.logLevel = logLevel;
}
protected HttpException(int responseCode, String message, Throwable cause) {
protected HttpException(int responseCode, String message, @Nullable Throwable cause) {
this(responseCode, message, cause, Level.INFO);
}
@@ -117,22 +119,6 @@ public abstract class HttpException extends RuntimeException {
}
}
/** Exception that causes a 403 response. */
public static final class ForbiddenException extends HttpException {
public ForbiddenException(String message) {
super(HttpServletResponse.SC_FORBIDDEN, message, null);
}
public ForbiddenException(String message, Exception cause) {
super(HttpServletResponse.SC_FORBIDDEN, message, cause);
}
@Override
public String getResponseCodeString() {
return "Forbidden";
}
}
/** Exception that causes a 404 response. */
public static final class NotFoundException extends HttpException {
public NotFoundException() {
@@ -149,18 +135,6 @@ public abstract class HttpException extends RuntimeException {
}
}
/** Exception that causes a 405 response. */
public static final class MethodNotAllowedException extends HttpException {
public MethodNotAllowedException() {
super(HttpServletResponse.SC_METHOD_NOT_ALLOWED, "Method not allowed", null);
}
@Override
public String getResponseCodeString() {
return "Method Not Allowed";
}
}
/** Exception that causes a 409 response. */
public static final class ConflictException extends HttpException {
public ConflictException(String message) {
@@ -14,12 +14,11 @@
package google.registry.ui.server.console.domains;
import com.google.common.collect.ImmutableMap;
import com.google.gson.JsonElement;
import google.registry.model.console.ConsolePermission;
/** An action that will run a delete EPP command on the given domain. */
public class ConsoleBulkDomainDeleteActionType implements ConsoleDomainActionType {
public class ConsoleBulkDomainDeleteActionType extends ConsoleDomainActionType {
private static final String DOMAIN_DELETE_XML =
"""
@@ -42,16 +41,13 @@ public class ConsoleBulkDomainDeleteActionType implements ConsoleDomainActionTyp
</command>
</epp>""";
private final String reason;
public ConsoleBulkDomainDeleteActionType(JsonElement jsonElement) {
this.reason = jsonElement.getAsJsonObject().get("reason").getAsString();
super(jsonElement);
}
@Override
public String getXmlContentsToRun(String domainName) {
return ConsoleDomainActionType.fillSubstitutions(
DOMAIN_DELETE_XML, ImmutableMap.of("DOMAIN_NAME", domainName, "REASON", reason));
protected String getXmlTemplate() {
return DOMAIN_DELETE_XML;
}
@Override
@@ -14,12 +14,11 @@
package google.registry.ui.server.console.domains;
import com.google.common.collect.ImmutableMap;
import com.google.gson.JsonElement;
import google.registry.model.console.ConsolePermission;
/** An action that will suspend the given domain, assigning all 5 server*Prohibited statuses. */
public class ConsoleBulkDomainSuspendActionType implements ConsoleDomainActionType {
public class ConsoleBulkDomainSuspendActionType extends ConsoleDomainActionType {
private static final String DOMAIN_SUSPEND_XML =
"""
@@ -52,16 +51,13 @@ public class ConsoleBulkDomainSuspendActionType implements ConsoleDomainActionTy
</command>
</epp>""";
private final String reason;
public ConsoleBulkDomainSuspendActionType(JsonElement jsonElement) {
this.reason = jsonElement.getAsJsonObject().get("reason").getAsString();
super(jsonElement);
}
@Override
public String getXmlContentsToRun(String domainName) {
return ConsoleDomainActionType.fillSubstitutions(
DOMAIN_SUSPEND_XML, ImmutableMap.of("DOMAIN_NAME", domainName, "REASON", reason));
protected String getXmlTemplate() {
return DOMAIN_SUSPEND_XML;
}
@Override
@@ -14,12 +14,11 @@
package google.registry.ui.server.console.domains;
import com.google.common.collect.ImmutableMap;
import com.google.gson.JsonElement;
import google.registry.model.console.ConsolePermission;
/** An action that will unsuspend the given domain, removing all 5 server*Prohibited statuses. */
public class ConsoleBulkDomainUnsuspendActionType implements ConsoleDomainActionType {
public class ConsoleBulkDomainUnsuspendActionType extends ConsoleDomainActionType {
private static final String DOMAIN_SUSPEND_XML =
"""
@@ -52,16 +51,13 @@ public class ConsoleBulkDomainUnsuspendActionType implements ConsoleDomainAction
</command>
</epp>""";
private final String reason;
public ConsoleBulkDomainUnsuspendActionType(JsonElement jsonElement) {
this.reason = jsonElement.getAsJsonObject().get("reason").getAsString();
super(jsonElement);
}
@Override
public String getXmlContentsToRun(String domainName) {
return ConsoleDomainActionType.fillSubstitutions(
DOMAIN_SUSPEND_XML, ImmutableMap.of("DOMAIN_NAME", domainName, "REASON", reason));
protected String getXmlTemplate() {
return DOMAIN_SUSPEND_XML;
}
@Override
@@ -14,20 +14,18 @@
package google.registry.ui.server.console.domains;
import com.google.common.collect.ImmutableMap;
import com.google.common.escape.Escaper;
import com.google.common.xml.XmlEscapers;
import com.google.gson.JsonElement;
import google.registry.model.console.ConsolePermission;
import java.util.Map;
/**
* A type of EPP action to perform on domain(s), run by the {@link ConsoleBulkDomainAction}.
*
* <p>Each {@link BulkAction} defines the class that implements that action, including the EPP XML
* that will be run and the permission required.
* <p>Each {@link BulkAction} defines the class that extends that action, including the EPP XML that
* will be run and the permission required.
*/
public interface ConsoleDomainActionType {
public abstract class ConsoleDomainActionType {
enum BulkAction {
DELETE(ConsoleBulkDomainDeleteActionType.class),
@@ -45,20 +43,6 @@ public interface ConsoleDomainActionType {
}
}
Escaper XML_ESCAPER = XmlEscapers.xmlContentEscaper();
static String fillSubstitutions(String xmlTemplate, ImmutableMap<String, String> replacements) {
String xml = xmlTemplate;
for (Map.Entry<String, String> entry : replacements.entrySet()) {
xml = xml.replaceAll("%" + entry.getKey() + "%", XML_ESCAPER.escape(entry.getValue()));
}
return xml;
}
String getXmlContentsToRun(String domainName);
ConsolePermission getNecessaryPermission();
static ConsoleDomainActionType parseActionType(String bulkDomainAction, JsonElement jsonElement) {
BulkAction bulkAction = BulkAction.valueOf(bulkDomainAction);
try {
@@ -67,4 +51,38 @@ public interface ConsoleDomainActionType {
throw new RuntimeException(e); // shouldn't happen
}
}
private static final Escaper XML_ESCAPER = XmlEscapers.xmlContentEscaper();
private final String reason;
public ConsoleDomainActionType(JsonElement jsonElement) {
this.reason = jsonElement.getAsJsonObject().get("reason").getAsString();
}
/** Returns the full XML representing this action, including all substitutions. */
public String getXmlContentsToRun(String domainName) {
return fillSubstitutions(getXmlTemplate(), domainName);
}
/** Returns the permission necessary to successfully perform this action. */
public abstract ConsolePermission getNecessaryPermission();
/** Returns the XML template contents for this action. */
protected abstract String getXmlTemplate();
/**
* Fills out the default set of substitutions in the provided XML template.
*
* <p>Override this method if non-default substitutions are required.
*/
protected String fillSubstitutions(String xmlTemplate, String domainName) {
String xml = xmlTemplate;
xml = replaceValue(xml, "DOMAIN_NAME", domainName);
return replaceValue(xml, "REASON", reason);
}
private String replaceValue(String xml, String key, String value) {
return xml.replaceAll("%" + key + "%", XML_ESCAPER.escape(value));
}
}
@@ -13,11 +13,11 @@
// limitations under the License.
package google.registry.persistence.converter;
import static com.google.common.base.Charsets.US_ASCII;
import static com.google.common.hash.Funnels.stringFunnel;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.testing.DatabaseHelper.insertInDb;
import static java.nio.charset.StandardCharsets.US_ASCII;
import com.google.common.collect.ImmutableSet;
import com.google.common.hash.BloomFilter;
@@ -21,7 +21,6 @@ import static google.registry.testing.DatabaseHelper.insertSimpleResources;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.testcontainers.containers.PostgreSQLContainer.POSTGRESQL_PORT;
import com.google.common.base.Charsets;
import com.google.common.base.Joiner;
import com.google.common.base.Suppliers;
import com.google.common.collect.ImmutableList;
@@ -305,7 +304,7 @@ public abstract class JpaTransactionManagerExtension
private static String readSqlInClassPath(String sqlScriptPath) {
try {
return Resources.toString(Resources.getResource(sqlScriptPath), Charsets.UTF_8);
return Resources.toString(Resources.getResource(sqlScriptPath), UTF_8);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
@@ -85,6 +85,7 @@ abstract class RdapActionBaseTestCase<A extends RdapActionBase> {
action.rdapJsonFormatter = RdapTestHelper.getTestRdapJsonFormatter(clock);
action.rdapMetrics = rdapMetrics;
action.requestMethod = GET;
action.clock = new FakeClock(DateTime.parse("2025-01-01T00:00:00.000Z"));
logout();
}
@@ -15,6 +15,7 @@
package google.registry.rdap;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.bsa.persistence.BsaTestingUtils.persistBsaLabel;
import static google.registry.model.EppResourceUtils.loadByForeignKey;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.persistActiveDomain;
@@ -27,8 +28,11 @@ import static google.registry.testing.FullFieldsTestEntityHelper.makeHistoryEntr
import static google.registry.testing.FullFieldsTestEntityHelper.makeRegistrar;
import static google.registry.testing.FullFieldsTestEntityHelper.makeRegistrarPocs;
import static google.registry.testing.GsonSubject.assertAboutJson;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static org.mockito.Mockito.verify;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.gson.JsonObject;
import google.registry.model.contact.Contact;
@@ -608,6 +612,34 @@ class RdapDomainActionTest extends RdapActionBaseTestCase<RdapDomainAction> {
.build());
}
@Test
void testBlockedByBsa() {
persistResource(
Tld.get("lol").asBuilder().setBsaEnrollStartTime(Optional.of(START_OF_TIME)).build());
persistBsaLabel("example");
ImmutableMap<?, ?> expectedBsaNotice =
ImmutableMap.of(
"description",
ImmutableList.of("This name has been blocked by a GlobalBlock service"),
"title",
"Blocked Domain",
"links",
ImmutableList.of(
ImmutableMap.of(
"href",
"https://brandsafetyalliance.co",
"rel",
"alternate",
"type",
"text/html")));
JsonObject expectedErrorResponse = generateExpectedJsonError("example.lol blocked by BSA", 404);
expectedErrorResponse
.getAsJsonArray("notices")
.add(RdapTestHelper.GSON.toJsonTree(expectedBsaNotice));
assertAboutJson().that(generateActualJson("example.lol")).isEqualTo(expectedErrorResponse);
assertThat(response.getStatus()).isEqualTo(404);
}
private Domain persistActiveDomainWithHost(
String label, String tld, DateTime creationTime, DateTime expirationTime) {
return persistResource(
@@ -33,8 +33,7 @@ import java.util.Map;
/** Test helper methods for RDAP tests. */
class RdapTestHelper {
private static final Gson GSON =
new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
static final Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
static JsonElement createJson(String... lines) {
return GSON.fromJson(Joiner.on("\n").join(lines), JsonElement.class);
@@ -240,5 +239,4 @@ class RdapTestHelper {
obj.remove("rdapConformance");
return reply;
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1
View File
@@ -184,3 +184,4 @@ V183__remove_fk_billingrecurrence_domainhistory.sql
V184__remove_fk_pollmessage_domainhistory.sql
V185__remove_fk_domaintransactionrecord_domainhistory.sql
V186__remove_fk_domaindsdatahistory_domainhistory.sql
V187__console_update_history.sql
@@ -0,0 +1,34 @@
-- Copyright 2025 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.
CREATE TABLE "ConsoleUpdateHistory" (
revision_id bigint NOT NULL,
modification_time timestamp with time zone NOT NULL,
method text NOT NULL,
type text NOT NULL,
url text NOT NULL,
description text,
acting_user text NOT NULL,
PRIMARY KEY (revision_id),
CONSTRAINT fk_console_update_history_acting_user
FOREIGN KEY(acting_user)
REFERENCES "User" (email_address)
);
CREATE INDEX IF NOT EXISTS idx_console_update_history_acting_user
ON "ConsoleUpdateHistory" (acting_user);
CREATE INDEX IF NOT EXISTS idx_console_update_history_type
ON "ConsoleUpdateHistory" (type);
CREATE INDEX IF NOT EXISTS idx_console_update_history_modification_time
ON "ConsoleUpdateHistory" (modification_time);
@@ -272,6 +272,21 @@ CREATE TABLE public."ConsoleEppActionHistory" (
);
--
-- Name: ConsoleUpdateHistory; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public."ConsoleUpdateHistory" (
revision_id bigint NOT NULL,
modification_time timestamp with time zone NOT NULL,
method text NOT NULL,
type text NOT NULL,
url text NOT NULL,
description text,
acting_user text NOT NULL
);
--
-- Name: Contact; Type: TABLE; Schema: public; Owner: -
--
@@ -1530,6 +1545,14 @@ ALTER TABLE ONLY public."ConsoleEppActionHistory"
ADD CONSTRAINT "ConsoleEppActionHistory_pkey" PRIMARY KEY (history_revision_id);
--
-- Name: ConsoleUpdateHistory ConsoleUpdateHistory_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public."ConsoleUpdateHistory"
ADD CONSTRAINT "ConsoleUpdateHistory_pkey" PRIMARY KEY (revision_id);
--
-- Name: ContactHistory ContactHistory_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
@@ -2052,6 +2075,27 @@ CREATE INDEX idx9g3s7mjv1yn4t06nqid39whss ON public."AllocationToken" USING btre
CREATE INDEX idx9q53px6r302ftgisqifmc6put ON public."ContactHistory" USING btree (history_type);
--
-- Name: idx_console_update_history_acting_user; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX idx_console_update_history_acting_user ON public."ConsoleUpdateHistory" USING btree (acting_user);
--
-- Name: idx_console_update_history_modification_time; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX idx_console_update_history_modification_time ON public."ConsoleUpdateHistory" USING btree (modification_time);
--
-- Name: idx_console_update_history_type; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX idx_console_update_history_type ON public."ConsoleUpdateHistory" USING btree (type);
--
-- Name: idx_registry_lock_registrar_id; Type: INDEX; Schema: public; Owner: -
--
@@ -2690,6 +2734,14 @@ ALTER TABLE ONLY public."BillingRecurrence"
ADD CONSTRAINT fk_billing_recurrence_registrar_id FOREIGN KEY (registrar_id) REFERENCES public."Registrar"(registrar_id) DEFERRABLE INITIALLY DEFERRED;
--
-- Name: ConsoleUpdateHistory fk_console_update_history_acting_user; Type: FK CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public."ConsoleUpdateHistory"
ADD CONSTRAINT fk_console_update_history_acting_user FOREIGN KEY (acting_user) REFERENCES public."User"(email_address);
--
-- Name: ContactHistory fk_contact_history_contact_repo_id; Type: FK CONSTRAINT; Schema: public; Owner: -
--
+4 -4
View File
@@ -37,11 +37,11 @@ dependencies {
}
tasks.register('copyConsole', Copy) {
from("${rootDir}/console-webapp/staged/dist") {
include "**/*"
from("${rootDir}/console-webapp/staged/") {
include "console-*/*"
}
into layout.buildDirectory.dir('jetty-base/webapps/console')
dependsOn(':console-webapp:buildConsoleWebapp')
into layout.buildDirectory.dir('jetty-base/webapps/')
dependsOn(':console-webapp:buildConsoleForAll')
}
tasks.register('stage') {
+4 -2
View File
@@ -13,8 +13,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.
environment=$1
env=${1:-"alpha"}
cd /jetty-base
java -Dgoogle.registry.environment=${environment:-"alpha"} \
cp -rf webapps/console-${env} webapps/console
echo "Running ${env}"
java -Dgoogle.registry.environment=${env} \
-Djava.util.logging.config.file=/logging.properties \
-jar /usr/local/jetty/start.jar
+1 -1
View File
@@ -51,7 +51,7 @@ else
mv services/"${service}"/build/staged-app "${dest}/${service}"
done
./gradlew :console-webapp:buildConsoleWebapp -Pconfiguration="${environment}"
./gradlew :console-webapp:buildConsoleWebapp -Penvironment="${environment}"
mkdir -p "${dest}/console" && cp -r console-webapp/staged/* "${dest}/console"
mv core/build/resources/main/google/registry/env/common/META-INF \
@@ -70,6 +70,7 @@ type Task struct {
URL string `xml:"url"`
Description string `xml:"description"`
Service string `xml:"service"`
Timeout string `xml:"timeout"`
Schedule string `xml:"schedule"`
Name string `xml:"name"`
}
@@ -189,7 +190,9 @@ func (manager TasksSyncManager) getArgs(task Task, operationType string) []strin
description = strings.ReplaceAll(description, "\n", " ")
var service = "backend"
if task.Service != "backend" && task.Service != "" {
// Only BSA tasks run on the BSA service in GAE. GKE tasks are always
// on the backend service.
if task.Service != "backend" && task.Service != "" && !gke {
service = task.Service
}
@@ -200,7 +203,7 @@ func (manager TasksSyncManager) getArgs(task Task, operationType string) []strin
uri = fmt.Sprintf("https://%s-dot-%s.appspot.com%s", service, projectName, strings.TrimSpace(task.URL))
}
return []string{
args := []string{
"--project", projectName,
"scheduler", "jobs", operationType,
"http", task.Name,
@@ -212,6 +215,12 @@ func (manager TasksSyncManager) getArgs(task Task, operationType string) []strin
"--oidc-service-account-email", getCloudSchedulerServiceAccountEmail(),
"--oidc-token-audience", clientId,
}
if task.Timeout != "" {
args = append(args, "--attempt-deadline", task.Timeout)
}
return args
}
func (manager TasksSyncManager) fetchExistingRecords() ExistingEntries {
+46
View File
@@ -0,0 +1,46 @@
# This will delete canary GAE versions named "nomulus".
#
# For reasons unknown, Spinnaker occasionally gets stuck when deploying to GAE
# canary, and the fix is to manually delete the canary versions before the
# deployment.
#
# To manually trigger a build on GCB, run:
# gcloud builds submit --config=cloudbuild-delete-canary.yaml \
# --substitutions=_ENV=[ENV] ..
#
# To trigger a build automatically, follow the instructions below and add a trigger:
# https://cloud.google.com/cloud-build/docs/running-builds/automate-builds
#
steps:
# Pull the credential for nomulus tool.
- name: 'gcr.io/$PROJECT_ID/builder:latest'
entrypoint: /bin/bash
args:
- -c
- |
set -e
gcloud secrets versions access latest \
--secret nomulus-tool-cloudbuild-credential > tool-credential.json
# Delete unused GAE versions.
- name: 'gcr.io/$PROJECT_ID/builder:latest'
entrypoint: /bin/bash
args:
- -c
- |
if [ ${_ENV} == production ]
then
project_id="domain-registry"
else
project_id="domain-registry-${_ENV}"
fi
gcloud auth activate-service-account --key-file=tool-credential.json
for service in default pubapi backend bsa tools console
do
gcloud app versions delete nomulus --service=$service \
--project=$project_id --quiet;
done
timeout: 3600s
options:
machineType: 'N1_HIGHCPU_8'
+3
View File
@@ -88,6 +88,7 @@ steps:
sed -i s/builder:latest/builder@$builder_digest/g release/cloudbuild-schema-deploy.yaml
sed -i s/builder:latest/builder@$builder_digest/g release/cloudbuild-schema-verify.yaml
sed -i s/builder:latest/builder@$builder_digest/g release/cloudbuild-delete.yaml
sed -i s/builder:latest/builder@$builder_digest/g release/cloudbuild-delete-canary.yaml
sed -i s/builder:latest/builder@$builder_digest/g release/cloudbuild-restart-proxies.yaml
sed -i s/GCP_PROJECT/${PROJECT_ID}/ proxy/kubernetes/proxy-*.yaml
sed -i s/'$${TAG_NAME}'/${TAG_NAME}/g release/cloudbuild-sync-and-tag.yaml
@@ -100,6 +101,8 @@ steps:
> release/cloudbuild-deploy-gke-${environment}.yaml
sed s/'$${_ENV}'/${environment}/g release/cloudbuild-delete.yaml \
> release/cloudbuild-delete-${environment}.yaml
sed s/'$${_ENV}'/${environment}/g release/cloudbuild-delete-canary.yaml \
> release/cloudbuild-delete-canary-${environment}.yaml
sed s/'$${_ENV}'/${environment}/g release/cloudbuild-restart-proxies.yaml \
> release/cloudbuild-restart-proxies-${environment}.yaml
sed s/'$${_ENV}'/${environment}/g release/cloudbuild-restart-proxies.yaml | \
@@ -20,6 +20,7 @@ import com.google.common.flogger.backend.system.SimpleLogRecord;
import com.google.gson.Gson;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
@@ -58,6 +59,13 @@ public class GcpJsonFormatter extends Formatter {
/** JSON field that stores the parameters of the current request, if any. */
private static final String HTTP_REQUEST = "httRequest";
/**
* JSON field that stores the arbitrary labels.
*
* <p>In practice, we store session cookies so we can identify requests from the same session.
*/
private static final String LABELS = "logging.googleapis.com/labels";
/** JSON field that contains the content, this will show up as the main entry in a log. */
private static final String MESSAGE = "message";
@@ -83,6 +91,9 @@ public class GcpJsonFormatter extends Formatter {
private static final ThreadLocal<HttpRequest> request = new ThreadLocal<>();
private static final ThreadLocal<Map<String, String>> labels =
ThreadLocal.withInitial(HashMap::new);
/**
* Set the Trace ID associated with any logging done by the current thread.
*
@@ -107,6 +118,19 @@ public class GcpJsonFormatter extends Formatter {
request.set(new HttpRequest(requestMethod, requestUrl, userAgent, protocol));
}
/**
* Set an arbitrary key-value pair as a label for the current request.
*
* <p>Existing labels will be overwritten.
*/
public static void setLabel(String key, String value) {
labels.get().put(key, value);
}
public static void unsetLabels() {
labels.get().clear();
}
public static void unsetCurrentRequest() {
request.remove();
}
@@ -180,6 +204,9 @@ public class GcpJsonFormatter extends Formatter {
if (request.get() != null) {
json.put(HTTP_REQUEST, request.get());
}
if (!labels.get().isEmpty()) {
json.put(LABELS, labels.get());
}
// This trailing newline is required for the proxy because otherwise multiple logs might be
// sent to Stackdriver together (due to the async nature of the proxy), and not parsed
// correctly.
@@ -72,6 +72,7 @@ class GcpJsonFormatterTest {
jdkLogger.removeHandler(handler);
GcpJsonFormatter.setCurrentTraceId(null);
GcpJsonFormatter.unsetCurrentRequest();
GcpJsonFormatter.unsetLabels();
}
@Test
@@ -79,7 +80,7 @@ class GcpJsonFormatterTest {
logger.atInfo().log("Something I have to say");
handler.close();
String output = ostream.toString(StandardCharsets.US_ASCII);
assertThat(output).isEqualTo(makeJson("INFO", 79, "testSuccess", "Something I have to say"));
assertThat(output).isEqualTo(makeJson("INFO", 80, "testSuccess", "Something I have to say"));
}
@Test
@@ -88,7 +89,7 @@ class GcpJsonFormatterTest {
logger.atInfo().log("Something I have to say");
handler.close();
String output = ostream.toString(StandardCharsets.US_ASCII);
String expected = makeJson("INFO", 88, "testSuccess_traceId", "Something I have to say");
String expected = makeJson("INFO", 89, "testSuccess_traceId", "Something I have to say");
// Remove the last two characters (}, \n) from the template and add the trace ID.
expected =
expected.substring(0, expected.length() - 2)
@@ -103,7 +104,7 @@ class GcpJsonFormatterTest {
handler.close();
String output = ostream.toString(StandardCharsets.US_ASCII);
String expected =
makeJson("INFO", 102, "testSuccess_currentRequest", "Something I have to say");
makeJson("INFO", 103, "testSuccess_currentRequest", "Something I have to say");
// Remove the last two characters (}, \n) from the template and add the request.
expected =
expected.substring(0, expected.length() - 2)
@@ -112,13 +113,28 @@ class GcpJsonFormatterTest {
assertThat(output).isEqualTo(expected);
}
@Test
void testSuccess_labels() {
GcpJsonFormatter.setLabel("label1", "value1");
GcpJsonFormatter.setLabel("label2", "value2");
logger.atInfo().log("Something I have to say");
handler.close();
String output = ostream.toString(StandardCharsets.US_ASCII);
String expected = makeJson("INFO", 120, "testSuccess_labels", "Something I have to say");
// Remove the last two characters (}, \n) from the template and add the labels.
expected =
expected.substring(0, expected.length() - 2)
+ ",\"logging.googleapis.com/labels\":{\"label1\":\"value1\",\"label2\":\"value2\"}}\n";
assertThat(output).isEqualTo(expected);
}
@Test
void testSuccess_logLevel() {
logger.atSevere().log("Something went terribly wrong");
handler.close();
String output = ostream.toString(StandardCharsets.US_ASCII);
assertThat(output)
.isEqualTo(makeJson("ERROR", 117, "testSuccess_logLevel", "Something went terribly wrong"));
.isEqualTo(makeJson("ERROR", 133, "testSuccess_logLevel", "Something went terribly wrong"));
}
@Test
@@ -127,7 +143,7 @@ class GcpJsonFormatterTest {
handler.close();
String output = ostream.toString(StandardCharsets.US_ASCII);
String prefix =
makeJson("ERROR", 126, "testSuccess_withCause", "Something went terribly wrong");
makeJson("ERROR", 142, "testSuccess_withCause", "Something went terribly wrong");
// Remove the last two characters (}, \n) from the template as the actual output contains
// the full stack trace.
prefix = prefix.substring(0, prefix.length() - 2);
@@ -141,7 +157,7 @@ class GcpJsonFormatterTest {
handler.close();
String output = ostream.toString(StandardCharsets.US_ASCII);
String prefix =
makeJson("ERROR", 140, "testSuccess_withStackTrace", "Something is worth checking");
makeJson("ERROR", 156, "testSuccess_withStackTrace", "Something is worth checking");
// Remove the last three characters (}, \n) from the template as the actual output contains
// the full stack trace.
prefix = prefix.substring(0, prefix.length() - 2);
@@ -167,7 +183,7 @@ class GcpJsonFormatterTest {
TimeLimiter.create().callWithTimeout(this::logSomething, 1, TimeUnit.SECONDS);
handler.close();
String output = ostream.toString(StandardCharsets.US_ASCII);
assertThat(output).isEqualTo(makeJson("INFO", 188, "logSomething", "Something I have to say"));
assertThat(output).isEqualTo(makeJson("INFO", 204, "logSomething", "Something I have to say"));
}
@Test
@@ -176,7 +192,7 @@ class GcpJsonFormatterTest {
TimeLimiter.create().callWithTimeout(this::logSomething, 1, TimeUnit.SECONDS);
handler.close();
String output = ostream.toString(StandardCharsets.US_ASCII);
String expected = makeJson("INFO", 188, "logSomething", "Something I have to say");
String expected = makeJson("INFO", 204, "logSomething", "Something I have to say");
// Remove the last two characters (}, \n) from the template and add the trace ID.
expected =
expected.substring(0, expected.length() - 2)