Compare commits

...
Author SHA1 Message Date
Pavlo TkachandGitHub 76bd13ddf0 Remove EppProxyProtocolHandler from the pipeline (#3202) 2026-08-06 23:53:11 +00:00
Juan CelhayandGitHub 8cbf3242a7 inject proxy image into cd manifests (#3203) 2026-08-06 23:51:54 +00:00
Pavlo TkachandGitHub b1f2eb5921 Rename EPP Server container (#3201) 2026-08-05 20:11:32 +00:00
gbrodmanandGitHub 4f332d397e Add tld,domain_name idx on Domain for RDAP searches (#3200)
this allows us to quickly serve requests like "*.tld" which we need to
order to allow for cursored results

b/535250462
2026-08-05 19:55:01 +00:00
gbrodmanandGitHub c9a82f1322 Streamline large synch blocks in list/TMCH CA loading (#3183)
For reserved/premium lists:
Use double-check locking so that subsequent calls to get the entire map
of entries don't need to even check the locking object. This makes
things quicker and removes lock-tracking overhead.

For TMCH CA:
we can just remove the synchronization block entirely. Everything inside
of it is either constants (e.g. ROOT_CERTS) or a Guava loading cache
(CRL_CACHE) which takes care of synchronization for us anyway.
2026-08-04 20:06:53 +00:00
gbrodmanandGitHub fabf0c07b2 Tighten control on Marksdb URL hostname (#3198)
this doesn't really matter but eh, a URL shouldn't be able to be like,
ry.marksdb.org.attacker.com

b/535251045
2026-08-04 19:42:38 +00:00
Pavlo TkachandGitHub aa54f9ddc9 Add epp server to cloud build deploy (#3197) 2026-08-04 19:13:32 +00:00
gbrodmanandGitHub 92edbfbde2 Skip unnecessary domain reads in Spec11Pipeline (#3196)
We can just grab the fields we need from the original query. There's no
point in making a ton of extra lookups.
2026-08-04 18:45:06 +00:00
Weimin YuandGitHub b1e127798f Refactor LoadTestAction for usability (#3192)
Calculates the delay seconds automatically. This value helps ensure that
all EPP requests are enqueued before the scheduled test start time.
Since queue insertion is much slower than dispatch, this is essential to
maintain a stable QPS rate.

Also parallelizes queue insertion using a thread pool. This reduces the
delay for enqueuing the requests.

BUG=http://b/533414332
2026-08-04 18:41:54 +00:00
gbrodmanandGitHub 3474cd6e9b Batch DNS refresh requests on host renames (#3181)
Some hosts can have more than 100k domains linked to them so we probably
don't want to insert all those entries at once.
2026-08-04 02:36:50 +00:00
gbrodmanandGitHub 1fc4a281c0 Add an IncrementalMetric for sync-cache-action runs (#3195)
This is configured to run every 5 minutes. We need to make sure that the
cache doesn't get too out of date, otherwise we'll be serving stale
data. We'll add an alert that fires if SUCCESS or NOT_CONFIGURED hasn't
happened recently.
2026-08-03 20:17:12 +00:00
Juan CelhayandGitHub 9a420a69b0 Add pre and post deploy steps to Cloud Deploy delivery pipeline (#3187)
* Fix image replacement in cd (#3186)

* read sql jobs from ar

* revert release change

* flatten file path for sql jobs

* no source to sql command

* add automation to pipeline

* fix automation

* fix replica seize for backend and console in partial phases
2026-08-03 18:34:47 +00:00
Pavlo TkachandGitHub a3421f2999 Update IPs name to match reserved for epp-server (#3194) 2026-08-03 13:42:58 +00:00
30 changed files with 442 additions and 517 deletions
@@ -28,6 +28,9 @@ import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.flogger.FluentLogger;
import com.google.common.net.MediaType;
import com.google.monitoring.metrics.IncrementableMetric;
import com.google.monitoring.metrics.LabelDescriptor;
import com.google.monitoring.metrics.MetricRegistryImpl;
import google.registry.cache.SimplifiedJedisClient;
import google.registry.model.EppResource;
import google.registry.model.common.Cursor;
@@ -39,6 +42,7 @@ import google.registry.request.Action;
import google.registry.request.Response;
import google.registry.request.auth.Auth;
import google.registry.request.lock.LockHandler;
import google.registry.util.NonFinalForTesting;
import jakarta.inject.Inject;
import java.time.Duration;
import java.time.Instant;
@@ -61,6 +65,25 @@ public class SyncRemoteCacheAction implements Runnable {
private static final String LOCK_NAME = "syncRemoteCacheAction";
private static final int BATCH_SIZE = 10000;
public enum SyncStatus {
SUCCESS,
FAILURE,
NOT_CONFIGURED
}
private static final ImmutableSet<LabelDescriptor> LABEL_DESCRIPTORS =
ImmutableSet.of(
LabelDescriptor.create("status", "Whether SyncRemoteCacheAction succeeded or failed."));
@NonFinalForTesting
static final IncrementableMetric SYNC_CACHE_RUNS_METRIC =
MetricRegistryImpl.getDefault()
.newIncrementableMetric(
"/batch/sync_remote_cache/runs",
"Count of SyncRemoteCacheAction executions",
"count",
LABEL_DESCRIPTORS);
private final LockHandler lockHandler;
private final Response response;
private final Optional<SimplifiedJedisClient> jedisClient;
@@ -79,14 +102,17 @@ public class SyncRemoteCacheAction implements Runnable {
if (jedisClient.isEmpty()) {
response.setStatus(SC_NO_CONTENT);
response.setPayload("No Jedis/Valkey configuration found");
SYNC_CACHE_RUNS_METRIC.increment(SyncStatus.NOT_CONFIGURED.name());
return;
}
Callable<Void> runner =
() -> {
try {
runLocked();
SYNC_CACHE_RUNS_METRIC.increment(SyncStatus.SUCCESS.name());
response.setStatus(SC_OK);
} catch (Exception e) {
SYNC_CACHE_RUNS_METRIC.increment(SyncStatus.FAILURE.name());
logger.atSevere().withCause(e).log("Errored out during execution.");
response.setStatus(SC_INTERNAL_SERVER_ERROR);
response.setPayload(String.format("Errored out with cause: %s", e));
@@ -95,6 +121,7 @@ public class SyncRemoteCacheAction implements Runnable {
};
if (!lockHandler.executeWithLocks(runner, null, Duration.ofHours(1), LOCK_NAME)) {
SYNC_CACHE_RUNS_METRIC.increment(SyncStatus.FAILURE.name());
// Send a 200-series status code to prevent this conflicting action from retrying.
response.setStatus(SC_NO_CONTENT);
response.setPayload("Could not acquire lock; already running?");
@@ -15,7 +15,6 @@
package google.registry.beam.spec11;
import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.google.common.collect.ImmutableSet;
import dagger.Component;
@@ -25,11 +24,9 @@ import google.registry.beam.common.RegistryJpaIO;
import google.registry.beam.common.RegistryJpaIO.Read;
import google.registry.beam.spec11.SafeBrowsingTransforms.EvaluateSafeBrowsingFn;
import google.registry.config.RegistryConfig.ConfigModule;
import google.registry.model.domain.Domain;
import google.registry.model.reporting.Spec11ThreatMatch;
import google.registry.model.reporting.Spec11ThreatMatch.ThreatType;
import google.registry.persistence.PersistenceModule.TransactionIsolationLevel;
import google.registry.persistence.VKey;
import google.registry.util.Clock;
import google.registry.util.Retrier;
import google.registry.util.UtilsModule;
@@ -39,8 +36,7 @@ import java.time.LocalDate;
import java.time.YearMonth;
import org.apache.beam.sdk.Pipeline;
import org.apache.beam.sdk.PipelineResult;
import org.apache.beam.sdk.coders.KvCoder;
import org.apache.beam.sdk.coders.StringUtf8Coder;
import org.apache.beam.sdk.coders.SerializableCoder;
import org.apache.beam.sdk.io.TextIO;
import org.apache.beam.sdk.options.PipelineOptionsFactory;
import org.apache.beam.sdk.transforms.DoFn;
@@ -112,44 +108,22 @@ public class Spec11Pipeline implements Serializable {
}
static PCollection<DomainNameInfo> readFromCloudSql(Pipeline pipeline) {
Read<Object[], KV<String, String>> read =
Read<Object[], DomainNameInfo> read =
RegistryJpaIO.read(
"select d.repoId, r.emailAddress from Domain d join Registrar r on"
+ " d.currentSponsorRegistrarId = r.registrarId where r.type = 'REAL' and"
+ " d.deletionTime > CAST(now() AS timestamp)",
"""
SELECT d.domainName, d.repoId, d.currentSponsorRegistrarId, r.emailAddress FROM
Domain d JOIN Registrar r ON d.currentSponsorRegistrarId = r.registrarId WHERE
r.type = 'REAL' AND d.deletionTime > CAST(now() AS timestamp)
""",
false,
Spec11Pipeline::parseRow)
.withCoder(KvCoder.of(StringUtf8Coder.of(), StringUtf8Coder.of()));
return pipeline
.apply("Read active domains from Cloud SQL", read)
.apply(
"Build DomainNameInfo",
ParDo.of(
new DoFn<KV<String, String>, DomainNameInfo>() {
@ProcessElement
public void processElement(
@Element KV<String, String> input, OutputReceiver<DomainNameInfo> output) {
Domain domain =
tm().transact(
() -> tm().loadByKey(VKey.create(Domain.class, input.getKey())));
String emailAddress = input.getValue();
if (emailAddress == null) {
emailAddress = "";
}
DomainNameInfo domainNameInfo =
DomainNameInfo.create(
domain.getDomainName(),
domain.getRepoId(),
domain.getCurrentSponsorRegistrarId(),
emailAddress);
output.output(domainNameInfo);
}
}));
.withCoder(SerializableCoder.of(DomainNameInfo.class));
return pipeline.apply("Read active domains from Cloud SQL", read);
}
private static KV<String, String> parseRow(Object[] row) {
return KV.of((String) row[0], (String) row[1]);
private static DomainNameInfo parseRow(Object[] row) {
String emailAddress = row[3] != null ? (String) row[3] : "";
return new DomainNameInfo((String) row[0], (String) row[1], (String) row[2], emailAddress);
}
static void saveToSql(
@@ -14,14 +14,18 @@
package google.registry.dns;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static google.registry.dns.DnsUtils.requestDomainDnsRefresh;
import static google.registry.dns.RefreshDnsOnHostRenameAction.PATH;
import static google.registry.model.EppResourceUtils.getLinkedDomainKeys;
import static google.registry.model.EppResourceUtils.isDeleted;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static jakarta.servlet.http.HttpServletResponse.SC_NO_CONTENT;
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.net.MediaType;
import google.registry.model.EppResourceUtils;
import google.registry.model.domain.Domain;
import google.registry.model.host.Host;
import google.registry.persistence.VKey;
@@ -29,8 +33,11 @@ import google.registry.request.Action;
import google.registry.request.Parameter;
import google.registry.request.Response;
import google.registry.request.auth.Auth;
import google.registry.util.Clock;
import jakarta.inject.Inject;
import java.time.Instant;
import java.util.List;
import java.util.Optional;
@Action(
service = Action.Service.BACKEND,
@@ -43,45 +50,52 @@ public class RefreshDnsOnHostRenameAction implements Runnable {
public static final String PARAM_HOST_KEY = "hostKey";
public static final String PATH = "/_dr/task/refreshDnsOnHostRename";
private static final int DNS_REFRESH_BATCH_SIZE = 1000;
private final VKey<Host> hostKey;
private final Response response;
private final Clock clock;
@Inject
RefreshDnsOnHostRenameAction(@Parameter(PARAM_HOST_KEY) String hostKey, Response response) {
RefreshDnsOnHostRenameAction(
@Parameter(PARAM_HOST_KEY) String hostKey, Response response, Clock clock) {
this.hostKey = VKey.createEppVKeyFromString(hostKey);
this.response = response;
this.clock = clock;
}
@Override
public void run() {
tm().transact(
() -> {
Instant now = tm().getTxTime();
Host host = tm().loadByKeyIfPresent(hostKey).orElse(null);
boolean hostValid = true;
String failureMessage = null;
if (host == null) {
hostValid = false;
failureMessage = String.format("Host to refresh does not exist: %s", hostKey);
} else if (EppResourceUtils.isDeleted(host, now)) {
hostValid = false;
failureMessage =
String.format("Host to refresh is already deleted: %s", host.getHostName());
} else {
getLinkedDomainKeys(
host.createVKey(), host.getUpdateTimestamp().getTimestamp(), null)
.stream()
.map(domainKey -> tm().loadByKey(domainKey))
.filter(Domain::shouldPublishToDns)
.forEach(domain -> requestDomainDnsRefresh(domain.getDomainName()));
}
Optional<Host> optionalHost = tm().transact(() -> tm().loadByKeyIfPresent(hostKey));
if (optionalHost.isEmpty()) {
setFailedStatus(String.format("Host to refresh does not exist: %s", hostKey));
return;
}
Instant now = clock.now();
Host host = optionalHost.get();
if (isDeleted(host, now)) {
setFailedStatus(String.format("Host to refresh is already deleted: %s", host.getHostName()));
return;
}
ImmutableSet<VKey<Domain>> linkedDomainKeys =
getLinkedDomainKeys(hostKey, host.getUpdateTimestamp().getTimestamp(), null);
for (List<VKey<Domain>> batch : Iterables.partition(linkedDomainKeys, DNS_REFRESH_BATCH_SIZE)) {
tm().transact(
() -> {
ImmutableSet<String> domainNames =
tm().loadByKeysIfPresent(batch).values().stream()
.filter(Domain::shouldPublishToDns)
.map(Domain::getDomainName)
.collect(toImmutableSet());
requestDomainDnsRefresh(domainNames);
});
}
response.setStatus(SC_OK);
}
if (!hostValid) {
// Set the response status code to be 204 so to not retry.
response.setContentType(MediaType.PLAIN_TEXT_UTF_8);
response.setStatus(SC_NO_CONTENT);
response.setPayload(failureMessage);
}
});
private void setFailedStatus(String message) {
response.setContentType(MediaType.PLAIN_TEXT_UTF_8);
response.setStatus(SC_NO_CONTENT);
response.setPayload(message);
}
}
@@ -23,7 +23,6 @@ import dagger.multibindings.IntoSet;
import google.registry.config.RegistryConfig.Config;
import google.registry.config.RegistryConfigSettings;
import google.registry.eppserver.Protocol.FrontendProtocol;
import google.registry.eppserver.handler.EppProxyProtocolHandler;
import google.registry.eppserver.handler.EppServiceHandler;
import google.registry.eppserver.quota.QuotaManager;
import google.registry.networking.handler.SslServerInitializer;
@@ -78,14 +77,12 @@ public final class EppProtocolModule {
@Provides
@EppProtocol
static ImmutableList<Provider<? extends ChannelHandler>> provideHandlerProviders(
Provider<EppProxyProtocolHandler> proxyProtocolHandlerProvider,
@EppProtocol Provider<SslServerInitializer<NioSocketChannel>> sslServerInitializerProvider,
@EppProtocol Provider<ReadTimeoutHandler> readTimeoutHandlerProvider,
Provider<LengthFieldBasedFrameDecoder> lengthFieldBasedFrameDecoderProvider,
Provider<LengthFieldPrepender> lengthFieldPrependerProvider,
Provider<EppServiceHandler> eppServiceHandlerProvider) {
return ImmutableList.of(
proxyProtocolHandlerProvider,
sslServerInitializerProvider,
readTimeoutHandlerProvider,
lengthFieldBasedFrameDecoderProvider,
@@ -1,208 +0,0 @@
// Copyright 2017 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.eppserver.handler;
import static com.google.common.base.Preconditions.checkState;
import static java.nio.charset.StandardCharsets.US_ASCII;
import com.google.common.flogger.FluentLogger;
import com.google.common.net.InetAddresses;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.util.AttributeKey;
import jakarta.inject.Inject;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.List;
/**
* Handler that processes possible existence of a PROXY protocol v1 header.
*
* <p>When an EPP client connects to the registry (through the proxy), the registry performs two
* validations to ensure that only known registrars are allowed. First it checks the sha265 hash of
* the client SSL certificate and match it to the hash stored in the database for the registrar. It
* then checks if the connection is from an allow-listed IP address that belongs to that registrar.
*
* <p>The proxy receives client connects via the GCP load balancer, which results in the loss of
* original client IP from the channel. Luckily, the load balancer supports the PROXY protocol v1,
* which adds a header with source IP information, among other things, to the TCP request at the
* start of the connection.
*
* <p>This handler determines if a connection is proxied (PROXY protocol v1 header present) and
* correctly sets the source IP address to the channel's attribute regardless of whether it is
* proxied. After that it removes itself from the channel pipeline because the proxy header is only
* present at the beginning of the connection.
*
* <p>This handler must be the very first handler in a protocol, even before SSL handlers, because
* PROXY protocol header comes as the very first thing, even before SSL handshake request.
*
* @see <a href="https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt">The PROXY protocol</a>
*/
public class EppProxyProtocolHandler extends ByteToMessageDecoder {
/** Key used to retrieve origin IP address from a channel's attribute. */
public static final AttributeKey<String> REMOTE_ADDRESS_KEY =
AttributeKey.valueOf("REMOTE_ADDRESS_KEY");
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
// The proxy header must start with this prefix.
// Sample header: "PROXY TCP4 255.255.255.255 255.255.255.255 65535 65535\r\n".
private static final byte[] HEADER_PREFIX = "PROXY".getBytes(US_ASCII);
private boolean finished = false;
private String proxyHeader = null;
@Inject
EppProxyProtocolHandler() {}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
super.channelRead(ctx, msg);
if (finished) {
String remoteIP;
if (proxyHeader != null) {
logger.atFine().log("PROXIED CONNECTION: %s", ctx.channel());
logger.atFine().log("PROXY HEADER for channel %s: %s", ctx.channel(), proxyHeader);
String[] headerArray = proxyHeader.split(" ", -1);
if (headerArray.length == 6) {
String parsedIP = headerArray[2];
if (InetAddresses.isInetAddress(parsedIP)) {
remoteIP = parsedIP;
logger.atFine().log(
"Header parsed, using %s as remote IP for channel %s", remoteIP, ctx.channel());
} else {
logger.atWarning().log(
"Invalid IP address in PROXY header: %s, falling back to source IP for channel %s",
parsedIP, ctx.channel());
remoteIP = getSourceIP(ctx);
}
// If the header is "PROXY UNKNOWN"
// (see https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt), likely when the
// remote connection to the external load balancer is through special means, make it
// 0.0.0.0 so that it can be treated accordingly by the relevant quota configs.
} else if (headerArray.length == 2 && headerArray[1].equals("UNKNOWN")) {
logger.atFine().log(
"Header parsed, source IP unknown, using 0.0.0.0 as remote IP for channel %s",
ctx.channel());
remoteIP = "0.0.0.0";
} else {
logger.atFine().log(
"Cannot parse the header, using source IP as remote IP for channel %s",
ctx.channel());
remoteIP = getSourceIP(ctx);
}
} else {
logger.atFine().log(
"No header present, using source IP directly for channel %s", ctx.channel());
remoteIP = getSourceIP(ctx);
}
if (remoteIP != null) {
ctx.channel().attr(REMOTE_ADDRESS_KEY).set(remoteIP);
} else {
logger.atWarning().log("Not able to obtain remote IP for channel %s", ctx.channel());
}
// ByteToMessageDecoder automatically flushes unread bytes in the ByteBuf to the next handler
// when itself is being removed.
ctx.pipeline().remove(this);
}
}
private static String getSourceIP(ChannelHandlerContext ctx) {
SocketAddress remoteAddress = ctx.channel().remoteAddress();
return (remoteAddress instanceof InetSocketAddress inetSocketAddress)
? inetSocketAddress.getAddress().getHostAddress()
: null;
}
/**
* Attempts to decode an internally accumulated buffer and find the proxy protocol header.
*
* <p>When the connection is not proxied (i. e. the initial bytes are not "PROXY"), simply set
* {@link #finished} to true and allow the handler to be removed. Otherwise the handler waits
* until there's enough bytes to parse the header, save the parsed header to {@link #proxyHeader},
* and then mark {@link #finished}.
*
* @param in internally accumulated buffer, newly arrived bytes are appended to it.
* @param out objects passed to the next handler, in this case nothing is ever passed because the
* header itself is processed and written to the attribute of the proxy, and the handler is
* then removed from the pipeline.
*/
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
// Wait until there are more bytes available than the header's length before processing.
if (in.readableBytes() >= HEADER_PREFIX.length) {
if (containsHeader(in)) {
// The inbound message contains the header, it must be a proxied connection. Note that
// currently proxied connection is only used for EPP protocol, which requires the connection
// to be SSL enabled. So the beginning of the inbound message upon connection can only be
// either the proxy header (when proxied), or SSL handshake request (when not proxied),
// which does not start with "PROXY". Therefore it is safe to assume that if the beginning
// of the message contains "PROXY", it must be proxied, and must contain \r\n.
int eol = findEndOfLine(in);
// If eol is not found, that is because that we do not yet have enough inbound message, do
// nothing and wait for more bytes to be readable. eol will eventually be positive because
// of the reasoning above: The connection starts with "PROXY", so it must be a proxied
// connection and contain \r\n.
if (eol >= 0) {
// ByteBuf.readBytes is called so that the header is processed and not passed to handlers
// further in the pipeline.
byte[] headerBytes = new byte[eol];
in.readBytes(headerBytes);
proxyHeader = new String(headerBytes, US_ASCII);
// Skip \r\n.
in.skipBytes(2);
// Proxy header processed, mark finished so that this handler is removed.
finished = true;
}
} else {
// The inbound message does not contain a proxy header, mark finished so that this handler
// is removed. Note that no inbound bytes are actually processed by this handler because we
// did not call ByteBuf.readBytes(), but ByteBuf.getByte(), which does not change reader
// index of the ByteBuf. So any inbound byte is then passed to the next handler to process.
finished = true;
}
}
}
/**
* Returns the index in the buffer of the end of line found. Returns -1 if no end of line was
* found in the buffer.
*/
private static int findEndOfLine(final ByteBuf buffer) {
final int n = buffer.writerIndex();
for (int i = buffer.readerIndex(); i < n; i++) {
final byte b = buffer.getByte(i);
if (b == '\r' && i < n - 1 && buffer.getByte(i + 1) == '\n') {
return i; // \r\n
}
}
return -1; // Not found.
}
/** Checks if the given buffer contains the proxy header prefix. */
private boolean containsHeader(ByteBuf buffer) {
// The readable bytes is always more or equal to the size of the header prefix because this
// method is only called when this condition is true.
checkState(buffer.readableBytes() >= HEADER_PREFIX.length);
for (int i = 0; i < HEADER_PREFIX.length; ++i) {
if (buffer.getByte(buffer.readerIndex() + i) != HEADER_PREFIX[i]) {
return false;
}
}
return true;
}
}
@@ -14,7 +14,6 @@
package google.registry.eppserver.handler;
import static google.registry.eppserver.handler.EppProxyProtocolHandler.REMOTE_ADDRESS_KEY;
import static google.registry.networking.handler.SslServerInitializer.CLIENT_CERTIFICATE_PROMISE_KEY;
import static google.registry.util.GcpJsonFormatter.setCurrentRequest;
import static google.registry.util.GcpJsonFormatter.setCurrentTraceId;
@@ -45,6 +44,8 @@ import io.netty.util.concurrent.Promise;
import io.netty.util.concurrent.ScheduledFuture;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.security.cert.X509Certificate;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@@ -69,6 +70,9 @@ public class EppServiceHandler extends SimpleChannelInboundHandler<ByteBuf> {
public static final AttributeKey<String> CLIENT_CERTIFICATE_HASH_KEY =
AttributeKey.valueOf("CLIENT_CERTIFICATE_HASH_KEY");
public static final AttributeKey<String> REMOTE_ADDRESS_KEY =
AttributeKey.valueOf("REMOTE_ADDRESS_KEY");
private final byte[] helloBytes;
private final FrontendMetrics metrics;
private final LocalConnectionLimiter localConnectionLimiter;
@@ -128,8 +132,22 @@ public class EppServiceHandler extends SimpleChannelInboundHandler<ByteBuf> {
if (!ctx.channel().isActive()) {
return;
}
sslClientCertificateHash = getCertificateHash(cert);
clientAddress = ctx.channel().attr(REMOTE_ADDRESS_KEY).get();
if (clientAddress == null) {
SocketAddress remoteAddress = ctx.channel().remoteAddress();
if (remoteAddress instanceof InetSocketAddress inetSocketAddress) {
clientAddress = inetSocketAddress.getAddress().getHostAddress();
}
}
if (clientAddress == null || clientAddress.isEmpty()) {
logger.atSevere().log("Failed to resolve client IP address, closing connection");
closeConnection(ctx);
return;
}
sslClientCertificateHash = getCertificateHash(cert);
ctx.channel().attr(CLIENT_CERTIFICATE_HASH_KEY).set(sslClientCertificateHash);
// 1. Connection throttling (IP only pre-login)
@@ -18,6 +18,7 @@ import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.Lists.partition;
import static google.registry.util.ResourceUtils.readResourceUtf8;
import static java.util.concurrent.Executors.newFixedThreadPool;
import com.google.cloud.tasks.v2.Task;
import com.google.common.collect.ImmutableList;
@@ -39,11 +40,15 @@ import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.function.Function;
/**
* Simple load test action that can generate configurable QPSes of various EPP actions.
*
* <p>This is not an end-to-end test. It exercises the Nomulus EPP service and the database, but
* does not cover the proxy.
*
* <p>All aspects of the load test are configured via URL parameters that are specified when the
* loadtest URL is being POSTed to. The {@code clientId} and {@code tld} parameters are required.
* All of the other parameters are optional, but if none are specified then no actual load testing
@@ -60,7 +65,7 @@ public class LoadTestAction implements Runnable {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private static final int NUM_QUEUES = 10;
private static final int NUM_QUEUES = 20;
private static final int MAX_TASKS_PER_LOAD = 100;
private static final int ARBITRARY_VALID_HOST_LENGTH = 40;
private static final int MAX_DOMAIN_LABEL_LENGTH = 63;
@@ -72,20 +77,17 @@ public class LoadTestAction implements Runnable {
public static final String PATH = "/_dr/loadtest";
// Average task insertion rate with a dedicated thread enqueuing to one queue. This is used to
// calculate the EPP request dispatch time. This value is based on observation and needs not to
// be accurate. However, it should be low enough so that all EPP tasks are enqueued before the
// first task is dispatched.
private static final int TASK_INSERTIONS_PER_QUEUE_PER_MINUTE = 1000;
/** The ID of the registrar to use for load testing. */
@Inject
@Parameter("loadtestClientId")
String registrarId;
/**
* The number of seconds to delay the execution of the first load testing tasks by. Preparatory
* work of creating independent hosts that will be used for later domain creation testing occurs
* during this period, so make sure that it is long enough.
*/
@Inject
@Parameter("delaySeconds")
int delaySeconds;
/**
* The number of seconds that tasks will be enqueued for. Note that if system QPS cannot handle
* the given load then it will take longer than this number of seconds for the test to complete.
@@ -157,9 +159,25 @@ public class LoadTestAction implements Runnable {
xmlHostInfo = loadXml("host_info").replace("%host%", EXISTING_HOST);
}
private int eppTaskCount() {
// See `run()` below for details: summing two task-generating loops.
return successfulDomainCreatesPerSecond
+ runSeconds
* (successfulHostCreatesPerSecond
+ failedHostCreatesPerSecond
+ domainInfosPerSecond
+ domainChecksPerSecond
+ hostInfosPerSecond
+ successfulDomainCreatesPerSecond
+ failedDomainCreatesPerSecond);
}
@Override
public void run() {
validateAndLogRequest();
// Delay the EPP request dispatch time to account for queue-insertion time.
int delaySeconds =
Math.ceilDiv(eppTaskCount(), TASK_INSERTIONS_PER_QUEUE_PER_MINUTE * NUM_QUEUES) * 60;
validateAndLogRequest(delaySeconds);
Instant initialStartSecond = clock.now().plus(Duration.ofSeconds(delaySeconds));
ImmutableList.Builder<String> preTaskXmls = new ImmutableList.Builder<>();
ImmutableList.Builder<String> hostPrefixesBuilder = new ImmutableList.Builder<>();
@@ -209,7 +227,7 @@ public class LoadTestAction implements Runnable {
logger.atInfo().log("Added %d total load test tasks.", taskOptions.size());
}
private void validateAndLogRequest() {
private void validateAndLogRequest(int delaySeconds) {
checkArgument(
RegistryEnvironment.get() != RegistryEnvironment.PRODUCTION,
"DO NOT RUN LOADTESTS IN PROD!");
@@ -297,9 +315,21 @@ public class LoadTestAction implements Runnable {
private void enqueue(ImmutableList<Task> tasks) {
List<List<Task>> chunks = partition(tasks, MAX_TASKS_PER_LOAD);
// Farm out tasks to multiple queues to work around queue qps quotas.
for (int i = 0; i < chunks.size(); i++) {
cloudTasksUtils.enqueue("load" + (i % NUM_QUEUES), chunks.get(i));
// Farm out tasks to multiple queues to work around queue qps quotas. Use multiple threads to
// speed up the enqueuing.
try (ExecutorService executorService = newFixedThreadPool(NUM_QUEUES)) {
for (int i = 0; i < chunks.size(); i++) {
final int index = i;
// Ignore `Future` returned by the pool b/c individual failures do not affect analysis.
// lgtm[java/local-variable-is-never-read] Suppress Github CodeQL's outdated warning
var _ =
executorService.submit(
() -> cloudTasksUtils.enqueue(getQueueName(index % NUM_QUEUES), chunks.get(index)));
}
}
}
private static String getQueueName(int queueId) {
return String.format("load%d", queueId);
}
}
@@ -38,12 +38,6 @@ public final class LoadTestModule {
return extractRequiredParameter(req, "clientId");
}
@Provides
@Parameter("delaySeconds")
static int provideDelaySeconds(HttpServletRequest req) {
return extractOptionalIntParameter(req, "delaySeconds").orElse(60);
}
@Provides
@Parameter("runSeconds")
static int provideRunSeconds(HttpServletRequest req) {
@@ -74,24 +68,6 @@ public final class LoadTestModule {
return extractOptionalIntParameter(req, "domainChecks").orElse(0);
}
@Provides
@Parameter("successfulContactCreates")
static int provideSuccessfulContactCreates(HttpServletRequest req) {
return extractOptionalIntParameter(req, "successfulContactCreates").orElse(0);
}
@Provides
@Parameter("failedContactCreates")
static int provideFailedContactCreates(HttpServletRequest req) {
return extractOptionalIntParameter(req, "failedContactCreates").orElse(0);
}
@Provides
@Parameter("contactInfos")
static int provideContactInfos(HttpServletRequest req) {
return extractOptionalIntParameter(req, "contactInfos").orElse(0);
}
@Provides
@Parameter("successfulHostCreates")
static int provideSuccessfulHostCreates(HttpServletRequest req) {
@@ -59,10 +59,10 @@ public final class PremiumList extends BaseDomainLabelList<BigDecimal, PremiumEn
* Mapping from unqualified domain names to their prices.
*
* <p>This field requires special treatment since we want to lazy load it. We have to remove it
* from the immutability contract so we can modify it after construction and we have to handle the
* database processing on our own so we can detach it after load.
* from the immutability contract so we can modify it after construction, and we have to handle
* the database processing on our own so we can detach it after load.
*/
@Insignificant @Transient ImmutableMap<String, BigDecimal> labelsToPrices;
@Insignificant @Transient volatile ImmutableMap<String, BigDecimal> labelsToPrices;
@Column(nullable = false)
BloomFilter<String> bloomFilter;
@@ -76,18 +76,27 @@ public final class PremiumList extends BaseDomainLabelList<BigDecimal, PremiumEn
* Returns a {@link Map} of domain labels to prices.
*
* <p>Note that this is lazily loaded and thus must be called inside a transaction. You generally
* should not be using this anyway as it's inefficient to load all of the PremiumEntry rows if you
* should not be using this anyway as it's inefficient to load all the PremiumEntry rows if you
* don't need them. To check prices, use {@link PremiumListDao#getPremiumPrice} instead.
*
* <p>We use locking to memoize the resulting object. We cannot use a simple memoizing Supplier
* because we need to be able to set this value when creating the lists.
*/
public synchronized ImmutableMap<String, BigDecimal> getLabelsToPrices() {
public ImmutableMap<String, BigDecimal> getLabelsToPrices() {
if (labelsToPrices == null) {
labelsToPrices =
PremiumListDao.loadAllPremiumEntries(name).stream()
.collect(
toImmutableMap(
PremiumEntry::getDomainLabel,
// Set the correct amount of precision for the premium list's currency.
premiumEntry -> convertAmountToMoney(premiumEntry.getValue()).getAmount()));
synchronized (this) {
// Extra null check to avoid race conditions
if (labelsToPrices == null) {
labelsToPrices =
PremiumListDao.loadAllPremiumEntries(name).stream()
.collect(
toImmutableMap(
PremiumEntry::getDomainLabel,
// Set the correct amount of precision for the list's currency.
premiumEntry ->
convertAmountToMoney(premiumEntry.getValue()).getAmount()));
}
}
}
return labelsToPrices;
}
@@ -23,7 +23,6 @@ import static google.registry.config.RegistryConfig.getDomainLabelListCacheDurat
import static google.registry.model.tld.label.ReservationType.FULLY_BLOCKED;
import static google.registry.persistence.transaction.QueryComposer.Comparator.EQ;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.util.CollectionUtils.nullToEmpty;
import com.github.benmanes.caffeine.cache.LoadingCache;
import com.google.common.base.Splitter;
@@ -69,7 +68,7 @@ public final class ReservedList
* from the immutability contract so we can modify it after construction and we have to handle the
* database processing on our own so we can detach it after load.
*/
@Insignificant @Transient Map<String, ReservedListEntry> reservedListMap;
@Insignificant @Transient volatile ImmutableMap<String, ReservedListEntry> reservedListMap;
@RecursivePreRemove
void preRemove() {
@@ -149,7 +148,7 @@ public final class ReservedList
}
/** A builder for constructing {@link ReservedListEntry} objects, since they are immutable. */
private static class Builder
public static class Builder
extends DomainLabelEntry.Builder<ReservedListEntry, ReservedListEntry.Builder> {
Builder() {}
@@ -185,19 +184,27 @@ public final class ReservedList
*
* <p>Note that this involves a database fetch of a potentially large number of elements and
* should be avoided unless necessary.
*
* <p>We use locking to memoize the resulting object. We cannot use a simple memoizing Supplier
* because we need to be able to set this value when creating the lists.
*/
public synchronized ImmutableMap<String, ReservedListEntry> getReservedListEntries() {
public ImmutableMap<String, ReservedListEntry> getReservedListEntries() {
if (reservedListMap == null) {
reservedListMap =
tm().reTransact(
() ->
tm()
.createQueryComposer(ReservedListEntry.class)
.where("revisionId", EQ, revisionId)
.stream()
.collect(toImmutableMap(ReservedListEntry::getDomainLabel, e -> e)));
synchronized (this) {
// Extra null check to avoid race conditions
if (reservedListMap == null) {
reservedListMap =
tm().reTransact(
() ->
tm()
.createQueryComposer(ReservedListEntry.class)
.where("revisionId", EQ, revisionId)
.stream()
.collect(toImmutableMap(ReservedListEntry::getDomainLabel, e -> e)));
}
}
}
return ImmutableMap.copyOf(nullToEmpty(reservedListMap));
return reservedListMap;
}
/**
@@ -220,7 +227,7 @@ public final class ReservedList
*/
public static ImmutableSet<ReservationType> getReservationTypes(String label, String tld) {
checkNotNull(label, "label");
if (label.length() == 0) {
if (label.isEmpty()) {
return ImmutableSet.of(FULLY_BLOCKED);
}
return getReservedListEntries(label, tld).stream()
@@ -271,7 +271,7 @@ public interface TransactionManager {
* A runnable that allows for checked exceptions to be thrown.
*
* <p>This makes it easier to write lambdas without having to worry about wrapping and re-throwing
* checked excpetions as unchecked ones.
* checked exceptions as unchecked ones.
*/
@FunctionalInterface
interface ThrowingRunnable {
@@ -64,7 +64,7 @@ public final class NordnVerifyAction implements Runnable {
static final String NORDN_URL_PARAM = "nordnUrl";
static final String NORDN_LOG_ID_PARAM = "nordnLogId";
private static final String MARKSDB_URL_BEGINNING = "ry.marksdb.org";
private static final String MARKSDB_HOST_NAME = "ry.marksdb.org";
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@@ -109,11 +109,7 @@ public final class NordnVerifyAction implements Runnable {
@VisibleForTesting
LordnLog verify() throws IOException, GeneralSecurityException {
String host = Ascii.toLowerCase(url.getHost());
checkArgument(
host.startsWith(MARKSDB_URL_BEGINNING),
"URL %s must start with %s",
url,
MARKSDB_URL_BEGINNING);
checkArgument(host.equals(MARKSDB_HOST_NAME), "Host %s must equal %s", host, MARKSDB_HOST_NAME);
logger.atInfo().log("LORDN verify task %s: Sending request to URL %s", actionLogId, url);
HttpURLConnection connection = urlConnectionService.createConnection(url);
lordnRequestInitializer.initialize(connection, tld);
@@ -127,9 +127,7 @@ public final class TmchCertificateAuthority {
* @see X509Utils#verifyCertificate
*/
public void verify(X509Certificate cert) throws GeneralSecurityException {
synchronized (TmchCertificateAuthority.class) {
X509Utils.verifyCertificate(getAndValidateRoot(), getCrl(), cert, clock.now());
}
X509Utils.verifyCertificate(getAndValidateRoot(), getCrl(), cert, clock.now());
}
/**
@@ -157,33 +155,23 @@ public final class TmchCertificateAuthority {
}
public X509Certificate getAndValidateRoot() throws GeneralSecurityException {
try {
X509Certificate root = ROOT_CERTS.get(tmchCaMode);
// The current production certificate expires on 2023-07-23. Future code monkey be reminded,
// if you are looking at this code because the next line throws an exception, ask ICANN for a
// new root certificate! (preferably before the current one expires...)
root.checkValidity(Date.from(clock.now()));
return root;
} catch (Exception e) {
if (e instanceof GeneralSecurityException generalSecurityException) {
throw generalSecurityException;
} else if (e instanceof RuntimeException runtimeException) {
throw runtimeException;
}
throw new RuntimeException(e);
}
X509Certificate root = ROOT_CERTS.get(tmchCaMode);
// The current production certificate expires on 2042-11-15. Future code monkey be reminded,
// if you are looking at this code because the next line throws an exception, ask ICANN for a
// new root certificate! (preferably before the current one expires...)
root.checkValidity(Date.from(clock.now()));
return root;
}
public X509CRL getCrl() throws GeneralSecurityException {
try {
return CRL_CACHE.get(tmchCaMode);
} catch (Exception e) {
} catch (RuntimeException e) {
if (e.getCause() instanceof GeneralSecurityException generalSecurityException) {
throw generalSecurityException;
} else if (e instanceof RuntimeException runtimeException) {
throw runtimeException;
} else {
throw e;
}
throw new RuntimeException(e);
}
}
}
@@ -15,6 +15,10 @@
package google.registry.batch;
import static com.google.common.truth.Truth.assertThat;
import static com.google.monitoring.metrics.contrib.LongMetricSubject.assertThat;
import static google.registry.batch.SyncRemoteCacheAction.SyncStatus.FAILURE;
import static google.registry.batch.SyncRemoteCacheAction.SyncStatus.NOT_CONFIGURED;
import static google.registry.batch.SyncRemoteCacheAction.SyncStatus.SUCCESS;
import static google.registry.model.common.Cursor.CursorType.REMOTE_CACHE_DOMAIN_SYNC;
import static google.registry.model.common.Cursor.CursorType.REMOTE_CACHE_HOST_SYNC;
import static google.registry.testing.DatabaseHelper.createTld;
@@ -73,15 +77,24 @@ class SyncRemoteCacheActionTest {
@BeforeEach
void beforeEach() {
createTld("tld");
SyncRemoteCacheAction.SYNC_CACHE_RUNS_METRIC.reset();
action = new SyncRemoteCacheAction(lockHandler, response, Optional.of(jedisClient));
}
private static void verifyMetrics(SyncRemoteCacheAction.SyncStatus status) {
assertThat(SyncRemoteCacheAction.SYNC_CACHE_RUNS_METRIC)
.hasValueForLabels(1, status.name())
.and()
.hasNoOtherValues();
}
@Test
void test_noJedisConfig() {
action = new SyncRemoteCacheAction(lockHandler, response, Optional.empty());
action.run();
assertThat(response.getStatus()).isEqualTo(SC_NO_CONTENT);
assertThat(response.getPayload()).contains("No Jedis/Valkey configuration found");
verifyMetrics(NOT_CONFIGURED);
}
@Test
@@ -91,6 +104,7 @@ class SyncRemoteCacheActionTest {
action.run();
assertThat(response.getStatus()).isEqualTo(SC_NO_CONTENT);
assertThat(response.getPayload()).contains("Could not acquire lock");
verifyMetrics(FAILURE);
}
@Test
@@ -100,6 +114,7 @@ class SyncRemoteCacheActionTest {
action.run();
assertThat(response.getStatus()).isEqualTo(SC_INTERNAL_SERVER_ERROR);
assertThat(response.getPayload()).contains("Errored out with cause");
verifyMetrics(FAILURE);
}
@Test
@@ -109,6 +124,7 @@ class SyncRemoteCacheActionTest {
verifyNoInteractions(jedisClient);
assertThat(DatabaseHelper.loadByKeyIfPresent(Cursor.createGlobalVKey(REMOTE_CACHE_DOMAIN_SYNC)))
.isEmpty();
verifyMetrics(SUCCESS);
}
@Test
@@ -131,6 +147,7 @@ class SyncRemoteCacheActionTest {
.getCursorTime()
.toString())
.isEqualTo("2025-01-01T00:00:00.001Z");
verifyMetrics(SUCCESS);
}
@Test
@@ -146,6 +163,7 @@ class SyncRemoteCacheActionTest {
ImmutableList.of(
new SimplifiedJedisClient.JedisResource<>("active.tld", activeDomain)));
verify(jedisClient).deleteAll(Domain.class, ImmutableList.of("deleted.tld"));
verifyMetrics(SUCCESS);
}
@Test
@@ -166,6 +184,7 @@ class SyncRemoteCacheActionTest {
verify(jedisClient)
.setAll(
ImmutableList.of(new SimplifiedJedisClient.JedisResource<>("example2.tld", domain2)));
verifyMetrics(SUCCESS);
}
@Test
@@ -175,6 +194,7 @@ class SyncRemoteCacheActionTest {
verifyNoInteractions(jedisClient);
assertThat(DatabaseHelper.loadByKeyIfPresent(Cursor.createGlobalVKey(REMOTE_CACHE_HOST_SYNC)))
.isEmpty();
verifyMetrics(SUCCESS);
}
@Test
@@ -197,6 +217,7 @@ class SyncRemoteCacheActionTest {
.getCursorTime()
.toString())
.isEqualTo("2025-01-01T00:00:00.001Z");
verifyMetrics(SUCCESS);
}
@Test
@@ -212,5 +233,6 @@ class SyncRemoteCacheActionTest {
ImmutableList.of(
new SimplifiedJedisClient.JedisResource<>(active.getRepoId(), active)));
verify(jedisClient).deleteAll(Host.class, ImmutableList.of(deleted.getRepoId()));
verifyMetrics(SUCCESS);
}
}
@@ -28,6 +28,7 @@ import static jakarta.servlet.http.HttpServletResponse.SC_NO_CONTENT;
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import google.registry.model.eppcommon.StatusValue;
import google.registry.model.host.Host;
import google.registry.persistence.transaction.JpaTestExtensions;
@@ -52,7 +53,7 @@ public class RefreshDnsOnHostRenameActionTest {
private RefreshDnsOnHostRenameAction action;
private void createAction(String hostKey) {
action = new RefreshDnsOnHostRenameAction(hostKey, response);
action = new RefreshDnsOnHostRenameAction(hostKey, response, clock);
}
@BeforeEach
@@ -99,4 +100,28 @@ public class RefreshDnsOnHostRenameActionTest {
assertThat(response.getPayload())
.isEqualTo("Host to refresh is already deleted: ns1.example.tld");
}
@Test
void testSuccess_multipleBatches() {
Host host = persistActiveHost("ns1.example.tld");
ImmutableSet.Builder<String> domainNamesBuilder = new ImmutableSet.Builder<>();
for (int i = 1; i <= 1001; i++) {
String domainName = "example" + i + ".tld";
domainNamesBuilder.add(domainName);
persistResource(newDomain(domainName, host));
}
createAction(host.createVKey().stringify());
action.run();
assertDomainDnsRequests(Iterables.toArray(domainNamesBuilder.build(), String.class));
assertThat(response.getStatus()).isEqualTo(SC_OK);
}
@Test
void testSuccess_noLinkedDomains() {
Host host = persistActiveHost("ns1.example.tld");
createAction(host.createVKey().stringify());
action.run();
assertNoDnsRequests();
assertThat(response.getStatus()).isEqualTo(SC_OK);
}
}
@@ -1,101 +0,0 @@
// Copyright 2024 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.eppserver.handler;
import static com.google.common.truth.Truth.assertThat;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.embedded.EmbeddedChannel;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.charset.StandardCharsets;
import org.junit.jupiter.api.Test;
class EppProxyProtocolHandlerTest {
private EmbeddedChannel createChannel(EppProxyProtocolHandler handler) {
return new EmbeddedChannel(handler) {
@Override
public SocketAddress remoteAddress() {
return new InetSocketAddress(InetAddress.getLoopbackAddress(), 12345);
}
};
}
@Test
void testProxyProtocol_parsesValidHeader() {
EppProxyProtocolHandler handler = new EppProxyProtocolHandler();
EmbeddedChannel channel = new EmbeddedChannel(handler);
String proxyHeader = "PROXY TCP4 192.168.1.1 10.0.0.1 50000 443\r\n";
ByteBuf buffer = Unpooled.wrappedBuffer(proxyHeader.getBytes(StandardCharsets.US_ASCII));
channel.writeInbound(buffer);
String remoteAddress = channel.attr(EppProxyProtocolHandler.REMOTE_ADDRESS_KEY).get();
assertThat(remoteAddress).isEqualTo("192.168.1.1");
assertThat(channel.pipeline().get(EppProxyProtocolHandler.class)).isNull();
}
@Test
void testProxyProtocol_invalidIP_fallsBackToSource() {
EppProxyProtocolHandler handler = new EppProxyProtocolHandler();
EmbeddedChannel channel = createChannel(handler);
String proxyHeader = "PROXY TCP4 invalid_ip_address 10.0.0.1 50000 443\r\n";
ByteBuf buffer = Unpooled.wrappedBuffer(proxyHeader.getBytes(StandardCharsets.US_ASCII));
channel.writeInbound(buffer);
String remoteAddress = channel.attr(EppProxyProtocolHandler.REMOTE_ADDRESS_KEY).get();
assertThat(remoteAddress).isEqualTo("127.0.0.1"); // Falls back to mocked remoteAddress
assertThat(channel.pipeline().get(EppProxyProtocolHandler.class)).isNull();
}
@Test
void testProxyProtocol_unknownHeader() {
EppProxyProtocolHandler handler = new EppProxyProtocolHandler();
EmbeddedChannel channel = new EmbeddedChannel(handler);
String proxyHeader = "PROXY UNKNOWN\r\n";
ByteBuf buffer = Unpooled.wrappedBuffer(proxyHeader.getBytes(StandardCharsets.US_ASCII));
channel.writeInbound(buffer);
String remoteAddress = channel.attr(EppProxyProtocolHandler.REMOTE_ADDRESS_KEY).get();
assertThat(remoteAddress).isEqualTo("0.0.0.0");
assertThat(channel.pipeline().get(EppProxyProtocolHandler.class)).isNull();
}
@Test
void testProxyProtocol_noHeader_notProxied() {
EppProxyProtocolHandler handler = new EppProxyProtocolHandler();
EmbeddedChannel channel = createChannel(handler);
String normalData = "NOT_A_PROXY_HEADER";
ByteBuf buffer = Unpooled.wrappedBuffer(normalData.getBytes(StandardCharsets.US_ASCII));
channel.writeInbound(buffer);
String remoteAddress = channel.attr(EppProxyProtocolHandler.REMOTE_ADDRESS_KEY).get();
assertThat(remoteAddress).isEqualTo("127.0.0.1"); // Falls back to mocked remoteAddress
assertThat(channel.pipeline().get(EppProxyProtocolHandler.class)).isNull();
ByteBuf passedOn = channel.readInbound();
assertThat(passedOn.toString(StandardCharsets.US_ASCII)).isEqualTo("NOT_A_PROXY_HEADER");
}
}
@@ -14,10 +14,10 @@
package google.registry.eppserver.handler;
import static google.registry.eppserver.handler.EppProxyProtocolHandler.REMOTE_ADDRESS_KEY;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.eppserver.handler.EppServiceHandler.REMOTE_ADDRESS_KEY;
import static google.registry.networking.handler.SslServerInitializer.CLIENT_CERTIFICATE_PROMISE_KEY;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.argThat;
@@ -51,6 +51,8 @@ import io.netty.util.concurrent.EventExecutor;
import io.netty.util.concurrent.ImmediateEventExecutor;
import io.netty.util.concurrent.Promise;
import io.netty.util.concurrent.ScheduledFuture;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.security.cert.X509Certificate;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
@@ -382,7 +384,7 @@ class EppServiceHandlerTest {
setUpSuccessfulHandshake();
Runnable timeoutTask = timeoutTaskCaptor.getValue();
assertNotNull(timeoutTask);
assertThat(timeoutTask).isNotNull();
ChannelFuture closeFuture = mock(ChannelFuture.class);
when(ctx.close()).thenReturn(closeFuture);
@@ -392,4 +394,69 @@ class EppServiceHandlerTest {
verify(metrics).registerQuotaRejection("epp_login_timeout", "192.168.1.1");
verify(ctx).close();
}
@Test
void testFallbackToSocketIpAddress_whenRemoteAddressKeyIsNull() throws Exception {
certPromise = new DefaultPromise<>(ImmediateEventExecutor.INSTANCE);
when(channel.attr(CLIENT_CERTIFICATE_PROMISE_KEY)).thenReturn(certPromiseAttr);
when(certPromiseAttr.get()).thenReturn(certPromise);
handler.channelActive(ctx);
// Mock REMOTE_ADDRESS_KEY to return null
when(channel.attr(REMOTE_ADDRESS_KEY)).thenReturn(remoteAddressAttr);
when(remoteAddressAttr.get()).thenReturn(null);
// Mock channel.remoteAddress() to return a socket address
InetSocketAddress socketAddress = new InetSocketAddress("10.0.0.1", 12345);
when(channel.remoteAddress()).thenReturn(socketAddress);
when(channel.attr(EppServiceHandler.CLIENT_CERTIFICATE_HASH_KEY)).thenReturn(certHashAttr);
when(certificate.getEncoded()).thenReturn(new byte[] {1, 2, 3});
when(localConnectionLimiter.acquireIp("10.0.0.1")).thenReturn(true);
when(idTokenSupplier.get()).thenReturn("fake_id_token");
// Stub request handler to capture the request
ArgumentCaptor<FakeHttpServletRequest> requestCaptor =
ArgumentCaptor.forClass(FakeHttpServletRequest.class);
doAnswer(
invocation -> {
FakeHttpServletResponse rsp = invocation.getArgument(1);
rsp.getWriter().write("<epp><greeting/></epp>");
return null;
})
.when(requestHandler)
.handleRequest(requestCaptor.capture(), any(FakeHttpServletResponse.class));
certPromise.setSuccess(certificate);
// Verify it resolved IP to 10.0.0.1
verify(localConnectionLimiter).acquireIp("10.0.0.1");
FakeHttpServletRequest capturedRequest = requestCaptor.getValue();
assertThat(capturedRequest).isNotNull();
assertThat(capturedRequest.getHeader(ProxyHttpHeaders.IP_ADDRESS)).isEqualTo("10.0.0.1");
}
@Test
void testFallbackToSocketIpAddress_fails_closesConnection() throws Exception {
certPromise = new DefaultPromise<>(ImmediateEventExecutor.INSTANCE);
when(channel.attr(CLIENT_CERTIFICATE_PROMISE_KEY)).thenReturn(certPromiseAttr);
when(certPromiseAttr.get()).thenReturn(certPromise);
handler.channelActive(ctx);
// Mock REMOTE_ADDRESS_KEY to return null
when(channel.attr(REMOTE_ADDRESS_KEY)).thenReturn(remoteAddressAttr);
when(remoteAddressAttr.get()).thenReturn(null);
// Mock channel.remoteAddress() to return a non-InetSocketAddress (e.g. mock SocketAddress)
SocketAddress mockSocketAddress = mock(SocketAddress.class);
when(channel.remoteAddress()).thenReturn(mockSocketAddress);
certPromise.setSuccess(certificate);
// Verify it logged error and closed connection
verify(ctx).close();
}
}
@@ -170,9 +170,7 @@ class NordnVerifyActionTest {
void testFailure_badUrl() throws Exception {
action.url = URI.create("http://example.com/blobio").toURL();
IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, action::run);
assertThat(thrown)
.hasMessageThat()
.isEqualTo("URL http://example.com/blobio must start with ry.marksdb.org");
assertThat(thrown).hasMessageThat().isEqualTo("Host example.com must equal ry.marksdb.org");
}
@Test
@@ -257,15 +257,15 @@ td.section {
<tbody>
<tr>
<td class="property_name">generated by</td>
<td class="property_value">SchemaCrawler 17.11.1</td>
<td class="property_value">SchemaCrawler 17.12.2</td>
</tr>
<tr>
<td class="property_name">generated on</td>
<td class="property_value">2026-07-14 19:18:20</td>
<td class="property_value">2026-08-05 12:44:04</td>
</tr>
<tr>
<td class="property_name">last flyway file</td>
<td id="lastFlywayFile" class="property_value">V225__user_registry_lock_email_address_index.sql</td>
<td id="lastFlywayFile" class="property_value">V226__tld_domain_name_index.sql</td>
</tr>
</tbody>
</table>
@@ -273,7 +273,7 @@ td.section {
<p>&nbsp;</p>
<svg viewBox="0.00 0.00 4783.00 3613.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="erDiagram" style="overflow: hidden; width: 100%; height: 800px">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 3608.5)">
<title>SchemaCrawler_Diagram</title> <polygon fill="white" stroke="none" points="-4,4 -4,-3608.5 4778.75,-3608.5 4778.75,4 -4,4" /> <text xml:space="preserve" text-anchor="start" x="4535.5" y="-29.2" font-family="Helvetica,sans-Serif" font-size="14.00">generated by</text> <text xml:space="preserve" text-anchor="start" x="4618.25" y="-29.2" font-family="Helvetica,sans-Serif" font-size="14.00">SchemaCrawler 17.11.1</text> <text xml:space="preserve" text-anchor="start" x="4534.75" y="-9.45" font-family="Helvetica,sans-Serif" font-size="14.00">generated on</text> <text xml:space="preserve" text-anchor="start" x="4618.25" y="-9.45" font-family="Helvetica,sans-Serif" font-size="14.00">2026-07-14 19:18:20</text> <polygon fill="none" stroke="#888888" points="4531.75,-4 4531.75,-45.5 4766.75,-45.5 4766.75,-4 4531.75,-4" /> <!-- allocationtoken_a08ccbef -->
<title>SchemaCrawler_Diagram</title> <polygon fill="white" stroke="none" points="-4,4 -4,-3608.5 4778.75,-3608.5 4778.75,4 -4,4" /> <text xml:space="preserve" text-anchor="start" x="4535.5" y="-29.2" font-family="Helvetica,sans-Serif" font-size="14.00">generated by</text> <text xml:space="preserve" text-anchor="start" x="4618.25" y="-29.2" font-family="Helvetica,sans-Serif" font-size="14.00">SchemaCrawler 17.12.2</text> <text xml:space="preserve" text-anchor="start" x="4534.75" y="-9.45" font-family="Helvetica,sans-Serif" font-size="14.00">generated on</text> <text xml:space="preserve" text-anchor="start" x="4618.25" y="-9.45" font-family="Helvetica,sans-Serif" font-size="14.00">2026-08-05 12:44:04</text> <polygon fill="none" stroke="#888888" points="4531.75,-4 4531.75,-45.5 4766.75,-45.5 4766.75,-4 4531.75,-4" /> <!-- allocationtoken_a08ccbef -->
<g id="node1" class="node">
<title>allocationtoken_a08ccbef</title> <polygon fill="#e9c2f2" stroke="none" points="479.25,-1014.62 479.25,-1034.38 664.25,-1034.38 664.25,-1014.62 479.25,-1014.62" /> <text xml:space="preserve" text-anchor="start" x="481.25" y="-1020.08" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">public."AllocationToken"</text> <polygon fill="#e9c2f2" stroke="none" points="664.25,-1014.62 664.25,-1034.38 737.25,-1034.38 737.25,-1014.62 664.25,-1014.62" /> <text xml:space="preserve" text-anchor="start" x="698.5" y="-1019.08" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text> <text xml:space="preserve" text-anchor="start" x="481.25" y="-1000.33" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">token</text> <text xml:space="preserve" text-anchor="start" x="658.5" y="-999.33" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> <text xml:space="preserve" text-anchor="start" x="666.25" y="-999.33" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text> <text xml:space="preserve" text-anchor="start" x="481.25" y="-979.58" font-family="Helvetica,sans-Serif" font-size="14.00">domain_name</text> <text xml:space="preserve" text-anchor="start" x="658.5" y="-979.58" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> <text xml:space="preserve" text-anchor="start" x="666.25" y="-979.58" font-family="Helvetica,sans-Serif" font-size="14.00">text</text> <text xml:space="preserve" text-anchor="start" x="481.25" y="-959.83" font-family="Helvetica,sans-Serif" font-size="14.00">redemption_domain_repo_id</text> <text xml:space="preserve" text-anchor="start" x="658.5" y="-959.83" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> <text xml:space="preserve" text-anchor="start" x="666.25" y="-959.83" font-family="Helvetica,sans-Serif" font-size="14.00">text</text> <text xml:space="preserve" text-anchor="start" x="481.25" y="-940.08" font-family="Helvetica,sans-Serif" font-size="14.00">token_type</text> <text xml:space="preserve" text-anchor="start" x="658.5" y="-940.08" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> <text xml:space="preserve" text-anchor="start" x="666.25" y="-940.08" font-family="Helvetica,sans-Serif" font-size="14.00">text</text> <polygon fill="none" stroke="#888888" points="478.25,-934.62 478.25,-1035.38 738.25,-1035.38 738.25,-934.62 478.25,-934.62" />
</g>
File diff suppressed because one or more lines are too long
+1
View File
@@ -223,3 +223,4 @@ V222__remove_contact.sql
V223__tld_change_xap_enabled_to_transitions.sql
V224__add_registrar_expiry_access_period_enabled.sql
V225__user_registry_lock_email_address_index.sql
V226__tld_domain_name_index.sql
@@ -0,0 +1,17 @@
-- Copyright 2026 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.
-- RDAP queries by TLD need to order results. Using a btree that combines
-- TLD and name allows us to do this quickly
CREATE INDEX CONCURRENTLY IF NOT EXISTS domain_tld_domain_name_idx ON "Domain" (tld, domain_name);
@@ -1841,6 +1841,13 @@ CREATE INDEX domain_history_to_transaction_record_idx ON public."DomainTransacti
CREATE UNIQUE INDEX domain_no_duplicate_active ON public."Domain" USING btree (domain_name) WHERE (deletion_time = '294247-01-10 04:00:54.775+00'::timestamp with time zone);
--
-- Name: domain_tld_domain_name_idx; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX domain_tld_domain_name_idx ON public."Domain" USING btree (tld, domain_name);
--
-- Name: domaindsdatahistory_domain_history_revision_id_hash; Type: INDEX; Schema: public; Owner: -
--
+4 -3
View File
@@ -12,12 +12,13 @@ spec:
metadata:
labels:
service: epp-server
traffic: epp-all
spec:
serviceAccountName: nomulus
nodeSelector:
cloud.google.com/machine-family: c4
containers:
- name: frontend
- name: epp-server
image: gcr.io/GCP_PROJECT/nomulus
ports:
- containerPort: 8080
@@ -61,7 +62,7 @@ spec:
fieldRef:
fieldPath: metadata.namespace
- name: CONTAINER_NAME
value: frontend
value: epp-server
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
@@ -98,7 +99,7 @@ spec:
ipFamilies: [IPv4, IPv6]
ipFamilyPolicy: RequireDualStack
selector:
service: epp-server
traffic: epp-all
ports:
- port: 700
targetPort: epp
+1
View File
@@ -84,6 +84,7 @@ steps:
sed -i "s|artifactStorage: artifactStorage|artifactStorage: $artifact_storage|" "$target_file"
sed -i "s|serviceAccount: serviceAccount|serviceAccount: $service_account|" "$target_file"
sed -i "s|serviceAccount: serviceAccount|serviceAccount: $service_account|" release/clouddeploy/delivery-pipeline.yaml
sed -i "s|cluster: cluster|cluster: $cluster_val|" "$target_file"
sed -i "s|workerPool: workerPool|workerPool: $worker_pool|" "$target_file"
fi
+4 -2
View File
@@ -76,6 +76,7 @@ steps:
echo "$nomulus_digest" > /workspace/nomulus_digest
proxy_digest=$(gcloud container images list-tags gcr.io/${PROJECT_ID}/proxy \
--format="get(digest)" --filter="tags = ${TAG_NAME}")
echo "$proxy_digest" > /workspace/proxy_digest
gcloud --project=${PROJECT_ID} beta container binauthz attestations \
sign-and-create --artifact-url=gcr.io/${PROJECT_ID}/nomulus@$nomulus_digest \
--attestor=build-attestor --attestor-project=${PROJECT_ID} \
@@ -196,14 +197,15 @@ steps:
echo "============================================="
# Read the pre-fetched image digest from the workspace file
nomulus_digest=$(cat /workspace/nomulus_digest)
proxy_digest=$(cat /workspace/proxy_digest)
gcloud deploy releases create "$release_name" \
--delivery-pipeline="$pipeline" \
--region="$region" \
--project=${PROJECT_ID} \
--images="gcr.io/${PROJECT_ID}/nomulus=gcr.io/${PROJECT_ID}/nomulus@${nomulus_digest}" \
--images="gcr.io/${PROJECT_ID}/nomulus=gcr.io/${PROJECT_ID}/nomulus@${nomulus_digest},gcr.io/${PROJECT_ID}/proxy=gcr.io/${PROJECT_ID}/proxy@${proxy_digest}" \
--source=. \
--skaffold-file=release/clouddeploy/skaffold.yaml \
--deploy-parameters="deployed_image=gcr.io/${PROJECT_ID}/nomulus@${nomulus_digest},base_image=us-docker.pkg.dev/${PROJECT_ID}/gcr.io/nomulus"
--deploy-parameters="deployed_image=gcr.io/${PROJECT_ID}/nomulus@${nomulus_digest},base_image=us-docker.pkg.dev/${PROJECT_ID}/gcr.io/nomulus,tag_name=${TAG_NAME},project_id=${PROJECT_ID}"
# The tarballs and jars to upload to GCS.
artifacts:
objects:
+19 -2
View File
@@ -236,11 +236,17 @@ steps:
# partial phase manifests
for stage in 1 5
do
if [ ${service} == backend ] || [ ${service} == console ]
then
replicas=1
else
replicas=${stage}
fi
awk 'NR==1,/^---$/ {if ($0 != "---") print}' ./jetty/kubernetes/nomulus-${env}-${service}.yaml | \
sed s/name:\ ${service}/name:\ ${service}-partial-phase/g | \
sed s/service:\ ${service}/deployment:\ ${service}-partial-phase/g | \
sed s/value:\ ${service}/value:\ ${service}-partial-phase/g | \
sed "/^spec:$/a\ replicas: ${stage}" \
sed "/^spec:$/a\ replicas: ${replicas}" \
> ./jetty/kubernetes/nomulus-${env}-${service}-partial-phase-${stage}.yaml
done
# gateway
@@ -254,9 +260,20 @@ steps:
> ./jetty/kubernetes/gateway/nomulus-backend-policy-${env}-${service}-canary.yaml
done
# Generate manifests for epp-server (which doesn't use HTTP gateway routing)
# Lowercase EPP-v2 to epp-v2 to match reserved static GCE IP names.
sed s/GCP_PROJECT/${PROJECT_ID}/g ./jetty/kubernetes/nomulus-epp-server.yaml | \
sed s/latest/${TAG_NAME}/g | \
sed s/ENVIRONMENT/${env}/g > ./jetty/kubernetes/nomulus-${env}-epp-server.yaml
sed s/ENVIRONMENT/${env}/g | \
sed s/EPP-v2/epp-v2/g > ./jetty/kubernetes/nomulus-${env}-epp-server.yaml
# Generate partial-phase manifests for epp-server for Cloud Deploy canary stages
for stage in 1 5
do
awk 'NR==1,/^---$/ {if ($0 != "---") print}' ./jetty/kubernetes/nomulus-${env}-epp-server.yaml | \
sed s/name:\ epp-server/name:\ epp-server-partial-phase/g | \
sed s/service:\ epp-server/deployment:\ epp-server-partial-phase/g | \
sed "/^spec:$/a\ replicas: ${stage}" \
> ./jetty/kubernetes/nomulus-${env}-epp-server-partial-phase-${stage}.yaml
done
done
# Upload the Gradle binary to GCS if it does not exist and point URL in Gradle wrapper to it.
- name: 'gcr.io/cloud-builders/gcloud'
+1 -1
View File
@@ -5,7 +5,7 @@ This directory contains the Google Cloud Deploy configuration files for the Nomu
## Files
### `delivery-pipeline.yaml`
Defines the `DeliveryPipeline` resource named `deploy-nomulus`. It sets up the serial pipeline for rolling out changes to different targets.
Defines the `DeliveryPipeline` resource named `deploy-nomulus` and its associated `Automation` resource (`deploy-nomulus/auto-advance-canary`). It sets up the serial pipeline for rolling out changes to different targets and automatically advancing canary rollouts.
### Target Configurations (e.g., `crash-target.yaml`)
Files matching this format define the `Target` resources for Cloud Deploy. They specify the GKE cluster and other environment-specific settings for deployment.
+35 -18
View File
@@ -14,17 +14,6 @@ serialPipeline:
- phaseId: "canary-1"
profiles: ["crash-partial-phase-1"]
percentage: 10
predeploy:
tasks:
- type: container
image: gcr.io/google.com/cloudsdktool/google-cloud-cli:stable
env:
TARGET_ID: ${{ target.id }}
command: ["/bin/bash"]
args:
- "-c"
- |
gcloud builds submit --config=release/cloudbuild-schema-verify-${TARGET_ID}.yaml
analysis:
# 10 minutes.
duration: 600s
@@ -55,17 +44,20 @@ serialPipeline:
args:
- "-c"
- |
gcloud artifacts docker tags add $DEPLOYED_IMAGE \
${BASE_IMAGE}:live-cd-${TARGET_ID}
gcloud container images add-tag $DEPLOYED_IMAGE \
${BASE_IMAGE}:live-cd-${TARGET_ID} --quiet
- type: container
image: gcr.io/google.com/cloudsdktool/google-cloud-cli:stable
env:
TARGET_ID: ${{ target.id }}
TAG_NAME: ${{ deploy_params['tag_name'] }}
PROJECT_ID: ${{ deploy_params['project_id'] }}
command: ["/bin/bash"]
args:
- "-c"
- |
gcloud builds submit --config=release/cloudbuild-schema-deploy-${TARGET_ID}.yaml
gcloud storage cp gs://${PROJECT_ID}-deploy/${TAG_NAME}/cloudbuild-schema-deploy-${TARGET_ID}.yaml .
gcloud builds submit --no-source --config=cloudbuild-schema-deploy-${TARGET_ID}.yaml
analysis:
# 10 minutes.
duration: 600s
@@ -86,11 +78,14 @@ serialPipeline:
image: gcr.io/google.com/cloudsdktool/google-cloud-cli:stable
env:
TARGET_ID: ${{ target.id }}
TAG_NAME: ${{ deploy_params['tag_name'] }}
PROJECT_ID: ${{ deploy_params['project_id'] }}
command: ["/bin/bash"]
args:
- "-c"
- |
gcloud builds submit --config=release/cloudbuild-schema-verify-${TARGET_ID}.yaml
gcloud storage cp gs://${PROJECT_ID}-deploy/${TAG_NAME}/cloudbuild-schema-verify-${TARGET_ID}.yaml .
gcloud builds submit --no-source --config=cloudbuild-schema-verify-${TARGET_ID}.yaml
analysis:
# 10 minutes.
duration: 600s
@@ -121,20 +116,42 @@ serialPipeline:
args:
- "-c"
- |
gcloud artifacts docker tags add $DEPLOYED_IMAGE \
${BASE_IMAGE}:live-cd-${TARGET_ID}
gcloud container images add-tag $DEPLOYED_IMAGE \
${BASE_IMAGE}:live-cd-${TARGET_ID} --quiet
- type: container
image: gcr.io/google.com/cloudsdktool/google-cloud-cli:stable
env:
TARGET_ID: ${{ target.id }}
TAG_NAME: ${{ deploy_params['tag_name'] }}
PROJECT_ID: ${{ deploy_params['project_id'] }}
command: ["/bin/bash"]
args:
- "-c"
- |
gcloud builds submit --config=release/cloudbuild-schema-deploy-${TARGET_ID}.yaml
gcloud storage cp gs://${PROJECT_ID}-deploy/${TAG_NAME}/cloudbuild-schema-deploy-${TARGET_ID}.yaml .
gcloud builds submit --no-source --config=cloudbuild-schema-deploy-${TARGET_ID}.yaml
analysis:
# 10 minutes.
duration: 600s
googleCloud:
alertPolicyChecks:
sandboxStableDeploymentAlertPolicyChecks
---
apiVersion: deploy.cloud.google.com/v1
kind: Automation
metadata:
name: deploy-nomulus/auto-advance-canary
description: Automatically advances rollouts through canary-1 phase after successful deployment and analysis.
# Placeholder: Replace with project service account.
serviceAccount: serviceAccount
selector:
targets:
- id: crash
- id: sandbox
rules:
- advanceRolloutRule:
id: advance-canary-phases
sourcePhases:
- "canary-1"
wait: 0m
+6
View File
@@ -11,6 +11,7 @@ profiles:
- ../../jetty/kubernetes/nomulus-crash-console.yaml
- ../../jetty/kubernetes/nomulus-crash-frontend.yaml
- ../../jetty/kubernetes/nomulus-crash-pubapi.yaml
- ../../jetty/kubernetes/nomulus-crash-epp-server.yaml
deploy:
kubectl: { }
- name: crash-partial-phase-1
@@ -20,6 +21,7 @@ profiles:
- ../../jetty/kubernetes/nomulus-crash-console-partial-phase-1.yaml
- ../../jetty/kubernetes/nomulus-crash-frontend-partial-phase-1.yaml
- ../../jetty/kubernetes/nomulus-crash-pubapi-partial-phase-1.yaml
- ../../jetty/kubernetes/nomulus-crash-epp-server-partial-phase-1.yaml
deploy:
kubectl: { }
- name: crash-partial-phase-5
@@ -29,6 +31,7 @@ profiles:
- ../../jetty/kubernetes/nomulus-crash-console-partial-phase-5.yaml
- ../../jetty/kubernetes/nomulus-crash-frontend-partial-phase-5.yaml
- ../../jetty/kubernetes/nomulus-crash-pubapi-partial-phase-5.yaml
- ../../jetty/kubernetes/nomulus-crash-epp-server-partial-phase-5.yaml
deploy:
kubectl: { }
- name: sandbox
@@ -38,6 +41,7 @@ profiles:
- ../../jetty/kubernetes/nomulus-sandbox-console.yaml
- ../../jetty/kubernetes/nomulus-sandbox-frontend.yaml
- ../../jetty/kubernetes/nomulus-sandbox-pubapi.yaml
- ../../jetty/kubernetes/nomulus-sandbox-epp-server.yaml
deploy:
kubectl: { }
- name: sandbox-partial-phase-1
@@ -47,6 +51,7 @@ profiles:
- ../../jetty/kubernetes/nomulus-sandbox-console-partial-phase-1.yaml
- ../../jetty/kubernetes/nomulus-sandbox-frontend-partial-phase-1.yaml
- ../../jetty/kubernetes/nomulus-sandbox-pubapi-partial-phase-1.yaml
- ../../jetty/kubernetes/nomulus-sandbox-epp-server-partial-phase-1.yaml
deploy:
kubectl: { }
- name: sandbox-partial-phase-5
@@ -56,5 +61,6 @@ profiles:
- ../../jetty/kubernetes/nomulus-sandbox-console-partial-phase-5.yaml
- ../../jetty/kubernetes/nomulus-sandbox-frontend-partial-phase-5.yaml
- ../../jetty/kubernetes/nomulus-sandbox-pubapi-partial-phase-5.yaml
- ../../jetty/kubernetes/nomulus-sandbox-epp-server-partial-phase-5.yaml
deploy:
kubectl: { }