1
0
mirror of https://github.com/google/nomulus synced 2026-02-11 07:11:40 +00:00

Remove more references to GAE (#2894)

These are old/pointless now that we've migrated to GKE. Note that this
doesn't update anything in the docs/ folder, as that's a much larger
project that should be done on its own.
This commit is contained in:
gbrodman
2025-12-01 11:43:49 -05:00
committed by GitHub
parent 0dc7ab99d7
commit 7149fd3307
72 changed files with 125 additions and 2537 deletions

View File

@@ -84,10 +84,10 @@ tasks.build.dependsOn(tasks.checkLicense)
// Paths to main and test sources.
ext.projectRootDir = "${rootDir}"
// Tasks to deploy/stage all App Engine services
// Tasks to deploy/stage all services
task deploy {
group = 'deployment'
description = 'Deploys all services to App Engine.'
description = 'Deploys all services.'
}
task stage {

View File

@@ -33,8 +33,8 @@ public abstract class DateTimeUtils {
/**
* A date in the far future that we can treat as infinity.
*
* <p>This value is (2^63-1)/1000 rounded down. AppEngine stores dates as 64 bit microseconds, but
* Java uses milliseconds, so this is the largest representable date that will survive a
* <p>This value is (2^63-1)/1000 rounded down. Postgres can store dates as 64 bit microseconds,
* but Java uses milliseconds, so this is the largest representable date that will survive a
* round-trip through the database.
*/
public static final DateTime END_OF_TIME = new DateTime(Long.MAX_VALUE / 1000, DateTimeZone.UTC);

View File

@@ -104,7 +104,7 @@ PROPERTIES = [
Property('testFilter',
'Comma separated list of test patterns, if specified run only '
'these.'),
Property('environment', 'GAE Environment for deployment and staging.'),
Property('environment', 'Environment for deployment and staging.'),
# Cloud SQL properties
Property('dbServer',

View File

@@ -9,7 +9,7 @@ expected to change.
## Deployment
Webapp is deployed with the nomulus default service war to Google App Engine.
The webapp is deployed with the nomulus default service war to GKE.
During nomulus default service war build task, gradle script triggers the
following:

View File

@@ -110,7 +110,7 @@ configurations {
// for details.
nomulus_test
// Exclude non-canonical servlet-api jars. Our AppEngine deployment uses
// Exclude non-canonical servlet-api jars. Our deployment uses
// javax.servlet:servlet-api:2.5
// For reasons we do not understand, marking the following dependencies as
// compileOnly instead of compile does not exclude them from runtimeClasspath.

View File

@@ -55,8 +55,6 @@ import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Optional;
import java.util.Random;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Supplier;
import org.joda.time.Duration;
@@ -118,19 +116,13 @@ public class CloudTasksUtils implements Serializable {
* <p>For GET requests we add them on to the URL, and for POST requests we add them in the body of
* the request.
*
* <p>The parameters {@code putHeadersFunction} and {@code setBodyFunction} are used so that this
* method can be called with either an AppEngine HTTP request or a standard non-AppEngine HTTP
* request. The two objects do not have the same methods, but both have ways of setting headers /
* body.
*
* @return the resulting path (unchanged for POST requests, with params added for GET requests)
*/
private static String processRequestParameters(
String path,
Method method,
Multimap<String, String> params,
BiConsumer<String, String> putHeadersFunction,
Consumer<ByteString> setBodyFunction) {
HttpRequest.Builder requestBuilder) {
if (CollectionUtils.isNullOrEmpty(params)) {
return path;
}
@@ -148,8 +140,8 @@ public class CloudTasksUtils implements Serializable {
if (method.equals(Method.GET)) {
return String.format("%s?%s", path, encodedParams);
}
putHeadersFunction.accept(HttpHeaders.CONTENT_TYPE, MediaType.FORM_DATA.toString());
setBodyFunction.accept(ByteString.copyFrom(encodedParams, StandardCharsets.UTF_8));
requestBuilder.putHeaders(HttpHeaders.CONTENT_TYPE, MediaType.FORM_DATA.toString());
requestBuilder.setBody(ByteString.copyFrom(encodedParams, StandardCharsets.UTF_8));
return path;
}
@@ -160,18 +152,17 @@ public class CloudTasksUtils implements Serializable {
* default service account as the principal. That account must have permission to submit tasks to
* Cloud Tasks.
*
* <p>The caller of this method is responsible for passing in the appropriate service based on the
* runtime (GAE/GKE). Use the overload that takes an action class if possible.
* <p>The caller of this method is responsible for passing in the appropriate service. Use the
* overload that takes an action class if possible.
*
* @param path the relative URI (staring with a slash and ending without one).
* @param method the HTTP method to be used for the request.
* @param service the GAE/GKE service to route the request to.
* @param service the service to route the request to.
* @param params a multimap of URL query parameters. Duplicate keys are saved as is, and it is up
* to the server to process the duplicate keys.
* @return the enqueued task.
* @see <a
* href=ttps://cloud.google.com/appengine/docs/standard/java/taskqueue/push/creating-tasks#target>Specifyinig
* the worker service</a>
* @see <a href=https://docs.cloud.google.com/tasks/docs/creating-http-target-tasks#java>Creating
* HTTP target tasks</a>
*/
protected Task createTask(
String path, Method method, Action.Service service, Multimap<String, String> params) {
@@ -180,9 +171,7 @@ public class CloudTasksUtils implements Serializable {
"The path must start with a '/'.");
HttpRequest.Builder requestBuilder =
HttpRequest.newBuilder().setHttpMethod(HttpMethod.valueOf(method.name()));
path =
processRequestParameters(
path, method, params, requestBuilder::putHeaders, requestBuilder::setBody);
path = processRequestParameters(path, method, params, requestBuilder);
OidcToken.Builder oidcTokenBuilder =
OidcToken.newBuilder()
.setServiceAccountEmail(credential.serviceAccount())
@@ -204,16 +193,15 @@ public class CloudTasksUtils implements Serializable {
* Cloud Tasks.
*
* <p>Prefer this overload over the one where the path and service are explicitly defined, as this
* class will automatically determine the service to use based on the action and the runtime.
* class will automatically determine the service to use based on the action.
*
* @param actionClazz the action class to run, must be annotated with {@link Action}.
* @param method the HTTP method to be used for the request.
* @param params a multimap of URL query parameters. Duplicate keys are saved as is, and it is up
* to the server to process the duplicate keys.
* @return the enqueued task.
* @see <a
* href=ttps://cloud.google.com/appengine/docs/standard/java/taskqueue/push/creating-tasks#target>Specifyinig
* the worker service</a>
* @see <a href=https://docs.cloud.google.com/tasks/docs/creating-http-target-tasks#java>Creating
* HTTP target tasks</a>
*/
public Task createTask(
Class<? extends Runnable> actionClazz, Method method, Multimap<String, String> params) {
@@ -236,19 +224,18 @@ public class CloudTasksUtils implements Serializable {
/**
* Create a {@link Task} to be enqueued with a random delay up to {@code jitterSeconds}.
*
* <p>The caller of this method is responsible for passing in the appropriate service based on the
* runtime (GAE/GKE). Use the overload that takes an action class if possible.
* <p>The caller of this method is responsible for passing in the appropriate service. Use the
* overload that takes an action class if possible.
*
* @param path the relative URI (staring with a slash and ending without one).
* @param method the HTTP method to be used for the request.
* @param service the GAE/GKE service to route the request to.
* @param service the service to route the request to.
* @param params a multimap of URL query parameters. Duplicate keys are saved as is, and it is up
* to the server to process the duplicate keys.
* @param jitterSeconds the number of seconds that a task is randomly delayed up to.
* @return the enqueued task.
* @see <a
* href=ttps://cloud.google.com/appengine/docs/standard/java/taskqueue/push/creating-tasks#target>Specifyinig
* the worker service</a>
* @see <a href=https://docs.cloud.google.com/tasks/docs/creating-http-target-tasks#java>Creating
* HTTP target tasks</a>
*/
public Task createTaskWithJitter(
String path,
@@ -271,7 +258,7 @@ public class CloudTasksUtils implements Serializable {
* Create a {@link Task} to be enqueued with a random delay up to {@code jitterSeconds}.
*
* <p>Prefer this overload over the one where the path and service are explicitly defined, as this
* class will automatically determine the service to use based on the action and the runtime.
* class will automatically determine the service to use based on the action.
*
* @param actionClazz the action class to run, must be annotated with {@link Action}.
* @param method the HTTP method to be used for the request.
@@ -279,9 +266,8 @@ public class CloudTasksUtils implements Serializable {
* to the server to process the duplicate keys.
* @param jitterSeconds the number of seconds that a task is randomly delayed up to.
* @return the enqueued task.
* @see <a
* href=ttps://cloud.google.com/appengine/docs/standard/java/taskqueue/push/creating-tasks#target>Specifyinig
* the worker service</a>
* @see <a href=https://docs.cloud.google.com/tasks/docs/creating-http-target-tasks#java>Creating
* HTTP target tasks</a>
*/
public Task createTaskWithJitter(
Class<? extends Runnable> actionClazz,
@@ -302,14 +288,13 @@ public class CloudTasksUtils implements Serializable {
*
* @param path the relative URI (staring with a slash and ending without one).
* @param method the HTTP method to be used for the request.
* @param service the GAE/GKE service to route the request to.
* @param service the service to route the request to.
* @param params a multimap of URL query parameters. Duplicate keys are saved as is, and it is up
* to the server to process the duplicate keys.
* @param delay the amount of time that a task needs to be delayed for.
* @return the enqueued task.
* @see <a
* href=ttps://cloud.google.com/appengine/docs/standard/java/taskqueue/push/creating-tasks#target>Specifyinig
* the worker service</a>
* @see <a href=https://docs.cloud.google.com/tasks/docs/creating-http-target-tasks#java>Creating
* HTTP target tasks</a>
*/
private Task createTaskWithDelay(
String path,
@@ -330,7 +315,7 @@ public class CloudTasksUtils implements Serializable {
* Create a {@link Task} to be enqueued with delay of {@code duration}.
*
* <p>Prefer this overload over the one where the path and service are explicitly defined, as this
* class will automatically determine the service to use based on the action and the runtime.
* class will automatically determine the service to use based on the action.
*
* @param actionClazz the action class to run, must be annotated with {@link Action}.
* @param method the HTTP method to be used for the request.
@@ -338,9 +323,8 @@ public class CloudTasksUtils implements Serializable {
* to the server to process the duplicate keys.
* @param delay the amount of time that a task needs to be delayed for.
* @return the enqueued task.
* @see <a
* href=ttps://cloud.google.com/appengine/docs/standard/java/taskqueue/push/creating-tasks#target>Specifyinig
* the worker service</a>
* @see <a href=https://docs.cloud.google.com/tasks/docs/creating-http-target-tasks#java>Creating
* HTTP target tasks</a>
*/
public Task createTaskWithDelay(
Class<? extends Runnable> actionClazz,

View File

@@ -112,11 +112,11 @@ public class RelockDomainAction implements Runnable {
public void run() {
/* We wish to manually control our retry behavior, in order to limit the number of retries
* and/or notify registrars / support only after a certain number of retries, or only
* with a certain type of failure. AppEngine will automatically retry on any non-2xx status
* with a certain type of failure. Cloud Tasks will automatically retry on any non-2xx status
* code, so return SC_NO_CONTENT (204) by default to avoid this auto-retry.
*
* See https://cloud.google.com/appengine/docs/standard/java/taskqueue/push/retrying-tasks
* for more details on retry behavior. */
* See https://docs.cloud.google.com/tasks/docs/configuring-queues#retry for more details on
* retry behavior. */
response.setStatus(SC_NO_CONTENT);
response.setContentType(MediaType.PLAIN_TEXT_UTF_8);
tm().transact(this::relockDomain);

View File

@@ -40,8 +40,6 @@ public class RegistryPipelineWorkerInitializer implements JvmInitializer {
@Override
public void beforeProcessing(PipelineOptions options) {
// TODO(b/416299900): remove next line after GAE is removed.
System.setProperty("google.registry.jetty", "true");
RegistryPipelineOptions registryOptions = options.as(RegistryPipelineOptions.class);
RegistryEnvironment environment = registryOptions.getRegistryEnvironment();
if (environment == null || environment.equals(RegistryEnvironment.UNITTEST)) {

View File

@@ -279,20 +279,6 @@ public class BigqueryConnection implements AutoCloseable {
private TableReference getTableReference() {
return table.getTableReference().clone();
}
/** Returns a string representation of the TableReference for the wrapped table. */
public String getStringReference() {
return tableReferenceToString(table.getTableReference());
}
/** Returns a string representation of the given TableReference. */
private static String tableReferenceToString(TableReference tableRef) {
return String.format(
"%s:%s.%s",
tableRef.getProjectId(),
tableRef.getDatasetId(),
tableRef.getTableId());
}
}
/**
@@ -398,29 +384,12 @@ public class BigqueryConnection implements AutoCloseable {
}
/**
* Starts an asynchronous query job to dump the results of the specified query into a local
* ImmutableTable object, row-keyed by the row number (indexed from 1), column-keyed by the
* TableFieldSchema for that column, and with the value object as the cell value. Note that null
* values will not actually be null, but they can be checked for using Data.isNull().
* Dumps the results of the specified query into a local ImmutableTable object, row-keyed by the
* row number (indexed from 1), column-keyed by the TableFieldSchema for that column, and with the
* value object as the cell value.
*
* <p>Returns a ListenableFuture that holds the ImmutableTable on success.
*/
public ListenableFuture<ImmutableTable<Integer, TableFieldSchema, Object>>
queryToLocalTable(String querySql) {
Job job = new Job()
.setConfiguration(new JobConfiguration()
.setQuery(new JobConfigurationQuery()
.setQuery(querySql)
.setDefaultDataset(getDataset())));
return transform(runJobToCompletion(job), this::getQueryResults, directExecutor());
}
/**
* Returns the result of calling queryToLocalTable, but synchronously to avoid spawning new
* background threads, which App Engine doesn't support.
*
* @see <a href="https://cloud.google.com/appengine/docs/standard/java/runtime#Threads">App Engine
* Runtime</a>
* <p>Note that null values will not actually be null, but they can be checked for using
* Data.isNull()
*/
public ImmutableTable<Integer, TableFieldSchema, Object> queryToLocalTableSync(String querySql) {
Job job = new Job()
@@ -634,10 +603,6 @@ public class BigqueryConnection implements AutoCloseable {
});
}
private ListenableFuture<Job> runJobToCompletion(final Job job) {
return service.submit(() -> runJob(job, null));
}
/** Helper that returns true if a dataset with this name exists. */
public boolean checkDatasetExists(String datasetName) throws IOException {
try {
@@ -676,14 +641,6 @@ public class BigqueryConnection implements AutoCloseable {
.setDatasetId(getDatasetId());
}
/** Returns table reference with the projectId and datasetId filled out for you. */
public TableReference getTable(String tableName) {
return new TableReference()
.setProjectId(getProjectId())
.setDatasetId(getDatasetId())
.setTableId(tableName);
}
/**
* Helper that creates a dataset with this name if it doesn't already exist, and returns true if
* creation took place.

View File

@@ -71,9 +71,7 @@ class BsaDiffCreator {
Optional<String> previousJobName = schedule.latestCompleted().map(CompletedJob::jobName);
/*
* Memory usage is a concern when creating a diff, when the newest download needs to be held in
* memory in its entirety. The top-grade AppEngine VM has 3GB of memory, leaving less than 1.5GB
* to application memory footprint after subtracting overheads due to copying garbage collection
* and non-heap data etc. Assuming 400K labels, each of which on average included in 5 orders,
* memory in its entirety. Assuming 400K labels, each of which on average included in 5 orders,
* the memory footprint is at least 300MB when loaded into a Hashset-backed Multimap (64-bit
* JVM, with 12-byte object header, 16-byte array header, and 16-byte alignment).
*

View File

@@ -44,8 +44,6 @@ public abstract class CredentialModule {
* <p>The credential returned by the Cloud Runtime depends on the runtime environment:
*
* <ul>
* <li>On App Engine, returns a scope-less {@code ComputeEngineCredentials} for
* PROJECT_ID@appspot.gserviceaccount.com
* <li>On Compute Engine, returns a scope-less {@code ComputeEngineCredentials} for
* PROJECT_NUMBER-compute@developer.gserviceaccount.com
* <li>On end user host, this returns the credential downloaded by gcloud. Please refer to <a
@@ -87,8 +85,8 @@ public abstract class CredentialModule {
* the application default credential user.
*
* <p>The Workspace domain must grant delegated admin access to the default service account user
* (project-id@appspot.gserviceaccount.com on AppEngine) with all scopes in {@code defaultScopes}
* and {@code delegationScopes}.
* (nomulus-service-account@{project-id}.iam.gserviceaccount.com on GCP) with all scopes in {@code
* defaultScopes} and {@code delegationScopes}.
*/
@AdcDelegatedCredential
@Provides
@@ -113,9 +111,9 @@ public abstract class CredentialModule {
* Provides a {@link GoogleCredentialsBundle} for sending emails through Google Workspace.
*
* <p>The Workspace domain must grant delegated admin access to the default service account user
* (project-id@appspot.gserviceaccount.com on AppEngine) with all scopes in {@code defaultScopes}
* and {@code delegationScopes}. In addition, the user {@code gSuiteOutgoingEmailAddress} must
* have the permission to send emails.
* (nomulus-service-account@{project-id}.iam.gserviceaccount.com on GCP) with all scopes in {@code
* defaultScopes} and {@code delegationScopes}. In addition, the user {@code
* gSuiteOutgoingEmailAddress} must have the permission to send emails.
*/
@GmailDelegatedCredential
@Provides

View File

@@ -55,8 +55,9 @@ import org.apache.commons.codec.binary.Base64;
*
* <p>This class accepts the application-default-credential as {@code ServiceAccountSigner},
* avoiding the need for exported private keys. In this case, the default credential user itself
* (project-id@appspot.gserviceaccount.com on AppEngine) must have domain-wide delegation to the
* Workspace APIs. The default credential user also must have the Token Creator role to itself.
* (nomulus-service-account@{project-id}.iam.gserviceaccount.com on GCP) must have domain-wide
* delegation to the Workspace APIs. The default credential user also must have the Token Creator
* role to itself.
*
* <p>If the user provides a credential {@code S} that carries its own private key, such as {@link
* com.google.auth.oauth2.ServiceAccountCredentials}, this class can use {@code S} to impersonate

View File

@@ -961,7 +961,7 @@ public final class RegistryConfig {
}
/**
* Number of times to retry a GAE operation when {@code TransientFailureException} is thrown.
* Number of times to retry an operation when {@code TransientFailureException} is thrown.
*
* <p>The number of milliseconds it'll sleep before giving up is {@code (2^n - 2) * 100}.
*
@@ -1422,7 +1422,7 @@ public final class RegistryConfig {
}
}
/** Returns the App Engine project ID, which is based off the environment name. */
/** Returns the project ID, which is based off the environment name. */
public static String getProjectId() {
return CONFIG_SETTINGS.get().gcpProject.projectId;
}
@@ -1448,51 +1448,6 @@ public final class RegistryConfig {
return makeUrl(String.format("https://%s.%s", service.getServiceId(), getBaseDomain()));
}
/**
* Returns the address of the Nomulus app default HTTP server.
*
* <p>This is used by the {@code nomulus} tool to connect to the App Engine remote API.
*/
public static URL getDefaultServer() {
return makeUrl(CONFIG_SETTINGS.get().gcpProject.defaultServiceUrl);
}
/**
* Returns the address of the Nomulus app backend HTTP server.
*
* <p>This is used by the {@code nomulus} tool to connect to the App Engine remote API.
*/
public static URL getBackendServer() {
return makeUrl(CONFIG_SETTINGS.get().gcpProject.backendServiceUrl);
}
/**
* Returns the address of the Nomulus app bsa HTTP server.
*
* <p>This is used by the {@code nomulus} tool to connect to the App Engine remote API.
*/
public static URL getBsaServer() {
return makeUrl(CONFIG_SETTINGS.get().gcpProject.bsaServiceUrl);
}
/**
* Returns the address of the Nomulus app tools HTTP server.
*
* <p>This is used by the {@code nomulus} tool to connect to the App Engine remote API.
*/
public static URL getToolsServer() {
return makeUrl(CONFIG_SETTINGS.get().gcpProject.toolsServiceUrl);
}
/**
* Returns the address of the Nomulus app pubapi HTTP server.
*
* <p>This is used by the {@code nomulus} tool to connect to the App Engine remote API.
*/
public static URL getPubapiServer() {
return makeUrl(CONFIG_SETTINGS.get().gcpProject.pubapiServiceUrl);
}
/** Returns the amount of time a singleton should be cached, before expiring. */
public static java.time.Duration getSingletonCacheRefreshDuration() {
return java.time.Duration.ofSeconds(CONFIG_SETTINGS.get().caching.singletonCacheRefreshSeconds);

View File

@@ -50,11 +50,6 @@ public class RegistryConfigSettings {
public long projectIdNumber;
public String locationId;
public boolean isLocal;
public String defaultServiceUrl;
public String backendServiceUrl;
public String bsaServiceUrl;
public String toolsServiceUrl;
public String pubapiServiceUrl;
public String baseDomain;
}

View File

@@ -12,17 +12,11 @@ gcpProject:
projectIdNumber: 123456789012
# Location of the GCP project, note that us-central1 and europe-west1 are special in that
# they are used without the trailing number in GCP commands and Google Cloud Console.
# See: https://cloud.google.com/appengine/docs/locations as an example
# See: https://docs.cloud.google.com/compute/docs/regions-zones as an example
locationId: registry-location-id
# whether to use local/test credentials when connecting to the servers
isLocal: true
# URLs of the services for the project.
defaultServiceUrl: https://default.example.com
backendServiceUrl: https://backend.example.com
bsaServiceUrl: https://bsa.example.com
toolsServiceUrl: https://tools.example.com
pubapiServiceUrl: https://pubapi.example.com
# The base domain name of the registry service. Services are reachable at [service].baseDomain.
baseDomain: registry.test
@@ -32,9 +26,9 @@ gSuite:
domainName: domain-registry.example
# Display name and email address used on outgoing emails through G Suite.
# The email address must be valid and have permission in the GAE app to send
# emails. For more info see:
# https://cloud.google.com/appengine/docs/standard/java/mail/#who_can_send_mail
# The email address must be valid and the domain must be set up to send emails.
# For more info see
# https://docs.cloud.google.com/compute/docs/tutorials/sending-mail
outgoingEmailDisplayName: Example Registry
outgoingEmailAddress: noreply@project-id.appspotmail.com
# TODO(b/279671974): reuse `outgoingEmailAddress` after migration
@@ -201,18 +195,16 @@ hibernate:
# but lock tables explicitly, either using framework-dependent API, or execute
# "select table for update" statements directly.
connectionIsolation: TRANSACTION_SERIALIZABLE
# Whether to log all SQL queries to App Engine logs. Overridable at runtime.
# Whether to log all SQL queries. Overridable at runtime.
logSqlQueries: false
# Connection pool configurations.
hikariConnectionTimeout: 20000
# Cloud SQL connections are a relatively scarce resource (maximum is 1000 as
# of March 2021). The minimumIdle should be a small value so that machines may
# release connections after a demand spike. The maximumPoolSize is set to 10
# because that is the maximum number of concurrent requests a Nomulus server
# instance can handle (as limited by AppEngine for basic/manual scaling). Note
# that BEAM pipelines are not subject to the maximumPoolSize value defined
# here. See PersistenceModule.java for more information.
# release connections after a demand spike. Note that BEAM pipelines are not
# subject to the maximumPoolSize value defined here. See PersistenceModule.java
# for more information.
hikariMinimumIdle: 1
hikariMaximumPoolSize: 40
hikariIdleTimeout: 300000
@@ -264,8 +256,8 @@ caching:
# Maximum total number of static premium list entry entities to cache in
# memory, across all premium lists for all TLDs. Tuning this up will use more
# memory (and might require using larger App Engine instances). Note that
# premium list entries that are absent are cached in addition to ones that are
# memory (and might require using larger instances). Note that premium list
# entries that are absent are cached in addition to ones that are
# present, so the total cache size is not bounded by the total number of
# premium price entries that exist.
staticPremiumListMaxCachedEntries: 200000
@@ -346,12 +338,8 @@ credentialOAuth:
localCredentialOauthScopes:
# View and manage data in all Google Cloud APIs.
- https://www.googleapis.com/auth/cloud-platform
# Call App Engine APIs locally.
- https://www.googleapis.com/auth/appengine.apis
# View your email address.
- https://www.googleapis.com/auth/userinfo.email
# View and manage your applications deployed on Google App Engine
- https://www.googleapis.com/auth/appengine.admin
# The lifetime of an access token generated by our custom credentials classes
# Must be shorter than one hour.
tokenRefreshDelaySeconds: 1800
@@ -433,7 +421,7 @@ misc:
spec11BccEmailAddresses:
- abuse@example.com
# Number of times to retry a GAE operation when a transient exception is thrown.
# Number of times to retry an operation when a transient exception is thrown.
# The number of milliseconds it'll sleep before giving up is (2^n - 2) * 100.
transientFailureRetries: 12

View File

@@ -57,8 +57,7 @@ import java.util.stream.Stream;
*
* <ul>
* <li>{@code endpoint} (Required) URL path of servlet to launch. This may contain pathargs.
* <li>{@code queue} (Required) Name of the App Engine push queue to which this task should be
* sent.
* <li>{@code queue} (Required) Name of the queue to which this task should be sent.
* <li>{@code forEachRealTld} Launch the task in each real TLD namespace.
* <li>{@code forEachTestTld} Launch the task in each test TLD namespace.
* <li>{@code runInEmpty} Launch the task once, without the TLD argument.

View File

@@ -36,8 +36,10 @@ import org.xbill.DNS.Opcode;
/**
* A transport for DNS messages. Sends/receives DNS messages over TCP using old-style {@link Socket}
* s and the message framing defined in <a href="https://tools.ietf.org/html/rfc1035">RFC 1035</a>.
* We would like use the dnsjava library's {@link org.xbill.DNS.SimpleResolver} class for this, but
* it requires {@link java.nio.channels.SocketChannel} which is not supported on AppEngine.
*
* <p>TODO(b/463732345): now that we're no longer on AppEngine, see if we can use the dnsjava
* library's {@link org.xbill.DNS.SimpleResolver} class instead of this (that requires {@link
* java.nio.channels.SocketChannel} which is not supported on AppEngine).
*/
public class DnsMessageTransport {

View File

@@ -51,7 +51,7 @@ public class FlowReporter {
@Inject Class<? extends Flow> flowClass;
@Inject FlowReporter() {}
/** Records information about the current flow execution in the GAE request logs. */
/** Records information about the current flow execution in the request logs. */
public void recordToLogs() {
// Explicitly log flow metadata separately from the EPP XML itself so that it stays compact
// enough to be sure to fit in a single log entry (the XML part in rare cases could be long

View File

@@ -73,7 +73,7 @@ public class FlowRunner {
eppRequestSource,
isDryRun ? "DRY_RUN" : "LIVE",
isSuperuser ? "SUPERUSER" : "NORMAL");
// Record flow info to the GAE request logs for reporting purposes if it's not a dry run.
// Record flow info to the request logs for reporting purposes if it's not a dry run.
if (!isDryRun) {
flowReporter.recordToLogs();
}

View File

@@ -44,7 +44,7 @@ public interface Keyring extends AutoCloseable {
* Returns public key for encrypting escrow deposits being staged to cloud storage.
*
* <p>This adds an additional layer of security so cloud storage administrators won't be tempted
* to go poking around the App Engine Cloud Console and see a dump of the entire database.
* to go poking around the Pantheon Cloud Console and see a dump of the entire database.
*
* <p>This keypair should only be known to the domain registry shared registry system.
*

View File

@@ -76,7 +76,7 @@ public class Cursor extends UpdateAutoTimestampEntity {
*
* <p>The way we solve this problem is by having {@code RdeUploadAction} check this cursor
* before performing an upload for a given TLD. If the cursor is less than two hours old, the
* action will fail with a status code above 300 and App Engine will keep retrying the action
* action will fail with a status code above 300 and Cloud Tasks will keep retrying the action
* until it's ready.
*/
RDE_UPLOAD_SFTP(true),

View File

@@ -28,7 +28,7 @@ import java.util.concurrent.TimeoutException;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.joda.time.DateTime;
/** Base for Servlets that handle all requests to our App Engine modules. */
/** Base for Servlets that handle all requests to our modules. */
public class ServletBase extends HttpServlet {
private final RequestHandler<?> requestHandler;

View File

@@ -33,10 +33,10 @@ import org.joda.time.Duration;
* Runner applying guaranteed reliability to an {@link EscrowTask}.
*
* <p>This class implements the <i>Locking Rolling Cursor</i> pattern, which solves the problem of
* how to reliably execute App Engine tasks which can't be made idempotent.
* how to reliably execute Cloud Tasks which can't be made idempotent.
*
* <p>{@link LockHandler} is used to ensure only one task executes at a time for a given {@code
* LockedCursorTask} subclass + TLD combination. This is necessary because App Engine tasks might
* LockedCursorTask} subclass + TLD combination. This is necessary because Cloud Task tasks might
* double-execute. Normally tasks solve this by being idempotent, but that's not possible for RDE,
* which writes to a GCS filename with a deterministic name. So locks are used to guarantee
* isolation. If we can't acquire the lock, it means the task is already running, so {@link

View File

@@ -100,8 +100,8 @@ import org.joda.time.Duration;
*
* <h2>Logging</h2>
*
* <p>To identify the reduce worker request for a deposit in App Engine's log viewer, you can use
* search text like {@code tld=soy}, {@code watermark=2015-01-01}, and {@code mode=FULL}.
* <p>To identify the reduce worker request for a deposit in the log viewer, you can use search text
* like {@code tld=soy}, {@code watermark=2015-01-01}, and {@code mode=FULL}.
*
* <h3>Error Handling</h3>
*

View File

@@ -107,7 +107,7 @@ public final class TransactionsReportingQueryBuilder implements QueryBuilder {
queriesBuilder.put(
getTableName(TRANSACTION_TRANSFER_LOSING, yearMonth), transactionTransferLosingQuery);
// App Engine log table suffixes use YYYYMMDD format
// Log table suffixes use YYYYMMDD format
DateTimeFormatter logTableFormatter = DateTimeFormat.forPattern("yyyyMMdd");
String attemptedAddsQuery =
SqlTemplate.create(getQueryFromFile(ATTEMPTED_ADDS + ".sql"))

View File

@@ -73,8 +73,8 @@ public abstract class HttpException extends RuntimeException {
/**
* Exception that causes a 204 response.
*
* <p>This is useful for App Engine task queue handlers that want to display an error, but don't
* want the task to automatically retry, since the status code is less than 300.
* <p>This is useful for task queue handlers that want to display an error, but don't want the
* task to automatically retry, since the status code is less than 300.
*/
public static final class NoContentException extends HttpException {
public NoContentException(String message) {

View File

@@ -25,7 +25,7 @@ import java.net.HttpURLConnection;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
/** Dagger modules for App Engine services and other vendor classes. */
/** Dagger modules for services and other vendor classes. */
public final class Modules {
/** Dagger module for {@link UrlConnectionService}. */

View File

@@ -69,7 +69,7 @@ final class Router {
ImmutableSortedMap.Builder<String, Route> routes =
new ImmutableSortedMap.Builder<>(Ordering.natural());
for (Method method : componentClass.getMethods()) {
// Make App Engine's security manager happy.
// Make the security manager happy.
method.setAccessible(true);
if (!isDaggerInstantiatorOfType(Runnable.class, method)) {
continue;

View File

@@ -27,12 +27,12 @@ import java.util.Map;
/**
* Utility class to help in dumping routing maps.
*
* <p>Each of the App Engine services (frontend, backend, and tools) has a Dagger component used for
* routing requests (e.g., FrontendRequestComponent). This class produces a text file representation
* of the routing configuration, showing what paths map to what action classes, as well as the
* properties of the action classes' annotations (which cover things like allowable HTTP methods,
* authentication settings, etc.). The text file can be useful for documentation, and is also used
* in unit tests to check against golden routing maps to find possibly unexpected changes.
* <p>The request-handling service has a Dagger component (RequestComponent) used for routing
* requests. This class produces a text file representation of the routing configuration, showing
* what paths map to what action classes, as well as the properties of the action classes'
* annotations (which cover things like allowable HTTP methods, authentication settings, etc.). The
* text file can be useful for documentation, and is also used in unit tests to check against golden
* routing maps to find possibly unexpected changes.
*
* <p>The file has fixed-width columns with a header row. The width of the columns is determined by
* the content to be displayed. The columns are:

View File

@@ -112,7 +112,7 @@ public final class NordnVerifyAction implements Runnable {
logger.atInfo().log(
"LORDN verify task %s response: HTTP response code %d", actionLogId, responseCode);
if (responseCode == SC_NO_CONTENT) {
// Send a 400+ status code so App Engine will retry the task.
// Send a 400+ status code so Cloud Tasks will retry the task.
throw new ConflictException("Not ready");
}
if (responseCode != SC_OK) {

View File

@@ -70,9 +70,6 @@ final class RegistryCli implements CommandRunner {
+ "Beam pipelines")
private String sqlAccessInfoFile = null;
@Parameter(names = "--gae", description = "Whether to use GAE runtime, instead of GKE")
private boolean useGae = false;
@Parameter(names = "--canary", description = "Whether to connect to the canary instances")
private boolean useCanary = false;
@@ -169,7 +166,6 @@ final class RegistryCli implements CommandRunner {
DaggerRegistryToolComponent.builder()
.credentialFilePath(credentialJson)
.sqlAccessInfoFile(sqlAccessInfoFile)
.useGke(!useGae)
.useCanary(useCanary)
.build();
@@ -203,7 +199,8 @@ final class RegistryCli implements CommandRunner {
"""
This error is likely the result of having another instance of
nomulus running at the same time. Check your system, shut down
the other instance, and try again.""");
the other instance, and try again.\
""");
System.err.println("===================================================================");
} else {
throw e;
@@ -213,7 +210,7 @@ final class RegistryCli implements CommandRunner {
}
private ServiceConnection getConnection() {
// Get the App Engine connection, advise the user if they are not currently logged in.
// Get the service connection, advise the user if they are not currently logged in.
if (connection == null) {
connection = component.serviceConnection();
}

View File

@@ -180,9 +180,6 @@ interface RegistryToolComponent {
@BindsInstance
Builder sqlAccessInfoFile(@Nullable @Config("sqlAccessInfoFile") String sqlAccessInfoFile);
@BindsInstance
Builder useGke(@Config("useGke") boolean useGke);
@BindsInstance
Builder useCanary(@Config("useCanary") boolean useCanary);

View File

@@ -29,8 +29,8 @@ import google.registry.util.OidcTokenUtils;
/**
* Module for providing the HttpRequestFactory.
*
* <p>Localhost connections go to the App Engine dev server. The dev server differs from most HTTP
* connections in that they don't require OAuth2 credentials, but instead require a special cookie.
* <p>Localhost connections go to the dev server. The dev server differs from most HTTP connections
* in that it doesn't require OAuth2 credentials, but instead requires a special cookie.
*/
@Module
final class RequestFactoryModule {
@@ -61,10 +61,9 @@ final class RequestFactoryModule {
AUTHORIZATION,
"Bearer "
+ OidcTokenUtils.createOidcToken(credentialsBundle, oauthClientId));
// GAE request times out after 10 min, so here we set the timeout to 10 min. This is
// Requests time out after 10 min, so here we set the timeout to 10 min. This is
// needed to support some nomulus commands like updating premium lists that take
// a lot of time to complete.
// See
// a lot of time to complete. See
// https://developers.google.com/api-client-library/java/google-api-java-client/errors
request.setConnectTimeout(REQUEST_TIMEOUT_MS);
request.setReadTimeout(REQUEST_TIMEOUT_MS);

View File

@@ -47,8 +47,8 @@ import org.json.simple.JSONValue;
/**
* An HTTP connection to a service.
*
* <p>By default - connects to the TOOLS service in GAE and the BACKEND service in GKE. To create a
* Connection to another service, call the {@link #withService} function.
* <p>By default - connects the BACKEND service. To create a connection to another service, call the
* {@link #withService} function.
*/
public class ServiceConnection {

View File

@@ -22,7 +22,6 @@ import static google.registry.request.Action.Method.GET;
import static google.registry.request.Action.Method.POST;
import static google.registry.request.RequestParameters.PARAM_TLDS;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import google.registry.model.EppResourceUtils;
@@ -31,7 +30,6 @@ import google.registry.request.Action;
import google.registry.request.Parameter;
import google.registry.request.auth.Auth;
import google.registry.util.Clock;
import google.registry.util.NonFinalForTesting;
import jakarta.inject.Inject;
/** An action that lists domains, for use by the {@code nomulus list_domains} command. */
@@ -42,9 +40,6 @@ import jakarta.inject.Inject;
auth = Auth.AUTH_ADMIN)
public final class ListDomainsAction extends ListObjectsAction<Domain> {
/** An App Engine limitation on how many subqueries can be used in a single query. */
@VisibleForTesting @NonFinalForTesting static int maxNumSubqueries = 30;
public static final String PATH = "/_dr/admin/list/domains";
@Inject

View File

@@ -19,7 +19,7 @@
-- monthly GKE logs, searching for all create commands and associating
-- them with their corresponding registrars.
-- Example log generated by FlowReporter in App Engine and GKE logs:
-- Example log generated by FlowReporter in GKE logs:
--google.registry.flows.FlowReporter
-- recordToLogs: FLOW-LOG-SIGNATURE-METADATA:
--{"serverTrid":"oNwL2J2eRya7bh7c9oHIzg==-2360a","clientId":"ipmirror"

View File

@@ -72,7 +72,7 @@ class PersistenceModuleTest {
}
@Test
void appengineIsolation() {
void connectionIsolation() {
assertThat(PersistenceModule.provideDefaultDatabaseConfigs().get(Environment.ISOLATION))
.isEqualTo(TransactionIsolationLevel.TRANSACTION_SERIALIZABLE.name());
}

View File

@@ -52,12 +52,12 @@ public final class RegistryTestServerMain {
@Parameter(
names = "--login_email",
description = "Login email address for App Engine Local User Service.")
description = "Login email address for the local user service.")
private String loginEmail = "Marla.Singer@crr.com";
@Parameter(
names = "--login_is_admin",
description = "Should logged in user be an admin for App Engine Local User Service.",
description = "Should logged in user be an admin for the local user service?",
arity = 1)
private boolean loginIsAdmin = true;
@@ -153,7 +153,6 @@ public final class RegistryTestServerMain {
}
} finally {
server.stop();
// appEngine.tearDown();
}
}

View File

@@ -102,29 +102,6 @@ class ListDomainsActionTest extends ListActionTestCase {
"^example2.foo$");
}
@Test
void testRun_moreTldsThanMaxNumSubqueries() {
ListDomainsAction.maxNumSubqueries = 2;
createTlds("baa", "bab", "bac", "bad");
action.tlds = ImmutableSet.of("baa", "bab", "bac", "bad");
action.limit = 4;
persistActiveDomain("domain1.baa", DateTime.parse("2010-03-04T16:00:00Z"));
persistActiveDomain("domain2.bab", DateTime.parse("2009-03-04T16:00:00Z"));
persistActiveDomain("domain3.bac", DateTime.parse("2011-03-04T16:00:00Z"));
persistActiveDomain("domain4.bad", DateTime.parse("2010-06-04T16:00:00Z"));
persistActiveDomain("domain5.baa", DateTime.parse("2008-01-04T16:00:00Z"));
// Since the limit is 4, expect all but domain5.baa (the oldest), sorted by creationTime asc.
testRunSuccess(
action,
Optional.empty(),
Optional.empty(),
Optional.empty(),
"^domain2.bab$",
"^domain1.baa$",
"^domain4.bad$",
"^domain3.bac$");
}
@Test
void testRun_twoLinesWithIdOnlyNoHeader() {
action.tlds = ImmutableSet.of("foo");

View File

@@ -19,7 +19,7 @@
-- monthly GKE logs, searching for all create commands and associating
-- them with their corresponding registrars.
-- Example log generated by FlowReporter in App Engine and GKE logs:
-- Example log generated by FlowReporter in GKE logs:
--google.registry.flows.FlowReporter
-- recordToLogs: FLOW-LOG-SIGNATURE-METADATA:
--{"serverTrid":"oNwL2J2eRya7bh7c9oHIzg==-2360a","clientId":"ipmirror"

View File

@@ -3,6 +3,5 @@ handlers = java.util.logging.ConsoleHandler
google.registry.level = FINE
com.google.appengine.api.taskqueue.dev.level = WARNING
com.google.apphosting.utils.config.level = WARNING
org.quartz.level = WARNING

View File

@@ -4,8 +4,4 @@
<Configure id="wac" class="org.eclipse.jetty.ee10.webapp.WebAppContext">
<Set name="contextPath">/</Set>
<Set name="war">./webapps/nomulus.war</Set>
<Call class="java.lang.System" name="setProperty">
<Arg>google.registry.jetty</Arg>
<Arg>true</Arg>
</Call>
</Configure>

View File

@@ -54,8 +54,8 @@ import javax.net.ssl.SSLSession;
* <p>The ssl handler added can require client authentication, but it uses an {@link
* InsecureTrustManagerFactory}, which accepts any ssl certificate presented by the client, as long
* as the client uses the corresponding private key to establish SSL handshake. The client
* certificate hash will be passed along to GAE as an HTTP header for verification (not handled by
* this handler).
* certificate hash will be passed along to the service as an HTTP header for verification (not
* handled by this handler).
*/
@Sharable
public class SslServerInitializer<C extends Channel> extends ChannelInitializer<C> {

View File

@@ -112,7 +112,7 @@ public interface Protocol {
}
/**
* Connection parameters for a connection from the proxy to the GAE app.
* Connection parameters for a connection from the proxy to Nomulus.
*
* <p>This protocol is associated to a {@link NioSocketChannel} established by the proxy
* connecting to a remote peer.

View File

@@ -50,7 +50,7 @@ import java.util.Queue;
import java.util.concurrent.TimeoutException;
/**
* A multi-protocol proxy server that listens for protocols in {@link
* A multiprotocol proxy server that listens for protocols in {@link
* ProxyModule.ProxyComponent#protocols()} }.
*/
public class ProxyServer implements Runnable {
@@ -157,8 +157,8 @@ public class ProxyServer implements Runnable {
* Establishes an outbound relay channel and sets the relevant metadata on both channels.
*
* <p>This method also adds a listener that is called when the established outbound connection
* is closed. The outbound connection to GAE is *not* guaranteed to persist. In case that the
* outbound connection closes but the inbound connection is still active, the listener calls
* is closed. The outbound connection to Nomulus is *not* guaranteed to persist. In case that
* the outbound connection closes but the inbound connection is still active, the listener calls
* this function again to re-establish another outbound connection. The metadata is also reset
* so that the inbound channel knows to relay to the new outbound channel.
*/
@@ -226,7 +226,7 @@ public class ProxyServer implements Runnable {
}
});
} else {
// We cannot connect to GAE for unknown reasons, no relay can be done so drop the
// We cannot connect to Nomulus for unknown reasons, no relay can be done so drop the
// inbound connection as well.
logger.atSevere().withCause(future.cause()).log(
"Cannot connect to relay channel for %s channel: %s.",

View File

@@ -22,7 +22,7 @@ gcpScopes:
# Cloud KMS and Stackdriver Monitoring APIs.
- https://www.googleapis.com/auth/cloud-platform
# The OAuth scope required to be included in the access token for the GAE app
# The OAuth scope required to be included in the access token for the app
# to authenticate.
- https://www.googleapis.com/auth/userinfo.email

View File

@@ -57,7 +57,7 @@ public class BackendMetricsHandler extends ChannelDuplexHandler {
private Channel relayedChannel;
/**
* A queue that saves the time at which a request is sent to the GAE app.
* A queue that saves the time at which a request is sent to Nomulus.
*
* <p>This queue is used to calculate HTTP request-response latency. HTTP 1.1 specification allows
* for pipelining, in which a client can sent multiple requests without waiting for each
@@ -123,7 +123,7 @@ public class BackendMetricsHandler extends ChannelDuplexHandler {
.addListener(
future -> {
if (future.isSuccess()) {
// Only instrument request metrics when the request is actually sent to GAE.
// Only instrument request metrics when the request is actually sent to Nomulus
metrics.requestSent(relayedProtocolName, clientCertHash, bytes);
requestSentTimeQueue.add(clock.nowUtc());
}

View File

@@ -209,7 +209,7 @@ public abstract class HttpsRelayServiceHandler extends ByteToMessageCodec<FullHt
super.write(ctx, msg, promise);
}
/** Exception thrown when the response status from GAE is not 200. */
/** Exception thrown when the response status from Nomulus is not 200. */
public static class NonOkHttpResponseException extends Exception {
private static final long serialVersionUID = 5340993059579288708L;

View File

@@ -13,7 +13,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
# This script builds the GAE artifacts for a given environment, moves the
# This script builds the artifacts for a given environment, moves the
# artifacts for all services to a designated location, and then creates a
# tarball from there.

View File

@@ -30,8 +30,6 @@ import (
"gopkg.in/yaml.v3"
)
var gke bool = false
var projectName string
var baseDomain string
@@ -190,18 +188,8 @@ func (manager TasksSyncManager) getArgs(task Task, operationType string) []strin
description = strings.ReplaceAll(description, "\n", " ")
var service = "backend"
// 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
}
var uri string
if gke {
uri = fmt.Sprintf("https://%s.%s%s", service, baseDomain, strings.TrimSpace(task.URL))
} else {
uri = fmt.Sprintf("https://%s-dot-%s.appspot.com%s", service, projectName, strings.TrimSpace(task.URL))
}
uri = fmt.Sprintf("https://%s.%s%s", service, baseDomain, strings.TrimSpace(task.URL))
args := []string{
"--project", projectName,
@@ -342,8 +330,7 @@ func getExistingEntries(cmd *exec.Cmd) ExistingEntries {
func main() {
if len(os.Args) < 4 || os.Args[1] == "" || os.Args[2] == "" || os.Args[3] == "" {
panic("Error - Invalid Parameters.\n" +
"Required params: 1 - Nomulus config YAML path; 2 - config XML path; 3 - project name;\n" +
"Optional params: 5 - [--gke]")
"Required params: 1 - Nomulus config YAML path; 2 - config XML path; 3 - project name;\n")
}
// Nomulus YAML config file path, used to extract OAuth client ID.
nomulusConfigFileLocation := os.Args[1]
@@ -351,11 +338,6 @@ func main() {
configFileLocation := os.Args[2]
// Project name where to submit the tasks
projectName = os.Args[3]
// Whether to deploy cloud scheduler tasks to run on GKE
if len(os.Args) > 4 && os.Args[4] == "--gke" {
gke = true
log.Default().Println("GKE mode enabled")
}
log.Default().Println("YAML Filepath " + nomulusConfigFileLocation)
yamlFile, err := os.Open(nomulusConfigFileLocation)

View File

@@ -1,46 +0,0 @@
# 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'

View File

@@ -1,60 +0,0 @@
# This will delete all stopped GAE versions (save 3) as there is a limit on how
# many versions can exist in a project.
#
# To manually trigger a build on GCB, run:
# gcloud builds submit --config=cloudbuild-delete.yaml \
# --substitutions=TAG_NAME=[TAG],_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
#
# Note: to work around issue in Spinnaker's 'Deployment Manifest' stage,
# variable references must avoid the ${var} format. Valid formats include
# $var or ${"${var}"}. This file uses the former. Since TAG_NAME and _ENV are
# expanded in the copies sent to Spinnaker, we preserve the brackets around
# them for safe pattern matching during release.
# See https://github.com/spinnaker/spinnaker/issues/3028 for more information.
#
# GAE has a limit of ~250 versions per-project, including unused versions. We
# therefore need to periodically delete old versions. This GCB job finds all
# stopped versions and delete all but the last 3 (in case we need to rollback).
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
for version in $(gcloud app versions list \
--filter="SERVICE:$service AND SERVING_STATUS:STOPPED" \
--format="value(VERSION.ID,LAST_DEPLOYED)" \
--project=$project_id | sort -k 2 | head -n -3)
do
gcloud app versions delete $version --service=$service \
--project=$project_id --quiet;
done
done
timeout: 3600s
options:
machineType: 'N1_HIGHCPU_8'

View File

@@ -198,7 +198,6 @@ artifacts:
- 'core/src/main/java/google/registry/config/files/tasks/cloud-scheduler-tasks-*.xml'
- 'release/cloudbuild-sync-and-tag.yaml'
- 'release/cloudbuild-deploy-*.yaml'
- 'release/cloudbuild-delete-*.yaml'
- 'release/cloudbuild-renew-prober-certs-*.yaml'
- 'release/cloudbuild-schema-deploy-*.yaml'
- 'release/cloudbuild-schema-verify-*.yaml'

View File

@@ -100,8 +100,6 @@ steps:
sed -i s/builder:latest/builder@$builder_digest/g release/cloudbuild-sync-and-tag.yaml
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
@@ -109,10 +107,6 @@ steps:
for environment in alpha crash qa sandbox production; do
sed s/'$${_ENV}'/${environment}/g release/cloudbuild-deploy-gke.yaml \
> 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 | \

View File

@@ -1,151 +0,0 @@
## Summary
This package contains an automated rollback tool for the Nomulus server on
AppEngine. When given the Nomulus tag of a deployed release, the tool directs
all traffics in the four recognized services (backend, default, pubapi, and
tools) to that release. In the process, it handles Nomulus tag to AppEngine
version ID translation, checks the target binary's compatibility with SQL
schema, starts/stops versions and redirects traffic in proper sequence, and
updates deployment metadata appropriately.
The tool has two limitations:
1. This tool only accepts one release tag as rollback target, which is applied
to all services.
2. The tool immediately migrates all traffic to the new versions. It does not
support gradual migration. This is not an issue now since gradual migration
is only available in automatically scaled versions, while none of versions
is using automatic scaling.
Although this tool is named a rollback tool, it can also reverse a rollback,
that is, rolling forward to a newer release.
## Prerequisites
This tool requires python version 3.7+. It also requires two GCP client
libraries: google-cloud-storage and google-api-python-client. They can be
installed using pip.
Registry team members should use either non-sudo pip3 or virtualenv/venv to
install the GCP libraries. A 'sudo pip install' may interfere with the Linux
tooling on your corp desktop. The non-sudo 'pip3 install' command installs the
libraries under $HOME/.local. The virtualenv or venv methods allow more control
over the installation location.
Below is an example of using virtualenv to install the libraries:
```shell
sudo apt-get install virtualenv python3-venv
python3 -m venv myproject
source myproject/bin/activate
pip install google-cloud-storage
pip install google-api-python-client
deactivate
```
If using virtualenv, make sure to run 'source myproject/bin/activate' before
running the rollback script.
## Usage
The tool can be invoked using the rollback_tool script in the Nomulus root
directory. The following parameters may be requested:
* dev_project: This is the GCP project that hosts the release and deployment
infrastructure, including the Spinnaker pipelines.
* project: This is the GCP project that hosts the Nomulus server to be rolled
back.
* env: This is the name of the Nomulus environment, e.g., sandbox or
production. Although the project to environment is available in Gradle
scripts and internal configuration files, it is not easy to extract them.
Therefore, we require the user to provide it for now.
A typical workflow goes as follows:
### Check Which Release is Serving
From the Nomulus root directory:
```shell
rollback_tool show_serving_release --dev_project ... --project ... --env ...
```
The output may look like:
```
backend nomulus-v049 nomulus-20201019-RC00
default nomulus-v049 nomulus-20201019-RC00
pubapi nomulus-v049 nomulus-20201019-RC00
tools nomulus-v049 nomulus-20201019-RC00
```
### Review Recent Deployments
```shell
rollback_tool show_recent_deployments --dev_project ... --project ... --env ...
```
This command displays up to 3 most recent deployments. The output (from sandbox
which only has two tracked deployments as of the writing of this document) may
look like:
```
backend nomulus-v048 nomulus-20201012-RC00
default nomulus-v048 nomulus-20201012-RC00
pubapi nomulus-v048 nomulus-20201012-RC00
tools nomulus-v048 nomulus-20201012-RC00
backend nomulus-v049 nomulus-20201019-RC00
default nomulus-v049 nomulus-20201019-RC00
pubapi nomulus-v049 nomulus-20201019-RC00
tools nomulus-v049 nomulus-20201019-RC00
```
### Roll to the Target Release
```shell
rollback_tool rollback --dev_project ... --project ... --env ... \
--targt_release {YOUR_CHOSEN_TAG} --run_mode ...
```
The rollback subcommand has two new parameters:
* target_release: This is the Nomulus tag of the target release, in the form
of nomulus-YYYYMMDD-RC[0-9][0-9]
* run_mode: This is the execution mode of the rollback action. There are three
modes:
1. dryrun: The tool will only output information about every step of the
rollback, including commands that a user can copy and run elsewhere.
2. interactive: The tool will prompt the user before executing each step.
The user may choose to abort the rollback, skip the step, or continue
with the step.
3. automatic: Tool will execute all steps in one shot.
The rollback steps are organized according to the following logic:
```
for service in ['backend', 'default', 'pubapi', 'tools']:
if service is on basicScaling: (See Notes # 1)
start the target version
if service is on manualScaling:
start the target version
set num_instances to its originally configured value
for service in ['backend', 'default', 'pubapi', 'tools']:
direct traffic to target version
for service in ['backend', 'default', 'pubapi', 'tools']:
if originally serving version is not the target version:
if originally serving version is on basicaScaling
stop the version
if originally serving version is on manualScaling:
stop the version
set_num_instances to 1 (See Notes #2)
```
Notes:
1. Versions on automatic scaling cannot be started or stopped by gcloud or the
AppEngine Admin REST API.
2. The minimum value assignable to num_instances through the REST API is 1.
This instance eventually will be released too.

View File

@@ -1,199 +0,0 @@
# Copyright 2020 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.
"""Helper for using the AppEngine Admin REST API."""
import time
from typing import FrozenSet, Optional, Set, Tuple
from googleapiclient import discovery
import common
# AppEngine services under management.
SERVICES = frozenset(['backend', 'default', 'pubapi', 'tools'])
# Number of times to check the status of an operation before timing out.
_STATUS_CHECK_TIMES = 5
# Delay between status checks of a long-running operation, in seconds
_STATUS_CHECK_INTERVAL = 5
class AppEngineAdmin:
"""Wrapper around the AppEngine Admin REST API client.
This class provides wrapper methods around the REST API for service and
version queries and for migrating between versions.
"""
def __init__(self,
project: str,
service_lookup: Optional[discovery.Resource] = None,
status_check_interval: int = _STATUS_CHECK_INTERVAL) -> None:
"""Initialize this instance for an AppEngine(GCP) project.
Args:
project: The GCP project name of this AppEngine instance.
service_lookup: The GCP discovery handle for service API lookup.
status_check_interval: The delay in seconds between status queries
when executing long running operations.
"""
self._project = project
if service_lookup is not None:
apps = service_lookup.apps()
else:
apps = discovery.build('appengine', 'v1beta').apps()
self._services = apps.services()
self._versions = self._services.versions()
self._instances = self._versions.instances()
self._operations = apps.operations()
self._status_check_interval = status_check_interval
@property
def project(self):
return self._project
def get_serving_versions(self) -> FrozenSet[common.VersionKey]:
"""Returns the serving versions of every Nomulus service.
For each service in appengine.SERVICES, gets the version(s) actually
serving traffic. Services with the 'SERVING' status but no allocated
traffic are not included. Services not included in appengine.SERVICES
are also ignored.
Returns: An immutable collection of the serving versions grouped by
service.
"""
services = common.list_all_pages(self._services.list,
'services',
appsId=self._project)
# Response format is specified at
# http://googleapis.github.io/google-api-python-client/docs/dyn/appengine_v1beta.apps.services.html#list.
versions = []
for service in services:
if service['id'] in SERVICES:
# yapf: disable
versions_with_traffic = (
service.get('split', {}).get('allocations', {}).keys())
# yapf: enable
for version in versions_with_traffic:
versions.append(common.VersionKey(service['id'], version))
return frozenset(versions)
# yapf: disable # argument indent wrong
def get_version_configs(
self, versions: Set[common.VersionKey]
) -> FrozenSet[common.VersionConfig]:
# yapf: enable
"""Returns the configuration of requested versions.
For each version in the request, gets the rollback-related data from
its static configuration (found in appengine-web.xml).
Args:
versions: A set of the VersionKey objects, each containing the
versions being queried in that service.
Returns:
The version configurations in an immutable set.
"""
requested_services = {version.service_id for version in versions}
version_configs = []
# Sort the requested services for ease of testing. For now the mocked
# AppEngine admin in appengine_test can only respond in a fixed order.
for service_id in sorted(requested_services):
response = common.list_all_pages(self._versions.list,
'versions',
appsId=self._project,
servicesId=service_id)
# Format of version_list is defined at
# https://googleapis.github.io/google-api-python-client/docs/dyn/appengine_v1beta.apps.services.versions.html#list.
for version in response:
if common.VersionKey(service_id, version['id']) in versions:
scalings = [
s for s in list(common.AppEngineScaling)
if s.value in version
]
if len(scalings) != 1:
raise common.CannotRollbackError(
f'Expecting exactly one scaling, found {scalings}')
scaling = common.AppEngineScaling(list(scalings)[0])
if scaling == common.AppEngineScaling.MANUAL:
manual_instances = version.get(
scaling.value).get('instances')
else:
manual_instances = None
version_configs.append(
common.VersionConfig(service_id, version['id'],
scaling, manual_instances))
return frozenset(version_configs)
def list_instances(
self,
version: common.VersionKey) -> Tuple[common.VmInstanceInfo, ...]:
instances = common.list_all_pages(self._versions.instances().list,
'instances',
appsId=self._project,
servicesId=version.service_id,
versionsId=version.version_id)
# Format of version_list is defined at
# https://googleapis.github.io/google-api-python-client/docs/dyn/appengine_v1beta.apps.services.versions.instances.html#list
return tuple([
common.VmInstanceInfo(
inst['id'], common.parse_gcp_timestamp(inst['startTime']))
for inst in instances
])
def set_manual_scaling_num_instance(self, service_id: str, version_id: str,
manual_instances: int) -> None:
"""Creates an request to change an AppEngine version's status."""
update_mask = 'manualScaling.instances'
body = {'manualScaling': {'instances': manual_instances}}
response = self._versions.patch(appsId=self._project,
servicesId=service_id,
versionsId=version_id,
updateMask=update_mask,
body=body).execute()
operation_id = response.get('name').split('operations/')[1]
for _ in range(_STATUS_CHECK_TIMES):
if self.query_operation_status(operation_id):
return
time.sleep(self._status_check_interval)
raise common.CannotRollbackError(
f'Operation {operation_id} timed out.')
def query_operation_status(self, operation_id):
response = self._operations.get(appsId=self._project,
operationsId=operation_id).execute()
if response.get('response') is not None:
return True
if response.get('error') is not None:
raise common.CannotRollbackError(response['error'])
assert not response.get('done'), 'Operation done but no results.'
return False

View File

@@ -1,132 +0,0 @@
# Copyright 2020 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.
"""Unit tests for appengine."""
from typing import Any, Dict, List, Tuple, Union
import unittest
from unittest import mock
from unittest.mock import patch
from googleapiclient import http
import appengine
import common
def setup_appengine_admin(
) -> Tuple[appengine.AppEngineAdmin, http.HttpRequest]:
"""Helper for setting up a mocked AppEngineAdmin instance.
Returns:
An AppEngineAdmin instance and a request with which API responses can
be mocked.
"""
# Assign mocked API response to mock_request.execute.
mock_request = mock.MagicMock()
mock_request.uri.return_value = 'myuri'
# Mocked resource shared by services, versions, instances, and operations.
resource = mock.MagicMock()
resource.list.return_value = mock_request
resource.get.return_value = mock_request
resource.patch.return_value = mock_request
# Root resource of AppEngine API. Exact type unknown.
apps = mock.MagicMock()
apps.services.return_value = resource
resource.versions.return_value = resource
resource.instances.return_value = resource
apps.operations.return_value = resource
service_lookup = mock.MagicMock()
service_lookup.apps.return_value = apps
appengine_admin = appengine.AppEngineAdmin('project', service_lookup, 1)
return (appengine_admin, mock_request)
class AppEngineTestCase(unittest.TestCase):
"""Unit tests for appengine."""
def setUp(self) -> None:
self._client, self._mock_request = setup_appengine_admin()
self.addCleanup(patch.stopall)
# yapf: disable
def _set_mocked_response(
self,
responses: Union[Dict[str, Any], List[Dict[str, Any]]]) -> None:
# yapf: enable
if isinstance(responses, list):
self._mock_request.execute.side_effect = responses
else:
self._mock_request.execute.return_value = responses
def test_get_serving_versions(self) -> None:
self._set_mocked_response({
'services': [{
'split': {
'allocations': {
'my_version': 3.14,
}
},
'id': 'pubapi'
}, {
'split': {
'allocations': {
'another_version': 2.71,
}
},
'id': 'error_dashboard'
}]
})
self.assertEqual(
self._client.get_serving_versions(),
frozenset([common.VersionKey('pubapi', 'my_version')]))
def test_get_version_configs(self):
self._set_mocked_response({
'versions': [{
'basicScaling': {
'maxInstances': 10
},
'id': 'version'
}]
})
self.assertEqual(
self._client.get_version_configs(
frozenset([common.VersionKey('default', 'version')])),
frozenset([
common.VersionConfig('default', 'version',
common.AppEngineScaling.BASIC)
]))
def test_async_update(self):
self._set_mocked_response([
{
'name': 'project/operations/op_id',
'done': False
},
{
'name': 'project/operations/op_id',
'done': False
},
{
'name': 'project/operations/op_id',
'response': {},
'done': True
},
])
self._client.set_manual_scaling_num_instance('service', 'version', 1)
self.assertEqual(self._mock_request.execute.call_count, 3)
if __name__ == '__main__':
unittest.main()

View File

@@ -1,181 +0,0 @@
# Copyright 2020 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.
"""Data types and utilities common to the other modules in this package."""
import dataclasses
import datetime
import enum
import pathlib
import re
from typing import Any, Optional, Tuple
from google.protobuf import timestamp_pb2
class CannotRollbackError(Exception):
"""Indicates that rollback cannot be done by this tool.
This error is for situations where rollbacks are either not allowed or
cannot be planned. Example scenarios include:
- The target release is incompatible with the SQL schema.
- The target release has never been deployed to AppEngine.
- The target release is no longer available, e.g., has been manually
deleted by the operators.
- A state-changing call to AppEngine Admin API has failed.
User must manually fix such problems before trying again to roll back.
"""
pass
class AppEngineScaling(enum.Enum):
"""Types of scaling schemes supported in AppEngine.
The value of each name is the property name in the REST API requests and
responses.
"""
AUTOMATIC = 'automaticScaling'
BASIC = 'basicScaling'
MANUAL = 'manualScaling'
@dataclasses.dataclass(frozen=True)
class VersionKey:
"""Identifier of a deployed version on AppEngine.
AppEngine versions as deployable units are managed on per-service basis.
Each instance of this class uniquely identifies an AppEngine version.
This class implements the __eq__ method so that its equality property
applies to subclasses by default unless they override it.
"""
service_id: str
version_id: str
def __eq__(self, other):
return (isinstance(other, VersionKey)
and self.service_id == other.service_id
and self.version_id == other.version_id)
@dataclasses.dataclass(frozen=True, eq=False)
class VersionConfig(VersionKey):
"""Rollback-related static configuration of an AppEngine version.
Contains data found from the application-web.xml for this version.
Attributes:
scaling: The scaling scheme of this version. This value determines what
steps are needed for the rollback. If a version is on automatic
scaling, we only need to direct traffic to it or away from it. The
version cannot be started, stopped, or have its number of instances
updated. If a version is on manual scaling, it not only needs to be
started or stopped explicitly, its instances need to be updated too
(to 1, the lowest allowed number) when it is shutdown, and to its
originally configured number of VM instances when brought up.
manual_scaling_instances: The originally configured VM instances to use
for each version that is on manual scaling.
"""
scaling: AppEngineScaling
manual_scaling_instances: Optional[int] = None
@dataclasses.dataclass(frozen=True)
class VmInstanceInfo:
"""Information about an AppEngine VM instance."""
instance_name: str
start_time: datetime.datetime
def get_nomulus_root() -> str:
"""Finds the current Nomulus root directory.
Returns:
The absolute path to the Nomulus root directory.
"""
for folder in pathlib.Path(__file__).parents:
if not folder.joinpath('rollback_tool').exists():
continue
if not folder.joinpath('settings.gradle').exists():
continue
with open(folder.joinpath('settings.gradle'), 'r') as file:
for line in file:
if re.match(r"^rootProject.name\s*=\s*'nomulus'\s*$", line):
return folder.absolute()
raise RuntimeError(
'Do not move this file out of the Nomulus directory tree.')
def list_all_pages(func, data_field: str, *args, **kwargs) -> Tuple[Any, ...]:
"""Collects all data items from a paginator-based 'List' API.
Args:
func: The GCP API method that supports paged responses.
data_field: The field in a response object containing the data
items to be returned. This is guaranteed to be an Iterable
type.
*args: Positional arguments passed to func.
*kwargs: Keyword arguments passed to func.
Returns: An immutable collection of data items assembled from the
paged responses.
"""
result_collector = []
page_token = None
while True:
request = func(*args, pageToken=page_token, **kwargs)
response = request.execute()
result_collector.extend(response.get(data_field, []))
page_token = response.get('nextPageToken')
if not page_token:
return tuple(result_collector)
def parse_gcp_timestamp(timestamp: str) -> datetime.datetime:
"""Parses a timestamp string in GCP API to datetime.
This method uses protobuf's Timestamp class to parse timestamp strings.
This class is used by GCP APIs to parse timestamp strings, and is tolerant
to certain cases which can break datetime as of Python 3.8, e.g., the
trailing 'Z' as timezone, and fractional seconds with number of digits
other than 3 or 6.
Args:
timestamp: A string in RFC 3339 format.
Returns: A datetime instance.
"""
ts = timestamp_pb2.Timestamp()
ts.FromJsonString(timestamp)
return ts.ToDatetime()
def to_gcp_timestamp(timestamp: datetime.datetime) -> str:
"""Converts a datetime to string.
This method uses protobuf's Timestamp class to parse timestamp strings.
This class is used by GCP APIs to parse timestamp strings.
Args:
timestamp: The datetime instance to be converted.
Returns: A string in RFC 3339 format.
"""
ts = timestamp_pb2.Timestamp()
ts.FromDatetime(timestamp)
return ts.ToJsonString()

View File

@@ -1,70 +0,0 @@
# Copyright 2020 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.
"""Unit tests for the common module."""
import datetime
import unittest
from unittest import mock
from unittest.mock import call, patch
import common
class CommonTestCase(unittest.TestCase):
"""Unit tests for the common module."""
def setUp(self) -> None:
self._mock_request = mock.MagicMock()
self._mock_api = mock.MagicMock()
self._mock_api.list.return_value = self._mock_request
self.addCleanup(patch.stopall)
def test_list_all_pages_single_page(self):
self._mock_request.execute.return_value = {'data': [1]}
response = common.list_all_pages(self._mock_api.list,
'data',
appsId='project')
self.assertSequenceEqual(response, [1])
self._mock_api.list.assert_called_once_with(pageToken=None,
appsId='project')
def test_list_all_pages_multi_page(self):
self._mock_request.execute.side_effect = [{
'data': [1],
'nextPageToken': 'token'
}, {
'data': [2]
}]
response = common.list_all_pages(self._mock_api.list,
'data',
appsId='project')
self.assertSequenceEqual(response, [1, 2])
self.assertSequenceEqual(self._mock_api.list.call_args_list, [
call(pageToken=None, appsId='project'),
call(pageToken='token', appsId='project')
])
def test_parse_timestamp(self):
self.assertEqual(common.parse_gcp_timestamp('2020-01-01T00:00:00Z'),
datetime.datetime(2020, 1, 1))
def test_parse_timestamp_irregular_nano_digits(self):
# datetime only accepts 3 or 6 digits in fractional second.
self.assertRaises(
ValueError,
lambda: datetime.datetime.fromisoformat('2020-01-01T00:00:00.9'))
self.assertEqual(common.parse_gcp_timestamp('2020-01-01T00:00:00.9Z'),
datetime.datetime(2020, 1, 1, microsecond=900000))
if __name__ == '__main__':
unittest.main()

View File

@@ -1,148 +0,0 @@
# Copyright 2020 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.
"""Helper for managing Nomulus deployment records on GCS."""
from typing import Dict, FrozenSet, Set
from google.cloud import storage
import common
def _get_version_map_name(env: str):
return f'nomulus.{env}.versions'
def _get_schema_tag_file(env: str):
return f'sql.{env}.tag'
class GcsClient:
"""Manages Nomulus deployment records on GCS."""
def __init__(self, project: str, gcs_client=None) -> None:
"""Initializes the instance for a GCP project.
Args:
project: The GCP project with Nomulus deployment records.
gcs_client: Optional API client to use.
"""
self._project = project
if gcs_client is not None:
self._client = gcs_client
else:
self._client = storage.Client(self._project)
@property
def project(self):
return self._project
def _get_deploy_bucket_name(self):
return f'{self._project}-deployed-tags'
def _get_release_to_version_mapping(
self, env: str) -> Dict[common.VersionKey, str]:
"""Returns the content of the release to version mapping file.
File content is returned in utf-8 encoding. Each line in the file is
in this format:
'{RELEASE_TAG},{APP_ENGINE_SERVICE_ID},{APP_ENGINE_VERSION}'.
"""
file_content = self._client.get_bucket(
self._get_deploy_bucket_name()).get_blob(
_get_version_map_name(env)).download_as_text()
mapping = {}
for line in file_content.splitlines(False):
tag, service_id, version_id = line.split(',')
mapping[common.VersionKey(service_id, version_id)] = tag
return mapping
def get_versions_by_release(self, env: str,
nom_tag: str) -> FrozenSet[common.VersionKey]:
"""Returns AppEngine version ids of a given Nomulus release tag.
Fetches the version mapping file maintained by the deployment process
and parses its content into a collection of VersionKey instances.
A release may map to multiple versions in a service if it has been
deployed multiple times. This is not intended behavior and may only
happen by mistake.
Args:
env: The environment of the deployed release, e.g., sandbox.
nom_tag: The Nomulus release tag.
Returns:
An immutable set of VersionKey instances.
"""
mapping = self._get_release_to_version_mapping(env)
return frozenset(
[version for version in mapping if mapping[version] == nom_tag])
def get_releases_by_versions(
self, env: str,
versions: Set[common.VersionKey]) -> Dict[common.VersionKey, str]:
"""Gets the release tags of the AppEngine versions.
Args:
env: The environment of the deployed release, e.g., sandbox.
versions: The AppEngine versions.
Returns:
A mapping of versions to release tags.
"""
mapping = self._get_release_to_version_mapping(env)
return {
version: tag
for version, tag in mapping.items() if version in versions
}
def get_recent_deployments(
self, env: str, num_records: int) -> Dict[common.VersionKey, str]:
"""Gets the most recent deployment records.
Deployment records are stored in a file, with one line per service.
Caller should adjust num_records according to the number of services
in AppEngine.
Args:
env: The environment of the deployed release, e.g., sandbox.
num_records: the number of lines to go back.
"""
file_content = self._client.get_bucket(
self._get_deploy_bucket_name()).get_blob(
_get_version_map_name(env)).download_as_text()
mapping = {}
for line in file_content.splitlines(False)[-num_records:]:
tag, service_id, version_id = line.split(',')
mapping[common.VersionKey(service_id, version_id)] = tag
return mapping
def get_schema_tag(self, env: str) -> str:
"""Gets the release tag of the SQL schema in the given environment.
This tag is needed for the server/schema compatibility test.
"""
file_content = self._client.get_bucket(
self._get_deploy_bucket_name()).get_blob(
_get_schema_tag_file(env)).download_as_text().splitlines(False)
assert len(
file_content
) == 1, f'Unexpected content in {_get_schema_tag_file(env)}.'
return file_content[0]

View File

@@ -1,152 +0,0 @@
# Copyright 2020 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.
"""Unit tests for gcs."""
import textwrap
import unittest
from unittest import mock
import common
import gcs
def setup_gcs_client(env: str):
"""Sets up a mocked GcsClient.
Args:
env: Name of the Nomulus environment.
Returns:
A GcsClient instance and two mocked blobs representing the two schema
tag file and version map file on GCS.
"""
schema_tag_blob = mock.MagicMock()
schema_tag_blob.download_as_text.return_value = 'tag\n'
version_map_blob = mock.MagicMock()
blobs_by_name = {
f'nomulus.{env}.versions': version_map_blob,
f'sql.{env}.tag': schema_tag_blob
}
bucket = mock.MagicMock()
bucket.get_blob.side_effect = lambda blob_name: blobs_by_name[blob_name]
google_client = mock.MagicMock()
google_client.get_bucket.return_value = bucket
gcs_client = gcs.GcsClient('project', google_client)
return (gcs_client, schema_tag_blob, version_map_blob)
class GcsTestCase(unittest.TestCase):
"""Unit tests for gcs."""
_ENV = 'crash'
def setUp(self) -> None:
self._client, self._schema_tag_blob, self._version_map_blob = \
setup_gcs_client(self._ENV)
self.addCleanup(mock.patch.stopall)
def test_get_schema_tag(self):
self.assertEqual(self._client.get_schema_tag(self._ENV), 'tag')
def test_get_versions_by_release(self):
self._version_map_blob.download_as_text.return_value = \
'nomulus-20200925-RC02,backend,nomulus-backend-v008'
self.assertEqual(
self._client.get_versions_by_release(self._ENV,
'nomulus-20200925-RC02'),
frozenset([common.VersionKey('backend', 'nomulus-backend-v008')]))
def test_get_versions_by_release_not_found(self):
self._version_map_blob.download_as_text.return_value = \
'nomulus-20200925-RC02,backend,nomulus-backend-v008'
self.assertEqual(
self._client.get_versions_by_release(self._ENV, 'no-such-tag'),
frozenset([]))
def test_get_versions_by_release_multiple_service(self):
self._version_map_blob.download_as_text.return_value = textwrap.dedent(
"""\
nomulus-20200925-RC02,backend,nomulus-backend-v008
nomulus-20200925-RC02,default,nomulus-default-v008
""")
self.assertEqual(
self._client.get_versions_by_release(self._ENV,
'nomulus-20200925-RC02'),
frozenset([
common.VersionKey('backend', 'nomulus-backend-v008'),
common.VersionKey('default', 'nomulus-default-v008')
]))
def test_get_versions_by_release_multiple_deployment(self):
self._version_map_blob.download_as_text.return_value = textwrap.dedent(
"""\
nomulus-20200925-RC02,backend,nomulus-backend-v008
nomulus-20200925-RC02,backend,nomulus-backend-v018
""")
self.assertEqual(
self._client.get_versions_by_release(self._ENV,
'nomulus-20200925-RC02'),
frozenset([
common.VersionKey('backend', 'nomulus-backend-v008'),
common.VersionKey('backend', 'nomulus-backend-v018')
]))
def test_get_releases_by_versions(self):
self._version_map_blob.download_as_text.return_value = textwrap.dedent(
"""\
nomulus-20200925-RC02,backend,nomulus-backend-v008
nomulus-20200925-RC02,default,nomulus-default-v008
""")
self.assertEqual(
self._client.get_releases_by_versions(
self._ENV, {
common.VersionKey('backend', 'nomulus-backend-v008'),
common.VersionKey('default', 'nomulus-default-v008')
}), {
common.VersionKey('backend', 'nomulus-backend-v008'):
'nomulus-20200925-RC02',
common.VersionKey('default', 'nomulus-default-v008'):
'nomulus-20200925-RC02',
})
def test_get_recent_deployments(self):
file_content = textwrap.dedent("""\
nomulus-20200925-RC02,backend,nomulus-backend-v008
nomulus-20200925-RC02,default,nomulus-default-v008
""")
self._version_map_blob.download_as_text.return_value = file_content
self.assertEqual(
self._client.get_recent_deployments(self._ENV, 2), {
common.VersionKey('default', 'nomulus-default-v008'):
'nomulus-20200925-RC02',
common.VersionKey('backend', 'nomulus-backend-v008'):
'nomulus-20200925-RC02'
})
def test_get_recent_deployments_fewer_lines(self):
self._version_map_blob.download_as_text.return_value = textwrap.dedent(
"""\
nomulus-20200925-RC02,backend,nomulus-backend-v008
nomulus-20200925-RC02,default,nomulus-default-v008
""")
self.assertEqual(
self._client.get_recent_deployments(self._ENV, 1), {
common.VersionKey('default', 'nomulus-default-v008'):
'nomulus-20200925-RC02'
})
if __name__ == '__main__':
unittest.main()

View File

@@ -1,198 +0,0 @@
# Copyright 2020 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.
"""Generates a sequence of operations for execution."""
from typing import FrozenSet, Tuple
import appengine
import common
import dataclasses
import gcs
import steps
@dataclasses.dataclass(frozen=True)
class ServiceRollback:
"""Data needed for rolling back one service.
Holds the configurations of both the currently serving version(s) and the
rollback target in a service.
Attributes:
target_version: The version to roll back to.
serving_versions: The currently serving versions to be stopped. This
set may be empty. It may also have multiple versions (when traffic
is split).
"""
target_version: common.VersionConfig
serving_versions: FrozenSet[common.VersionConfig]
def __post_init__(self):
"""Validates that all versions are for the same service."""
if self.serving_versions:
for config in self.serving_versions:
assert config.service_id == self.target_version.service_id
# yapf: disable
def _get_service_rollback_plan(
target_configs: FrozenSet[common.VersionConfig],
serving_configs: FrozenSet[common.VersionConfig]
) -> Tuple[ServiceRollback, ...]:
# yapf: enable
"""Determines the versions to bring up/down in each service.
In each service, this method makes sure that at least one version is found
for the rollback target. If multiple versions are found, which may only
happen if the target release was deployed multiple times, randomly choose
one.
If a target version is already serving traffic, instead of checking if it
gets 100 percent of traffic, this method still generates operations to
start it and direct all traffic to it. This is not a problem since these
operations are idempotent.
Attributes:
target_configs: The rollback target versions in each managed service
(as defined in appengine.SERVICES).
serving_configs: The currently serving versions in each service.
Raises:
CannotRollbackError: Rollback is impossible because a target version
cannot be found for some service.
Returns:
For each service, the versions to bring up/down if applicable.
"""
targets_by_service = {}
for version in target_configs:
targets_by_service.setdefault(version.service_id, set()).add(version)
serving_by_service = {}
for version in serving_configs:
serving_by_service.setdefault(version.service_id, set()).add(version)
# The target_configs parameter only has configs for managed services.
# Since targets_by_service is derived from it, its keyset() should equal
# to appengine.SERVICES.
if targets_by_service.keys() != appengine.SERVICES:
cannot_rollback = appengine.SERVICES.difference(
targets_by_service.keys())
raise common.CannotRollbackError(
f'Target version(s) not found for {cannot_rollback}')
plan = []
for service_id, versions in targets_by_service.items():
serving_configs = serving_by_service.get(service_id, set())
versions_to_stop = serving_configs.difference(versions)
chosen_target = list(versions)[0]
plan.append(ServiceRollback(chosen_target,
frozenset(versions_to_stop)))
return tuple(plan)
# yapf: disable
def _generate_steps(
gcs_client: gcs.GcsClient,
appengine_admin: appengine.AppEngineAdmin,
env: str,
target_release: str,
rollback_plan: Tuple[ServiceRollback, ...]
) -> Tuple[steps.RollbackStep, ...]:
# yapf: enable
"""Generates the sequence of operations for execution.
A rollback consists of the following steps:
1. Run schema compatibility test for the target release.
2. For each service,
a. If the target version does not use automatic scaling, start it.
i. If target version uses manual scaling, sets its instances to the
configured values.
b. If the target version uses automatic scaling, do nothing.
3. For each service, immediately direct all traffic to the target version.
4. For each service, go over its versions to be stopped:
a. If a version uses automatic scaling, do nothing.
b. If a version does not use automatic scaling, stop it.
i. If a version uses manual scaling, sets its instances to 1 (one, the
lowest value allowed on the REST API) to release the instances.
5. Update the appropriate deployed tag file on GCS with the target release
tag.
Returns:
The sequence of operations to execute for rollback.
"""
rollback_steps = [
steps.check_schema_compatibility(gcs_client.project, target_release,
gcs_client.get_schema_tag(env))
]
for plan in rollback_plan:
if plan.target_version.scaling != common.AppEngineScaling.AUTOMATIC:
rollback_steps.append(
steps.start_or_stop_version(appengine_admin.project, 'start',
plan.target_version))
if plan.target_version.scaling == common.AppEngineScaling.MANUAL:
rollback_steps.append(
steps.set_manual_scaling_instances(
appengine_admin, plan.target_version,
plan.target_version.manual_scaling_instances))
for plan in rollback_plan:
rollback_steps.append(
steps.direct_service_traffic_to_version(appengine_admin.project,
plan.target_version))
for plan in rollback_plan:
for version in plan.serving_versions:
if version.scaling != common.AppEngineScaling.AUTOMATIC:
rollback_steps.append(
steps.start_or_stop_version(appengine_admin.project,
'stop', version))
if version.scaling == common.AppEngineScaling.MANUAL:
# Release all but one instances. Cannot set num_instances to 0
# with this api.
rollback_steps.append(
steps.set_manual_scaling_instances(appengine_admin,
version, 1))
rollback_steps.append(
steps.update_deploy_tags(gcs_client.project, env, target_release))
rollback_steps.append(
steps.sync_live_release(gcs_client.project, target_release))
return tuple(rollback_steps)
def get_rollback_plan(gcs_client: gcs.GcsClient,
appengine_admin: appengine.AppEngineAdmin, env: str,
target_release: str) -> Tuple[steps.RollbackStep, ...]:
"""Generates the sequence of rollback operations for execution."""
target_versions = gcs_client.get_versions_by_release(env, target_release)
serving_versions = appengine_admin.get_serving_versions()
all_version_configs = appengine_admin.get_version_configs(
target_versions.union(serving_versions))
target_configs = frozenset([
config for config in all_version_configs if config in target_versions
])
serving_configs = frozenset([
config for config in all_version_configs if config in serving_versions
])
rollback_plan = _get_service_rollback_plan(target_configs, serving_configs)
return _generate_steps(gcs_client, appengine_admin, env, target_release,
rollback_plan)

View File

@@ -1,130 +0,0 @@
# Copyright 2020 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.
"""End-to-end test of rollback."""
import textwrap
from typing import Any, Dict
import unittest
from unittest import mock
import appengine_test
import gcs_test
import plan
def _make_serving_version(service: str, version: str) -> Dict[str, Any]:
"""Creates description of one serving version in API response."""
return {
'split': {
'allocations': {
version: 1,
}
},
'id': service
}
def _make_version_config(version,
scaling: str,
instance_tag: str,
instances: int = 10) -> Dict[str, Any]:
"""Creates one version config as part of an API response."""
return {scaling: {instance_tag: instances}, 'id': version}
class RollbackTestCase(unittest.TestCase):
"""End-to-end test of rollback."""
def setUp(self) -> None:
self._appengine_admin, self._appengine_request = (
appengine_test.setup_appengine_admin())
self._gcs_client, self._schema_tag, self._version_map = (
gcs_test.setup_gcs_client('crash'))
self.addCleanup(mock.patch.stopall)
def test_rollback_success(self):
self._schema_tag.download_as_text.return_value = (
'nomulus-2010-1014-RC00')
self._version_map.download_as_text.return_value = textwrap.dedent("""\
nomulus-20201014-RC00,backend,nomulus-backend-v009
nomulus-20201014-RC00,default,nomulus-default-v009
nomulus-20201014-RC00,pubapi,nomulus-pubapi-v009
nomulus-20201014-RC00,tools,nomulus-tools-v009
nomulus-20201014-RC01,backend,nomulus-backend-v011
nomulus-20201014-RC01,default,nomulus-default-v010
nomulus-20201014-RC01,pubapi,nomulus-pubapi-v010
nomulus-20201014-RC01,tools,nomulus-tools-v010
""")
self._appengine_request.execute.side_effect = [
# Response to get_serving_versions:
{
'services': [
_make_serving_version('backend', 'nomulus-backend-v011'),
_make_serving_version('default', 'nomulus-default-v010'),
_make_serving_version('pubapi', 'nomulus-pubapi-v010'),
_make_serving_version('tools', 'nomulus-tools-v010')
]
},
# Responses to get_version_configs. AppEngineAdmin queries the
# services by alphabetical order to facilitate this test.
{
'versions': [
_make_version_config('nomulus-backend-v009',
'basicScaling', 'maxInstances'),
_make_version_config('nomulus-backend-v011',
'basicScaling', 'maxInstances')
]
},
{
'versions': [
_make_version_config('nomulus-default-v009',
'basicScaling', 'maxInstances'),
_make_version_config('nomulus-default-v010',
'basicScaling', 'maxInstances')
]
},
{
'versions': [
_make_version_config('nomulus-pubapi-v009',
'manualScaling', 'instances'),
_make_version_config('nomulus-pubapi-v010',
'manualScaling', 'instances')
]
},
{
'versions': [
_make_version_config('nomulus-tools-v009',
'automaticScaling',
'maxTotalInstances'),
_make_version_config('nomulus-tools-v010',
'automaticScaling',
'maxTotalInstances')
]
}
]
steps = plan.get_rollback_plan(self._gcs_client, self._appengine_admin,
'crash', 'nomulus-20201014-RC00')
self.assertEqual(len(steps), 15)
self.assertRegex(steps[0].info(),
'.*nom_build :integration:sqlIntegrationTest.*')
self.assertRegex(steps[1].info(), '.*gcloud app versions start.*')
self.assertRegex(steps[5].info(),
'.*gcloud app services set-traffic.*')
self.assertRegex(steps[9].info(), '.*gcloud app versions stop.*')
self.assertRegex(steps[13].info(),
'.*echo nomulus-20201014-RC00 | gcloud storage cat -.*')
self.assertRegex(steps[14].info(), '.*gcloud storage rsync --delete-unmatched-destination-objects .*')
if __name__ == '__main__':
unittest.main()

View File

@@ -1,178 +0,0 @@
# Copyright 2020 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.
"""Script to rollback the Nomulus server on AppEngine."""
import argparse
import dataclasses
import sys
import textwrap
from typing import Any, Optional, Tuple
import appengine
import gcs
import plan
MAIN_HELP = 'Script to roll back the Nomulus server on AppEngine.'
ROLLBACK_HELP = 'Rolls back Nomulus to the target release.'
GET_SERVING_RELEASE_HELP = 'Shows the release tag(s) of the serving versions.'
GET_RECENT_DEPLOYMENTS_HELP = ('Shows recently deployed versions and their '
'release tags.')
ROLLBACK_MODE_HELP = textwrap.dedent("""\
The execution mode.
- dryrun: Prints descriptions of all steps.
- interactive: Prompts for confirmation before executing
each step.
- auto: Executes all steps in one go.
""")
@dataclasses.dataclass(frozen=True)
class Argument:
"""Describes a command line argument.
This class is for use with argparse.ArgumentParser. Except for the
'arg_names' attribute which specifies the argument name and/or flags, all
other attributes must match an accepted parameter in the parser's
add_argument() method.
"""
arg_names: Tuple[str, ...]
help: str
default: Optional[Any] = None
required: bool = True
choices: Optional[Tuple[str, ...]] = None
def get_arg_attrs(self):
return dict((k, v) for k, v in vars(self).items() if k != 'arg_names')
ARGUMENTS = (Argument(('--dev_project', '-d'),
'The GCP project with Nomulus deployment records.'),
Argument(('--project', '-p'),
'The GCP project where the Nomulus server is deployed.'),
Argument(('--env', '-e'),
'The name of the Nomulus server environment.',
choices=('production', 'sandbox', 'crash', 'alpha')))
ROLLBACK_ARGUMENTS = (Argument(('--target_release', '-t'),
'The release to be deployed.'),
Argument(('--run_mode', '-m'),
ROLLBACK_MODE_HELP,
required=False,
default='dryrun',
choices=('dryrun', 'interactive', 'auto')))
def rollback(dev_project: str, project: str, env: str, target_release: str,
run_mode: str) -> None:
"""Rolls back a Nomulus server to the target release.
Args:
dev_project: The GCP project with deployment records.
project: The GCP project of the Nomulus server.
env: The environment name of the Nomulus server.
target_release: The tag of the release to be brought up.
run_mode: How to handle the rollback steps: print-only (dryrun)
one step at a time with user confirmation (interactive),
or all steps in one shot (automatic).
"""
steps = plan.get_rollback_plan(gcs.GcsClient(dev_project),
appengine.AppEngineAdmin(project), env,
target_release)
print('Rollback steps:\n\n')
for step in steps:
print(f'{step.info()}\n')
if run_mode == 'dryrun':
continue
if run_mode == 'interactive':
confirmation = input(
'Do you wish to (c)ontinue, (s)kip, or (a)bort? ')
if confirmation == 'a':
return
if confirmation == 's':
continue
step.execute()
def show_serving_release(dev_project: str, project: str, env: str) -> None:
"""Shows the release tag(s) of the currently serving versions."""
serving_versions = appengine.AppEngineAdmin(project).get_serving_versions()
versions_to_tags = gcs.GcsClient(dev_project).get_releases_by_versions(
env, serving_versions)
print(f'{project}:')
for version, tag in versions_to_tags.items():
print(f'{version.service_id}\t{version.version_id}\t{tag}')
def show_recent_deployments(dev_project: str, project: str, env: str) -> None:
"""Show release and version of recent deployments."""
num_services = len(appengine.SERVICES)
num_records = 3 * num_services
print(f'{project}:')
for version, tag in gcs.GcsClient(dev_project).get_recent_deployments(
env, num_records).items():
print(f'{version.service_id}\t{version.version_id}\t{tag}')
def main() -> int:
parser = argparse.ArgumentParser(prog='nom_rollback',
description=MAIN_HELP)
subparsers = parser.add_subparsers(dest='command',
help='Supported commands')
rollback_parser = subparsers.add_parser(
'rollback',
help=ROLLBACK_HELP,
formatter_class=argparse.RawTextHelpFormatter)
for flag in ARGUMENTS:
rollback_parser.add_argument(*flag.arg_names, **flag.get_arg_attrs())
for flag in ROLLBACK_ARGUMENTS:
rollback_parser.add_argument(*flag.arg_names, **flag.get_arg_attrs())
show_serving_release_parser = subparsers.add_parser(
'show_serving_release', help=GET_SERVING_RELEASE_HELP)
for flag in ARGUMENTS:
show_serving_release_parser.add_argument(*flag.arg_names,
**flag.get_arg_attrs())
show_recent_deployments_parser = subparsers.add_parser(
'show_recent_deployments', help=GET_RECENT_DEPLOYMENTS_HELP)
for flag in ARGUMENTS:
show_recent_deployments_parser.add_argument(*flag.arg_names,
**flag.get_arg_attrs())
args = parser.parse_args()
command = args.command
args = {k: v for k, v in vars(args).items() if k != 'command'}
{
'rollback': rollback,
'show_recent_deployments': show_recent_deployments,
'show_serving_release': show_serving_release
}[command](**args)
return 0
if __name__ == '__main__':
try:
sys.exit(main())
except Exception as ex: # pylint: disable=broad-except
print(ex)
sys.exit(1)

View File

@@ -1,186 +0,0 @@
# Copyright 2020 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.
"""Script to rolling-restart the Nomulus server on AppEngine.
This script effects a rolling restart of the Nomulus server by deleting VM
instances at a controlled pace and leave it to the AppEngine scaling policy
to bring up new VM instances.
For each service, this script gets a list of VM instances and sequentially
handles each instance as follows:
1. Issue a gcloud delete command for this instance.
2. Poll the AppEngine at fixed intervals until this instance no longer exists.
Instance deletion is not instantaneous. An instance actively processing
requests takes time to shutdown, and its replacement almost always comes
up immediately after the shutdown. For this reason, we believe that our current
implementation is sufficient safe, and will not pursue more sophisticated
algorithms.
Note that for backend instances that may handle large queries, it may take tens
of seconds, even minutes, to shut down one of them.
This script also accepts an optional start_time parameter that serves as a
filter of instances to delete: only those instances that started before this
time will be deleted. This parameter makes error handling easy. When this
script fails, simply rerun with the same start_time until it succeeds.
"""
import argparse
import datetime
import sys
import time
from typing import Iterable, Optional, Tuple
import appengine
import common
import steps
HELP_MAIN = 'Script to rolling-restart the Nomulus server on AppEngine'
HELP_MIN_DELAY = 'Minimum delay in seconds between instance deletions.'
HELP_MIN_LIVE_INSTANCE_PERCENT = (
'Minimum number of instances to keep, as a percentage '
'of the total at the beginning of the restart process.')
# yapf: disable
def generate_steps(
appengine_admin: appengine.AppEngineAdmin,
version: common.VersionKey,
started_before: datetime.datetime
) -> Tuple[steps.KillNomulusInstance, ...]:
# yapf: enable
instances = appengine_admin.list_instances(version)
return tuple([
steps.kill_nomulus_instance(appengine_admin.project, version,
inst.instance_name) for inst in instances
if inst.start_time <= started_before
])
def execute_steps(appengine_admin: appengine.AppEngineAdmin,
version: common.VersionKey,
cmds: Tuple[steps.KillNomulusInstance, ...], min_delay: int,
configured_num_instances: Optional[int]) -> None:
print(f'Restarting {len(cmds)} instances in {version.service_id}')
for cmd in cmds:
print(cmd.info())
cmd.execute()
while True:
time.sleep(min_delay)
running_instances = [
inst.instance_name
for inst in appengine_admin.list_instances(version)
]
if cmd.instance_name in running_instances:
print('Waiting for VM to shut down...')
continue
if (configured_num_instances is not None
and len(running_instances) < configured_num_instances):
print('Waiting for new VM to come up...')
continue
break
print('VM instance has shut down.\n')
print(f'Done: {len(cmds)} instances in {version.service_id}\n')
# yapf: disable
def restart_one_service(appengine_admin: appengine.AppEngineAdmin,
version: common.VersionKey,
min_delay: int,
started_before: datetime.datetime,
configured_num_instances: Optional[int]) -> None:
# yapf: enable
"""Restart VM instances in one service according to their start time.
Args:
appengine_admin: The client of AppEngine Admin API.
version: The Nomulus version to restart. This must be the currently
serving version.
min_delay: The minimum delay between successive deletions.
started_before: Only VM instances started before this time are to be
deleted.
configured_num_instances: When present, the constant number of instances
this version is configured with.
"""
cmds = generate_steps(appengine_admin, version, started_before)
# yapf: disable
execute_steps(
appengine_admin, version, cmds, min_delay, configured_num_instances)
# yapf: enable
# yapf: disable
def rolling_restart(project: str,
services: Iterable[str],
min_delay: int,
started_before: datetime.datetime):
# yapf: enable
print(f'Rolling restart {project} at '
f'{common.to_gcp_timestamp(started_before)}\n')
appengine_admin = appengine.AppEngineAdmin(project)
version_configs = appengine_admin.get_version_configs(
set(appengine_admin.get_serving_versions()))
restart_versions = [
version for version in version_configs
if version.service_id in services
]
# yapf: disable
for version in restart_versions:
restart_one_service(appengine_admin,
version,
min_delay,
started_before,
version.manual_scaling_instances)
# yapf: enable
def main() -> int:
parser = argparse.ArgumentParser(prog='rolling_restart',
description=HELP_MAIN)
parser.add_argument('--project',
'-p',
required=True,
help='The GCP project of the Nomulus server.')
parser.add_argument('--services',
'-s',
nargs='+',
choices=appengine.SERVICES,
default=appengine.SERVICES,
help='The services to rollback.')
parser.add_argument('--min_delay',
'-d',
type=int,
default=5,
choices=range(1, 100),
help=HELP_MIN_DELAY)
parser.add_argument(
'--started_before',
'-b',
type=common.parse_gcp_timestamp,
default=datetime.datetime.utcnow(),
help='Only kill VM instances started before this time.')
args = parser.parse_args()
rolling_restart(**vars(args))
return 0
if __name__ == '__main__':
try:
sys.exit(main())
except Exception as ex: # pylint: disable=broad-except
print(ex)
sys.exit(1)

View File

@@ -1,149 +0,0 @@
# Copyright 2020 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.
"""Unit tests of rolling_restart."""
import datetime
import unittest
from unittest import mock
import common
import rolling_restart
import steps
import appengine_test
class RollingRestartTestCase(unittest.TestCase):
"""Tests for rolling_restart."""
def setUp(self) -> None:
self._appengine_admin, self._appengine_request = (
appengine_test.setup_appengine_admin())
self._version = common.VersionKey('my_service', 'my_version')
self.addCleanup(mock.patch.stopall)
def _setup_execute_steps_tests(self):
self._appengine_request.execute.side_effect = [
# First list_instance response.
{
'instances': [{
'id': 'vm_to_delete',
'startTime': '2019-01-01T00:00:00Z'
}, {
'id': 'vm_to_stay',
'startTime': '2019-01-01T00:00:00Z'
}]
},
# Second list_instance response
{
'instances': [{
'id': 'vm_to_stay',
'startTime': '2019-01-01T00:00:00Z'
}]
},
# Third list_instance response
{
'instances': [{
'id': 'vm_to_stay',
'startTime': '2019-01-01T00:00:00Z'
}, {
'id': 'vm_new',
'startTime': '2019-01-01T00:00:00Z'
}]
}
]
def _setup_generate_steps_tests(self):
self._appengine_request.execute.side_effect = [
# First page of list_instance response.
{
'instances': [{
'id': 'vm_2019',
'startTime': '2019-01-01T00:00:00Z'
}],
'nextPageToken':
'token'
},
# Second and final page of list_instance response
{
'instances': [{
'id': 'vm_2020',
'startTime': '2020-01-01T00:00:00Z'
}]
}
]
def test_kill_vm_command(self) -> None:
cmd = steps.kill_nomulus_instance(
'my_project', common.VersionKey('my_service', 'my_version'),
'my_inst')
self.assertEqual(cmd.instance_name, 'my_inst')
self.assertIn(('gcloud app instances delete my_inst --quiet '
'--user-output-enabled=false --service my_service '
'--version my_version --project my_project'),
cmd.info())
def _generate_kill_vm_command(self, version: common.VersionKey,
instance_name: str):
return steps.kill_nomulus_instance(self._appengine_admin.project,
version, instance_name)
def test_generate_commands(self):
self._setup_generate_steps_tests()
commands = rolling_restart.generate_steps(self._appengine_admin,
self._version,
datetime.datetime.utcnow())
self.assertSequenceEqual(commands, [
self._generate_kill_vm_command(self._version, 'vm_2019'),
self._generate_kill_vm_command(self._version, 'vm_2020')
])
def test_generate_commands_older_vm(self):
self._setup_generate_steps_tests()
version = common.VersionKey('my_service', 'my_version')
# yapf: disable
commands = rolling_restart.generate_steps(
self._appengine_admin,
version,
common.parse_gcp_timestamp('2019-12-01T00:00:00Z'))
# yapf: enable
self.assertSequenceEqual(
commands, [self._generate_kill_vm_command(version, 'vm_2019')])
def test_execute_steps_variable_instances(self):
self._setup_execute_steps_tests()
cmd = mock.MagicMock()
cmd.instance_name = 'vm_to_delete'
cmds = tuple([cmd]) # yapf does not format (cmd,) correctly.
rolling_restart.execute_steps(appengine_admin=self._appengine_admin,
version=self._version,
cmds=cmds,
min_delay=0,
configured_num_instances=None)
self.assertEqual(self._appengine_request.execute.call_count, 2)
def test_execute_steps_fixed_instances(self):
self._setup_execute_steps_tests()
cmd = mock.MagicMock()
cmd.instance_name = 'vm_to_delete'
cmds = tuple([cmd]) # yapf does not format (cmd,) correctly.
rolling_restart.execute_steps(appengine_admin=self._appengine_admin,
version=self._version,
cmds=cmds,
min_delay=0,
configured_num_instances=2)
self.assertEqual(self._appengine_request.execute.call_count, 3)
if __name__ == '__main__':
unittest.main()

View File

@@ -1,186 +0,0 @@
# Copyright 2020 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.
"""Definition of rollback steps and factory methods to create them."""
import dataclasses
import subprocess
import textwrap
from typing import Tuple
import appengine
import common
@dataclasses.dataclass(frozen=True)
class RollbackStep:
"""One rollback step.
Most steps are implemented using commandline tools, e.g., gcloud,
and execute their commands by forking a subprocess. Each step
also has a info method that returns its command with a description.
Two steps are handled differently. The _UpdateDeployTag step gets a piped
shell command, which needs to be handled differently. The
_SetManualScalingNumInstances step uses the AppEngine Admin API client in
this package to set the number of instances.
"""
description: str
command: Tuple[str, ...]
def info(self) -> str:
return f'# {self.description}\n' f'{" ".join(self.command)}'
def execute(self) -> None:
"""Executes the step.
Raises:
CannotRollbackError if command fails.
"""
if subprocess.call(self.command) != 0:
raise common.CannotRollbackError(f'Failed: {self.description}')
def check_schema_compatibility(dev_project: str, nom_tag: str,
sql_tag: str) -> RollbackStep:
return RollbackStep(description='Check compatibility with SQL schema.',
command=(f'{common.get_nomulus_root()}/nom_build',
':integration:sqlIntegrationTest',
f'--schema_version={sql_tag}',
f'--nomulus_version={nom_tag}',
'--publish_repo='
f'gcs://{dev_project}-deployed-tags/maven'))
@dataclasses.dataclass(frozen=True)
class _SetManualScalingNumInstances(RollbackStep):
"""Sets the number of instances for a manual scaling version.
The Nomulus set_num_instances command is currently broken. This step uses
the AppEngine REST API to update the version.
"""
appengine_admin: appengine.AppEngineAdmin
version: common.VersionKey
num_instance: int
def execute(self) -> None:
self.appengine_admin.set_manual_scaling_num_instance(
self.version.service_id, self.version.version_id,
self.num_instance)
def set_manual_scaling_instances(appengine_admin: appengine.AppEngineAdmin,
version: common.VersionConfig,
num_instances: int) -> RollbackStep:
cmd_description = textwrap.dedent("""\
Nomulus set_num_instances command is currently broken.
This script uses the AppEngine REST API to update the version.
To set this value without using this tool, you may use the REST API at
https://cloud.google.com/appengine/docs/admin-api/reference/rest/v1beta/apps.services.versions/patch
""")
return _SetManualScalingNumInstances(
f'Set number of instance for manual-scaling version '
f'{version.version_id} in {version.service_id} to {num_instances}.',
(cmd_description, ''), appengine_admin, version, num_instances)
def start_or_stop_version(project: str, action: str,
version: common.VersionKey) -> RollbackStep:
"""Creates a rollback step that starts or stops an AppEngine version.
Args:
project: The GCP project of the AppEngine application.
action: Start or Stop.
version: The version being managed.
"""
return RollbackStep(
f'{action.title()} {version.version_id} in {version.service_id}',
('gcloud', 'app', 'versions', action, version.version_id, '--quiet',
'--service', version.service_id, '--project', project))
def direct_service_traffic_to_version(
project: str, version: common.VersionKey) -> RollbackStep:
return RollbackStep(
f'Direct all traffic to {version.version_id} in {version.service_id}.',
('gcloud', 'app', 'services', 'set-traffic', version.service_id,
'--quiet', f'--splits={version.version_id}=1', '--project', project))
@dataclasses.dataclass(frozen=True)
class KillNomulusInstance(RollbackStep):
"""Step that kills a Nomulus VM instance."""
instance_name: str
# yapf: disable
def kill_nomulus_instance(project: str,
version: common.VersionKey,
instance_name: str) -> KillNomulusInstance:
# yapf: enable
return KillNomulusInstance(
'Delete one VM instance.',
('gcloud', 'app', 'instances', 'delete', instance_name, '--quiet',
'--user-output-enabled=false', '--service', version.service_id,
'--version', version.version_id, '--project', project), instance_name)
@dataclasses.dataclass(frozen=True)
class _UpdateDeployTag(RollbackStep):
"""Updates the deployment tag on GCS."""
nom_tag: str
destination: str
def execute(self) -> None:
with subprocess.Popen(('gcloud', 'storage', 'cp', '-', self.destination),
stdin=subprocess.PIPE) as p:
try:
p.communicate(self.nom_tag.encode('utf-8'))
if p.wait() != 0:
raise common.CannotRollbackError(
f'Failed: {self.description}')
except:
p.kill()
raise
def update_deploy_tags(dev_project: str, env: str,
nom_tag: str) -> RollbackStep:
destination = f'gs://{dev_project}-deployed-tags/nomulus.{env}.tag'
return _UpdateDeployTag(
f'Update Nomulus tag in {env}',
(f'echo {nom_tag} | gcloud storage cp - {destination}', ''), nom_tag,
destination)
def sync_live_release(dev_project: str, nom_tag: str) -> RollbackStep:
"""Syncs the target release artifacts to the live folder.
By convention the gs://{dev_project}-deploy/live folder should contain the
artifacts from the currently serving release.
For Domain Registry team members, this step updates the nomulus tool
installed on corp desktops.
"""
artifacts_folder = f'gs://{dev_project}-deploy/{nom_tag}'
live_folder = f'gs://{dev_project}-deploy/live'
return RollbackStep(
f'Syncing {artifacts_folder} to {live_folder}.',
('gcloud', 'storage', 'rsync', '--delete-unmatched-destination-objects', artifacts_folder, live_folder))

View File

@@ -1,55 +0,0 @@
#!/bin/bash
# Copyright 2023 The Nomulus Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# This script updates number of instances of the service running on GCP
# Required parameters are:
# 1) projectId
# 2) service name
#
# Example:
# ./update_num_instances.sh domain-registry-sandbox pubapi
set -e
project=$1
service=$2
[[ -z "$1" || -z "$2" ]] && { echo "2 parameters required - projectId and service" ; exit 1; }
echo "Project: $project";
echo "Service: $service";
deployed_version=$(gcloud app versions list --service "${service}" \
--project "${project}" \
--filter "TRAFFIC_SPLIT>0.00" \
--format="csv[no-heading](VERSION.ID)")
service_description=$(curl -H "Authorization: Bearer $(gcloud auth print-access-token)" https://appengine.googleapis.com/v1/apps/${project}/services/${service}/versions/${deployed_version})
echo "Service configuration: $service_description"
echo "Input new number of instances: "
read num_instances
if [[ -n ${num_instances//[0-9]/} ]]; then
echo "Should be an integer"
exit 1;
fi
echo "Settings new number of instances: $num_instances"
curl -X PATCH https://appengine.googleapis.com/v1/apps/${project}/services/${service}/versions/${deployed_version}?updateMask=manualScaling.instances \
-H "Authorization: Bearer $(gcloud auth print-access-token)" \
-H 'Content-Type: application/json' \
-d "{ \"manualScaling\": { \"instances\": $num_instances }}"
service_description=$(curl -H "Authorization: Bearer $(gcloud auth print-access-token)" https://appengine.googleapis.com/v1/apps/${project}/services/${service}/versions/${deployed_version})
echo "Updated service configuration: $service_description"

View File

@@ -32,7 +32,7 @@ import javax.annotation.Nullable;
/**
* JUL formatter that formats log messages in a single-line JSON that Stackdriver logging can parse.
*
* <p>The structured logs written to {@code STDOUT} and {@code STDERR} will be picked up by GAE/GKE
* <p>The structured logs written to {@code STDOUT} and {@code STDERR} will be picked up by GKE
* logging agent and automatically ingested by Stackdriver. Certain fields (see below) in the JSON
* will be converted to the corresponding <a
* href="https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry">{@code Log Entry}</a>

View File

@@ -36,9 +36,9 @@ public final class ProxyHttpHeaders {
/**
* Fallback HTTP header name used to pass the client IP address from the proxy to Nomulus.
*
* <p>Note that Java 17's servlet implementation (at least on App Engine) injects some seemingly
* unrelated addresses into this header. We only use this as a fallback so the proxy can
* transition to use the above header that should not be interfered with.
* <p>Note that Java 17's servlet implementation may inject some seemingly unrelated addresses
* into this header. We only use this as a fallback so the proxy can transition to use the above
* header that should not be interfered with.
*/
public static final String FALLBACK_IP_ADDRESS = HttpHeaders.X_FORWARDED_FOR;

View File

@@ -51,7 +51,7 @@ public final class TypeUtils {
public static <T> T instantiate(Class<? extends T> clazz) {
checkArgument(
Modifier.isPublic(clazz.getModifiers()),
"AppEngine's custom security manager won't let us reflectively access non-public types");
"GCP's custom security manager won't let us reflectively access non-public types");
try {
return clazz.getConstructor().newInstance();
} catch (ReflectiveOperationException e) {
@@ -68,7 +68,7 @@ public final class TypeUtils {
public static <T, U> T instantiate(Class<? extends T> clazz, U arg) {
checkArgument(
Modifier.isPublic(clazz.getModifiers()),
"AppEngine's custom security manager won't let us reflectively access non-public types");
"GCP's custom security manager won't let us reflectively access non-public types");
try {
return clazz.getConstructor(arg.getClass()).newInstance(arg);
} catch (ReflectiveOperationException e) {

View File

@@ -26,8 +26,8 @@ import org.yaml.snakeyaml.Yaml;
* <p>There are always two YAML configuration files that are used: the {@code default-config.yaml}
* file, which contains default configuration for all environments, and the environment-specific
* {@code nomulus-config-ENVIRONMENT.yaml} file, which contains overrides for the default values for
* environment-specific settings such as the App Engine project ID. The environment-specific
* configuration can be blank, but it must exist.
* environment-specific settings such as the project ID. The environment-specific configuration can
* be blank, but it must exist.
*/
public final class YamlUtils {