mirror of
https://github.com/google/nomulus
synced 2026-09-29 11:15:45 +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:
+2
-2
@@ -84,10 +84,10 @@ tasks.build.dependsOn(tasks.checkLicense)
|
|||||||
// Paths to main and test sources.
|
// Paths to main and test sources.
|
||||||
ext.projectRootDir = "${rootDir}"
|
ext.projectRootDir = "${rootDir}"
|
||||||
|
|
||||||
// Tasks to deploy/stage all App Engine services
|
// Tasks to deploy/stage all services
|
||||||
task deploy {
|
task deploy {
|
||||||
group = 'deployment'
|
group = 'deployment'
|
||||||
description = 'Deploys all services to App Engine.'
|
description = 'Deploys all services.'
|
||||||
}
|
}
|
||||||
|
|
||||||
task stage {
|
task stage {
|
||||||
|
|||||||
@@ -33,8 +33,8 @@ public abstract class DateTimeUtils {
|
|||||||
/**
|
/**
|
||||||
* A date in the far future that we can treat as infinity.
|
* 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
|
* <p>This value is (2^63-1)/1000 rounded down. Postgres can store dates as 64 bit microseconds,
|
||||||
* Java uses milliseconds, so this is the largest representable date that will survive a
|
* but Java uses milliseconds, so this is the largest representable date that will survive a
|
||||||
* round-trip through the database.
|
* round-trip through the database.
|
||||||
*/
|
*/
|
||||||
public static final DateTime END_OF_TIME = new DateTime(Long.MAX_VALUE / 1000, DateTimeZone.UTC);
|
public static final DateTime END_OF_TIME = new DateTime(Long.MAX_VALUE / 1000, DateTimeZone.UTC);
|
||||||
|
|||||||
+1
-1
@@ -104,7 +104,7 @@ PROPERTIES = [
|
|||||||
Property('testFilter',
|
Property('testFilter',
|
||||||
'Comma separated list of test patterns, if specified run only '
|
'Comma separated list of test patterns, if specified run only '
|
||||||
'these.'),
|
'these.'),
|
||||||
Property('environment', 'GAE Environment for deployment and staging.'),
|
Property('environment', 'Environment for deployment and staging.'),
|
||||||
|
|
||||||
# Cloud SQL properties
|
# Cloud SQL properties
|
||||||
Property('dbServer',
|
Property('dbServer',
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ expected to change.
|
|||||||
|
|
||||||
## Deployment
|
## 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
|
During nomulus default service war build task, gradle script triggers the
|
||||||
following:
|
following:
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -110,7 +110,7 @@ configurations {
|
|||||||
// for details.
|
// for details.
|
||||||
nomulus_test
|
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
|
// javax.servlet:servlet-api:2.5
|
||||||
// For reasons we do not understand, marking the following dependencies as
|
// For reasons we do not understand, marking the following dependencies as
|
||||||
// compileOnly instead of compile does not exclude them from runtimeClasspath.
|
// compileOnly instead of compile does not exclude them from runtimeClasspath.
|
||||||
|
|||||||
@@ -55,8 +55,6 @@ import java.nio.charset.StandardCharsets;
|
|||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Random;
|
import java.util.Random;
|
||||||
import java.util.function.BiConsumer;
|
|
||||||
import java.util.function.Consumer;
|
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
import org.joda.time.Duration;
|
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
|
* <p>For GET requests we add them on to the URL, and for POST requests we add them in the body of
|
||||||
* the request.
|
* 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)
|
* @return the resulting path (unchanged for POST requests, with params added for GET requests)
|
||||||
*/
|
*/
|
||||||
private static String processRequestParameters(
|
private static String processRequestParameters(
|
||||||
String path,
|
String path,
|
||||||
Method method,
|
Method method,
|
||||||
Multimap<String, String> params,
|
Multimap<String, String> params,
|
||||||
BiConsumer<String, String> putHeadersFunction,
|
HttpRequest.Builder requestBuilder) {
|
||||||
Consumer<ByteString> setBodyFunction) {
|
|
||||||
if (CollectionUtils.isNullOrEmpty(params)) {
|
if (CollectionUtils.isNullOrEmpty(params)) {
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
@@ -148,8 +140,8 @@ public class CloudTasksUtils implements Serializable {
|
|||||||
if (method.equals(Method.GET)) {
|
if (method.equals(Method.GET)) {
|
||||||
return String.format("%s?%s", path, encodedParams);
|
return String.format("%s?%s", path, encodedParams);
|
||||||
}
|
}
|
||||||
putHeadersFunction.accept(HttpHeaders.CONTENT_TYPE, MediaType.FORM_DATA.toString());
|
requestBuilder.putHeaders(HttpHeaders.CONTENT_TYPE, MediaType.FORM_DATA.toString());
|
||||||
setBodyFunction.accept(ByteString.copyFrom(encodedParams, StandardCharsets.UTF_8));
|
requestBuilder.setBody(ByteString.copyFrom(encodedParams, StandardCharsets.UTF_8));
|
||||||
return path;
|
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
|
* default service account as the principal. That account must have permission to submit tasks to
|
||||||
* Cloud Tasks.
|
* Cloud Tasks.
|
||||||
*
|
*
|
||||||
* <p>The caller of this method is responsible for passing in the appropriate service based on the
|
* <p>The caller of this method is responsible for passing in the appropriate service. Use the
|
||||||
* runtime (GAE/GKE). Use the overload that takes an action class if possible.
|
* overload that takes an action class if possible.
|
||||||
*
|
*
|
||||||
* @param path the relative URI (staring with a slash and ending without one).
|
* @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 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
|
* @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.
|
* to the server to process the duplicate keys.
|
||||||
* @return the enqueued task.
|
* @return the enqueued task.
|
||||||
* @see <a
|
* @see <a href=https://docs.cloud.google.com/tasks/docs/creating-http-target-tasks#java>Creating
|
||||||
* href=ttps://cloud.google.com/appengine/docs/standard/java/taskqueue/push/creating-tasks#target>Specifyinig
|
* HTTP target tasks</a>
|
||||||
* the worker service</a>
|
|
||||||
*/
|
*/
|
||||||
protected Task createTask(
|
protected Task createTask(
|
||||||
String path, Method method, Action.Service service, Multimap<String, String> params) {
|
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 '/'.");
|
"The path must start with a '/'.");
|
||||||
HttpRequest.Builder requestBuilder =
|
HttpRequest.Builder requestBuilder =
|
||||||
HttpRequest.newBuilder().setHttpMethod(HttpMethod.valueOf(method.name()));
|
HttpRequest.newBuilder().setHttpMethod(HttpMethod.valueOf(method.name()));
|
||||||
path =
|
path = processRequestParameters(path, method, params, requestBuilder);
|
||||||
processRequestParameters(
|
|
||||||
path, method, params, requestBuilder::putHeaders, requestBuilder::setBody);
|
|
||||||
OidcToken.Builder oidcTokenBuilder =
|
OidcToken.Builder oidcTokenBuilder =
|
||||||
OidcToken.newBuilder()
|
OidcToken.newBuilder()
|
||||||
.setServiceAccountEmail(credential.serviceAccount())
|
.setServiceAccountEmail(credential.serviceAccount())
|
||||||
@@ -204,16 +193,15 @@ public class CloudTasksUtils implements Serializable {
|
|||||||
* Cloud Tasks.
|
* Cloud Tasks.
|
||||||
*
|
*
|
||||||
* <p>Prefer this overload over the one where the path and service are explicitly defined, as this
|
* <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 actionClazz the action class to run, must be annotated with {@link Action}.
|
||||||
* @param method the HTTP method to be used for the request.
|
* @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
|
* @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.
|
* to the server to process the duplicate keys.
|
||||||
* @return the enqueued task.
|
* @return the enqueued task.
|
||||||
* @see <a
|
* @see <a href=https://docs.cloud.google.com/tasks/docs/creating-http-target-tasks#java>Creating
|
||||||
* href=ttps://cloud.google.com/appengine/docs/standard/java/taskqueue/push/creating-tasks#target>Specifyinig
|
* HTTP target tasks</a>
|
||||||
* the worker service</a>
|
|
||||||
*/
|
*/
|
||||||
public Task createTask(
|
public Task createTask(
|
||||||
Class<? extends Runnable> actionClazz, Method method, Multimap<String, String> params) {
|
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}.
|
* 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
|
* <p>The caller of this method is responsible for passing in the appropriate service. Use the
|
||||||
* runtime (GAE/GKE). Use the overload that takes an action class if possible.
|
* overload that takes an action class if possible.
|
||||||
*
|
*
|
||||||
* @param path the relative URI (staring with a slash and ending without one).
|
* @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 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
|
* @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.
|
* to the server to process the duplicate keys.
|
||||||
* @param jitterSeconds the number of seconds that a task is randomly delayed up to.
|
* @param jitterSeconds the number of seconds that a task is randomly delayed up to.
|
||||||
* @return the enqueued task.
|
* @return the enqueued task.
|
||||||
* @see <a
|
* @see <a href=https://docs.cloud.google.com/tasks/docs/creating-http-target-tasks#java>Creating
|
||||||
* href=ttps://cloud.google.com/appengine/docs/standard/java/taskqueue/push/creating-tasks#target>Specifyinig
|
* HTTP target tasks</a>
|
||||||
* the worker service</a>
|
|
||||||
*/
|
*/
|
||||||
public Task createTaskWithJitter(
|
public Task createTaskWithJitter(
|
||||||
String path,
|
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}.
|
* 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
|
* <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 actionClazz the action class to run, must be annotated with {@link Action}.
|
||||||
* @param method the HTTP method to be used for the request.
|
* @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.
|
* to the server to process the duplicate keys.
|
||||||
* @param jitterSeconds the number of seconds that a task is randomly delayed up to.
|
* @param jitterSeconds the number of seconds that a task is randomly delayed up to.
|
||||||
* @return the enqueued task.
|
* @return the enqueued task.
|
||||||
* @see <a
|
* @see <a href=https://docs.cloud.google.com/tasks/docs/creating-http-target-tasks#java>Creating
|
||||||
* href=ttps://cloud.google.com/appengine/docs/standard/java/taskqueue/push/creating-tasks#target>Specifyinig
|
* HTTP target tasks</a>
|
||||||
* the worker service</a>
|
|
||||||
*/
|
*/
|
||||||
public Task createTaskWithJitter(
|
public Task createTaskWithJitter(
|
||||||
Class<? extends Runnable> actionClazz,
|
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 path the relative URI (staring with a slash and ending without one).
|
||||||
* @param method the HTTP method to be used for the request.
|
* @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
|
* @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.
|
* to the server to process the duplicate keys.
|
||||||
* @param delay the amount of time that a task needs to be delayed for.
|
* @param delay the amount of time that a task needs to be delayed for.
|
||||||
* @return the enqueued task.
|
* @return the enqueued task.
|
||||||
* @see <a
|
* @see <a href=https://docs.cloud.google.com/tasks/docs/creating-http-target-tasks#java>Creating
|
||||||
* href=ttps://cloud.google.com/appengine/docs/standard/java/taskqueue/push/creating-tasks#target>Specifyinig
|
* HTTP target tasks</a>
|
||||||
* the worker service</a>
|
|
||||||
*/
|
*/
|
||||||
private Task createTaskWithDelay(
|
private Task createTaskWithDelay(
|
||||||
String path,
|
String path,
|
||||||
@@ -330,7 +315,7 @@ public class CloudTasksUtils implements Serializable {
|
|||||||
* Create a {@link Task} to be enqueued with delay of {@code duration}.
|
* 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
|
* <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 actionClazz the action class to run, must be annotated with {@link Action}.
|
||||||
* @param method the HTTP method to be used for the request.
|
* @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.
|
* to the server to process the duplicate keys.
|
||||||
* @param delay the amount of time that a task needs to be delayed for.
|
* @param delay the amount of time that a task needs to be delayed for.
|
||||||
* @return the enqueued task.
|
* @return the enqueued task.
|
||||||
* @see <a
|
* @see <a href=https://docs.cloud.google.com/tasks/docs/creating-http-target-tasks#java>Creating
|
||||||
* href=ttps://cloud.google.com/appengine/docs/standard/java/taskqueue/push/creating-tasks#target>Specifyinig
|
* HTTP target tasks</a>
|
||||||
* the worker service</a>
|
|
||||||
*/
|
*/
|
||||||
public Task createTaskWithDelay(
|
public Task createTaskWithDelay(
|
||||||
Class<? extends Runnable> actionClazz,
|
Class<? extends Runnable> actionClazz,
|
||||||
|
|||||||
@@ -112,11 +112,11 @@ public class RelockDomainAction implements Runnable {
|
|||||||
public void run() {
|
public void run() {
|
||||||
/* We wish to manually control our retry behavior, in order to limit the number of retries
|
/* 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
|
* 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.
|
* 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
|
* See https://docs.cloud.google.com/tasks/docs/configuring-queues#retry for more details on
|
||||||
* for more details on retry behavior. */
|
* retry behavior. */
|
||||||
response.setStatus(SC_NO_CONTENT);
|
response.setStatus(SC_NO_CONTENT);
|
||||||
response.setContentType(MediaType.PLAIN_TEXT_UTF_8);
|
response.setContentType(MediaType.PLAIN_TEXT_UTF_8);
|
||||||
tm().transact(this::relockDomain);
|
tm().transact(this::relockDomain);
|
||||||
|
|||||||
@@ -40,8 +40,6 @@ public class RegistryPipelineWorkerInitializer implements JvmInitializer {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void beforeProcessing(PipelineOptions options) {
|
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);
|
RegistryPipelineOptions registryOptions = options.as(RegistryPipelineOptions.class);
|
||||||
RegistryEnvironment environment = registryOptions.getRegistryEnvironment();
|
RegistryEnvironment environment = registryOptions.getRegistryEnvironment();
|
||||||
if (environment == null || environment.equals(RegistryEnvironment.UNITTEST)) {
|
if (environment == null || environment.equals(RegistryEnvironment.UNITTEST)) {
|
||||||
|
|||||||
@@ -279,20 +279,6 @@ public class BigqueryConnection implements AutoCloseable {
|
|||||||
private TableReference getTableReference() {
|
private TableReference getTableReference() {
|
||||||
return table.getTableReference().clone();
|
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
|
* Dumps the results of the specified query into a local ImmutableTable object, row-keyed by the
|
||||||
* ImmutableTable object, row-keyed by the row number (indexed from 1), column-keyed by the
|
* row number (indexed from 1), column-keyed by the TableFieldSchema for that column, and with the
|
||||||
* TableFieldSchema for that column, and with the value object as the cell value. Note that null
|
* value object as the cell value.
|
||||||
* values will not actually be null, but they can be checked for using Data.isNull().
|
|
||||||
*
|
*
|
||||||
* <p>Returns a ListenableFuture that holds the ImmutableTable on success.
|
* <p>Note that null values will not actually be null, but they can be checked for using
|
||||||
*/
|
* Data.isNull()
|
||||||
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>
|
|
||||||
*/
|
*/
|
||||||
public ImmutableTable<Integer, TableFieldSchema, Object> queryToLocalTableSync(String querySql) {
|
public ImmutableTable<Integer, TableFieldSchema, Object> queryToLocalTableSync(String querySql) {
|
||||||
Job job = new Job()
|
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. */
|
/** Helper that returns true if a dataset with this name exists. */
|
||||||
public boolean checkDatasetExists(String datasetName) throws IOException {
|
public boolean checkDatasetExists(String datasetName) throws IOException {
|
||||||
try {
|
try {
|
||||||
@@ -676,14 +641,6 @@ public class BigqueryConnection implements AutoCloseable {
|
|||||||
.setDatasetId(getDatasetId());
|
.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
|
* Helper that creates a dataset with this name if it doesn't already exist, and returns true if
|
||||||
* creation took place.
|
* creation took place.
|
||||||
|
|||||||
@@ -71,9 +71,7 @@ class BsaDiffCreator {
|
|||||||
Optional<String> previousJobName = schedule.latestCompleted().map(CompletedJob::jobName);
|
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 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
|
* memory in its entirety. Assuming 400K labels, each of which on average included in 5 orders,
|
||||||
* 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,
|
|
||||||
* the memory footprint is at least 300MB when loaded into a Hashset-backed Multimap (64-bit
|
* 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).
|
* JVM, with 12-byte object header, 16-byte array header, and 16-byte alignment).
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -44,8 +44,6 @@ public abstract class CredentialModule {
|
|||||||
* <p>The credential returned by the Cloud Runtime depends on the runtime environment:
|
* <p>The credential returned by the Cloud Runtime depends on the runtime environment:
|
||||||
*
|
*
|
||||||
* <ul>
|
* <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
|
* <li>On Compute Engine, returns a scope-less {@code ComputeEngineCredentials} for
|
||||||
* PROJECT_NUMBER-compute@developer.gserviceaccount.com
|
* PROJECT_NUMBER-compute@developer.gserviceaccount.com
|
||||||
* <li>On end user host, this returns the credential downloaded by gcloud. Please refer to <a
|
* <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.
|
* the application default credential user.
|
||||||
*
|
*
|
||||||
* <p>The Workspace domain must grant delegated admin access to the default service account 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}
|
* (nomulus-service-account@{project-id}.iam.gserviceaccount.com on GCP) with all scopes in {@code
|
||||||
* and {@code delegationScopes}.
|
* defaultScopes} and {@code delegationScopes}.
|
||||||
*/
|
*/
|
||||||
@AdcDelegatedCredential
|
@AdcDelegatedCredential
|
||||||
@Provides
|
@Provides
|
||||||
@@ -113,9 +111,9 @@ public abstract class CredentialModule {
|
|||||||
* Provides a {@link GoogleCredentialsBundle} for sending emails through Google Workspace.
|
* 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
|
* <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}
|
* (nomulus-service-account@{project-id}.iam.gserviceaccount.com on GCP) with all scopes in {@code
|
||||||
* and {@code delegationScopes}. In addition, the user {@code gSuiteOutgoingEmailAddress} must
|
* defaultScopes} and {@code delegationScopes}. In addition, the user {@code
|
||||||
* have the permission to send emails.
|
* gSuiteOutgoingEmailAddress} must have the permission to send emails.
|
||||||
*/
|
*/
|
||||||
@GmailDelegatedCredential
|
@GmailDelegatedCredential
|
||||||
@Provides
|
@Provides
|
||||||
|
|||||||
@@ -55,8 +55,9 @@ import org.apache.commons.codec.binary.Base64;
|
|||||||
*
|
*
|
||||||
* <p>This class accepts the application-default-credential as {@code ServiceAccountSigner},
|
* <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
|
* 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
|
* (nomulus-service-account@{project-id}.iam.gserviceaccount.com on GCP) must have domain-wide
|
||||||
* Workspace APIs. The default credential user also must have the Token Creator role to itself.
|
* 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
|
* <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
|
* com.google.auth.oauth2.ServiceAccountCredentials}, this class can use {@code S} to impersonate
|
||||||
|
|||||||
@@ -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}.
|
* <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() {
|
public static String getProjectId() {
|
||||||
return CONFIG_SETTINGS.get().gcpProject.projectId;
|
return CONFIG_SETTINGS.get().gcpProject.projectId;
|
||||||
}
|
}
|
||||||
@@ -1448,51 +1448,6 @@ public final class RegistryConfig {
|
|||||||
return makeUrl(String.format("https://%s.%s", service.getServiceId(), getBaseDomain()));
|
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. */
|
/** Returns the amount of time a singleton should be cached, before expiring. */
|
||||||
public static java.time.Duration getSingletonCacheRefreshDuration() {
|
public static java.time.Duration getSingletonCacheRefreshDuration() {
|
||||||
return java.time.Duration.ofSeconds(CONFIG_SETTINGS.get().caching.singletonCacheRefreshSeconds);
|
return java.time.Duration.ofSeconds(CONFIG_SETTINGS.get().caching.singletonCacheRefreshSeconds);
|
||||||
|
|||||||
@@ -50,11 +50,6 @@ public class RegistryConfigSettings {
|
|||||||
public long projectIdNumber;
|
public long projectIdNumber;
|
||||||
public String locationId;
|
public String locationId;
|
||||||
public boolean isLocal;
|
public boolean isLocal;
|
||||||
public String defaultServiceUrl;
|
|
||||||
public String backendServiceUrl;
|
|
||||||
public String bsaServiceUrl;
|
|
||||||
public String toolsServiceUrl;
|
|
||||||
public String pubapiServiceUrl;
|
|
||||||
public String baseDomain;
|
public String baseDomain;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,17 +12,11 @@ gcpProject:
|
|||||||
projectIdNumber: 123456789012
|
projectIdNumber: 123456789012
|
||||||
# Location of the GCP project, note that us-central1 and europe-west1 are special in that
|
# 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.
|
# 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
|
locationId: registry-location-id
|
||||||
|
|
||||||
# whether to use local/test credentials when connecting to the servers
|
# whether to use local/test credentials when connecting to the servers
|
||||||
isLocal: true
|
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.
|
# The base domain name of the registry service. Services are reachable at [service].baseDomain.
|
||||||
baseDomain: registry.test
|
baseDomain: registry.test
|
||||||
@@ -32,9 +26,9 @@ gSuite:
|
|||||||
domainName: domain-registry.example
|
domainName: domain-registry.example
|
||||||
|
|
||||||
# Display name and email address used on outgoing emails through G Suite.
|
# 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
|
# The email address must be valid and the domain must be set up to send emails.
|
||||||
# emails. For more info see:
|
# For more info see
|
||||||
# https://cloud.google.com/appengine/docs/standard/java/mail/#who_can_send_mail
|
# https://docs.cloud.google.com/compute/docs/tutorials/sending-mail
|
||||||
outgoingEmailDisplayName: Example Registry
|
outgoingEmailDisplayName: Example Registry
|
||||||
outgoingEmailAddress: noreply@project-id.appspotmail.com
|
outgoingEmailAddress: noreply@project-id.appspotmail.com
|
||||||
# TODO(b/279671974): reuse `outgoingEmailAddress` after migration
|
# TODO(b/279671974): reuse `outgoingEmailAddress` after migration
|
||||||
@@ -201,18 +195,16 @@ hibernate:
|
|||||||
# but lock tables explicitly, either using framework-dependent API, or execute
|
# but lock tables explicitly, either using framework-dependent API, or execute
|
||||||
# "select table for update" statements directly.
|
# "select table for update" statements directly.
|
||||||
connectionIsolation: TRANSACTION_SERIALIZABLE
|
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
|
logSqlQueries: false
|
||||||
|
|
||||||
# Connection pool configurations.
|
# Connection pool configurations.
|
||||||
hikariConnectionTimeout: 20000
|
hikariConnectionTimeout: 20000
|
||||||
# Cloud SQL connections are a relatively scarce resource (maximum is 1000 as
|
# 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
|
# 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
|
# release connections after a demand spike. Note that BEAM pipelines are not
|
||||||
# because that is the maximum number of concurrent requests a Nomulus server
|
# subject to the maximumPoolSize value defined here. See PersistenceModule.java
|
||||||
# instance can handle (as limited by AppEngine for basic/manual scaling). Note
|
# for more information.
|
||||||
# that BEAM pipelines are not subject to the maximumPoolSize value defined
|
|
||||||
# here. See PersistenceModule.java for more information.
|
|
||||||
hikariMinimumIdle: 1
|
hikariMinimumIdle: 1
|
||||||
hikariMaximumPoolSize: 40
|
hikariMaximumPoolSize: 40
|
||||||
hikariIdleTimeout: 300000
|
hikariIdleTimeout: 300000
|
||||||
@@ -264,8 +256,8 @@ caching:
|
|||||||
|
|
||||||
# Maximum total number of static premium list entry entities to cache in
|
# 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, across all premium lists for all TLDs. Tuning this up will use more
|
||||||
# memory (and might require using larger App Engine instances). Note that
|
# memory (and might require using larger instances). Note that premium list
|
||||||
# premium list entries that are absent are cached in addition to ones that are
|
# 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
|
# present, so the total cache size is not bounded by the total number of
|
||||||
# premium price entries that exist.
|
# premium price entries that exist.
|
||||||
staticPremiumListMaxCachedEntries: 200000
|
staticPremiumListMaxCachedEntries: 200000
|
||||||
@@ -346,12 +338,8 @@ credentialOAuth:
|
|||||||
localCredentialOauthScopes:
|
localCredentialOauthScopes:
|
||||||
# View and manage data in all Google Cloud APIs.
|
# View and manage data in all Google Cloud APIs.
|
||||||
- https://www.googleapis.com/auth/cloud-platform
|
- https://www.googleapis.com/auth/cloud-platform
|
||||||
# Call App Engine APIs locally.
|
|
||||||
- https://www.googleapis.com/auth/appengine.apis
|
|
||||||
# View your email address.
|
# View your email address.
|
||||||
- https://www.googleapis.com/auth/userinfo.email
|
- 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
|
# The lifetime of an access token generated by our custom credentials classes
|
||||||
# Must be shorter than one hour.
|
# Must be shorter than one hour.
|
||||||
tokenRefreshDelaySeconds: 1800
|
tokenRefreshDelaySeconds: 1800
|
||||||
@@ -433,7 +421,7 @@ misc:
|
|||||||
spec11BccEmailAddresses:
|
spec11BccEmailAddresses:
|
||||||
- abuse@example.com
|
- 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.
|
# The number of milliseconds it'll sleep before giving up is (2^n - 2) * 100.
|
||||||
transientFailureRetries: 12
|
transientFailureRetries: 12
|
||||||
|
|
||||||
|
|||||||
@@ -57,8 +57,7 @@ import java.util.stream.Stream;
|
|||||||
*
|
*
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>{@code endpoint} (Required) URL path of servlet to launch. This may contain pathargs.
|
* <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
|
* <li>{@code queue} (Required) Name of the queue to which this task should be sent.
|
||||||
* sent.
|
|
||||||
* <li>{@code forEachRealTld} Launch the task in each real TLD namespace.
|
* <li>{@code forEachRealTld} Launch the task in each real TLD namespace.
|
||||||
* <li>{@code forEachTestTld} Launch the task in each test TLD namespace.
|
* <li>{@code forEachTestTld} Launch the task in each test TLD namespace.
|
||||||
* <li>{@code runInEmpty} Launch the task once, without the TLD argument.
|
* <li>{@code runInEmpty} Launch the task once, without the TLD argument.
|
||||||
|
|||||||
@@ -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}
|
* 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>.
|
* 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 {
|
public class DnsMessageTransport {
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ public class FlowReporter {
|
|||||||
@Inject Class<? extends Flow> flowClass;
|
@Inject Class<? extends Flow> flowClass;
|
||||||
@Inject FlowReporter() {}
|
@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() {
|
public void recordToLogs() {
|
||||||
// Explicitly log flow metadata separately from the EPP XML itself so that it stays compact
|
// 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
|
// enough to be sure to fit in a single log entry (the XML part in rare cases could be long
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ public class FlowRunner {
|
|||||||
eppRequestSource,
|
eppRequestSource,
|
||||||
isDryRun ? "DRY_RUN" : "LIVE",
|
isDryRun ? "DRY_RUN" : "LIVE",
|
||||||
isSuperuser ? "SUPERUSER" : "NORMAL");
|
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) {
|
if (!isDryRun) {
|
||||||
flowReporter.recordToLogs();
|
flowReporter.recordToLogs();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ public interface Keyring extends AutoCloseable {
|
|||||||
* Returns public key for encrypting escrow deposits being staged to cloud storage.
|
* 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
|
* <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.
|
* <p>This keypair should only be known to the domain registry shared registry system.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ public class Cursor extends UpdateAutoTimestampEntity {
|
|||||||
*
|
*
|
||||||
* <p>The way we solve this problem is by having {@code RdeUploadAction} check this cursor
|
* <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
|
* 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.
|
* until it's ready.
|
||||||
*/
|
*/
|
||||||
RDE_UPLOAD_SFTP(true),
|
RDE_UPLOAD_SFTP(true),
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import java.util.concurrent.TimeoutException;
|
|||||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||||
import org.joda.time.DateTime;
|
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 {
|
public class ServletBase extends HttpServlet {
|
||||||
|
|
||||||
private final RequestHandler<?> requestHandler;
|
private final RequestHandler<?> requestHandler;
|
||||||
|
|||||||
@@ -33,10 +33,10 @@ import org.joda.time.Duration;
|
|||||||
* Runner applying guaranteed reliability to an {@link EscrowTask}.
|
* Runner applying guaranteed reliability to an {@link EscrowTask}.
|
||||||
*
|
*
|
||||||
* <p>This class implements the <i>Locking Rolling Cursor</i> pattern, which solves the problem of
|
* <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
|
* <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,
|
* 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
|
* 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
|
* isolation. If we can't acquire the lock, it means the task is already running, so {@link
|
||||||
|
|||||||
@@ -100,8 +100,8 @@ import org.joda.time.Duration;
|
|||||||
*
|
*
|
||||||
* <h2>Logging</h2>
|
* <h2>Logging</h2>
|
||||||
*
|
*
|
||||||
* <p>To identify the reduce worker request for a deposit in App Engine's log viewer, you can use
|
* <p>To identify the reduce worker request for a deposit in the log viewer, you can use search text
|
||||||
* search text like {@code tld=soy}, {@code watermark=2015-01-01}, and {@code mode=FULL}.
|
* like {@code tld=soy}, {@code watermark=2015-01-01}, and {@code mode=FULL}.
|
||||||
*
|
*
|
||||||
* <h3>Error Handling</h3>
|
* <h3>Error Handling</h3>
|
||||||
*
|
*
|
||||||
|
|||||||
+1
-1
@@ -107,7 +107,7 @@ public final class TransactionsReportingQueryBuilder implements QueryBuilder {
|
|||||||
queriesBuilder.put(
|
queriesBuilder.put(
|
||||||
getTableName(TRANSACTION_TRANSFER_LOSING, yearMonth), transactionTransferLosingQuery);
|
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");
|
DateTimeFormatter logTableFormatter = DateTimeFormat.forPattern("yyyyMMdd");
|
||||||
String attemptedAddsQuery =
|
String attemptedAddsQuery =
|
||||||
SqlTemplate.create(getQueryFromFile(ATTEMPTED_ADDS + ".sql"))
|
SqlTemplate.create(getQueryFromFile(ATTEMPTED_ADDS + ".sql"))
|
||||||
|
|||||||
@@ -73,8 +73,8 @@ public abstract class HttpException extends RuntimeException {
|
|||||||
/**
|
/**
|
||||||
* Exception that causes a 204 response.
|
* 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
|
* <p>This is useful for task queue handlers that want to display an error, but don't want the
|
||||||
* want the task to automatically retry, since the status code is less than 300.
|
* task to automatically retry, since the status code is less than 300.
|
||||||
*/
|
*/
|
||||||
public static final class NoContentException extends HttpException {
|
public static final class NoContentException extends HttpException {
|
||||||
public NoContentException(String message) {
|
public NoContentException(String message) {
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import java.net.HttpURLConnection;
|
|||||||
import javax.net.ssl.HttpsURLConnection;
|
import javax.net.ssl.HttpsURLConnection;
|
||||||
import javax.net.ssl.SSLContext;
|
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 {
|
public final class Modules {
|
||||||
|
|
||||||
/** Dagger module for {@link UrlConnectionService}. */
|
/** Dagger module for {@link UrlConnectionService}. */
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ final class Router {
|
|||||||
ImmutableSortedMap.Builder<String, Route> routes =
|
ImmutableSortedMap.Builder<String, Route> routes =
|
||||||
new ImmutableSortedMap.Builder<>(Ordering.natural());
|
new ImmutableSortedMap.Builder<>(Ordering.natural());
|
||||||
for (Method method : componentClass.getMethods()) {
|
for (Method method : componentClass.getMethods()) {
|
||||||
// Make App Engine's security manager happy.
|
// Make the security manager happy.
|
||||||
method.setAccessible(true);
|
method.setAccessible(true);
|
||||||
if (!isDaggerInstantiatorOfType(Runnable.class, method)) {
|
if (!isDaggerInstantiatorOfType(Runnable.class, method)) {
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -27,12 +27,12 @@ import java.util.Map;
|
|||||||
/**
|
/**
|
||||||
* Utility class to help in dumping routing maps.
|
* 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
|
* <p>The request-handling service has a Dagger component (RequestComponent) used for routing
|
||||||
* routing requests (e.g., FrontendRequestComponent). This class produces a text file representation
|
* requests. This class produces a text file representation of the routing configuration, showing
|
||||||
* of the routing configuration, showing what paths map to what action classes, as well as the
|
* what paths map to what action classes, as well as the properties of the action classes'
|
||||||
* properties of the action classes' annotations (which cover things like allowable HTTP methods,
|
* annotations (which cover things like allowable HTTP methods, authentication settings, etc.). The
|
||||||
* authentication settings, etc.). The text file can be useful for documentation, and is also used
|
* text file can be useful for documentation, and is also used in unit tests to check against golden
|
||||||
* in unit tests to check against golden routing maps to find possibly unexpected changes.
|
* 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
|
* <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:
|
* the content to be displayed. The columns are:
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ public final class NordnVerifyAction implements Runnable {
|
|||||||
logger.atInfo().log(
|
logger.atInfo().log(
|
||||||
"LORDN verify task %s response: HTTP response code %d", actionLogId, responseCode);
|
"LORDN verify task %s response: HTTP response code %d", actionLogId, responseCode);
|
||||||
if (responseCode == SC_NO_CONTENT) {
|
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");
|
throw new ConflictException("Not ready");
|
||||||
}
|
}
|
||||||
if (responseCode != SC_OK) {
|
if (responseCode != SC_OK) {
|
||||||
|
|||||||
@@ -70,9 +70,6 @@ final class RegistryCli implements CommandRunner {
|
|||||||
+ "Beam pipelines")
|
+ "Beam pipelines")
|
||||||
private String sqlAccessInfoFile = null;
|
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")
|
@Parameter(names = "--canary", description = "Whether to connect to the canary instances")
|
||||||
private boolean useCanary = false;
|
private boolean useCanary = false;
|
||||||
|
|
||||||
@@ -169,7 +166,6 @@ final class RegistryCli implements CommandRunner {
|
|||||||
DaggerRegistryToolComponent.builder()
|
DaggerRegistryToolComponent.builder()
|
||||||
.credentialFilePath(credentialJson)
|
.credentialFilePath(credentialJson)
|
||||||
.sqlAccessInfoFile(sqlAccessInfoFile)
|
.sqlAccessInfoFile(sqlAccessInfoFile)
|
||||||
.useGke(!useGae)
|
|
||||||
.useCanary(useCanary)
|
.useCanary(useCanary)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
@@ -203,7 +199,8 @@ final class RegistryCli implements CommandRunner {
|
|||||||
"""
|
"""
|
||||||
This error is likely the result of having another instance of
|
This error is likely the result of having another instance of
|
||||||
nomulus running at the same time. Check your system, shut down
|
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("===================================================================");
|
System.err.println("===================================================================");
|
||||||
} else {
|
} else {
|
||||||
throw e;
|
throw e;
|
||||||
@@ -213,7 +210,7 @@ final class RegistryCli implements CommandRunner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private ServiceConnection getConnection() {
|
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) {
|
if (connection == null) {
|
||||||
connection = component.serviceConnection();
|
connection = component.serviceConnection();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -180,9 +180,6 @@ interface RegistryToolComponent {
|
|||||||
@BindsInstance
|
@BindsInstance
|
||||||
Builder sqlAccessInfoFile(@Nullable @Config("sqlAccessInfoFile") String sqlAccessInfoFile);
|
Builder sqlAccessInfoFile(@Nullable @Config("sqlAccessInfoFile") String sqlAccessInfoFile);
|
||||||
|
|
||||||
@BindsInstance
|
|
||||||
Builder useGke(@Config("useGke") boolean useGke);
|
|
||||||
|
|
||||||
@BindsInstance
|
@BindsInstance
|
||||||
Builder useCanary(@Config("useCanary") boolean useCanary);
|
Builder useCanary(@Config("useCanary") boolean useCanary);
|
||||||
|
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ import google.registry.util.OidcTokenUtils;
|
|||||||
/**
|
/**
|
||||||
* Module for providing the HttpRequestFactory.
|
* Module for providing the HttpRequestFactory.
|
||||||
*
|
*
|
||||||
* <p>Localhost connections go to the App Engine dev server. The dev server differs from most HTTP
|
* <p>Localhost connections go to the dev server. The dev server differs from most HTTP connections
|
||||||
* connections in that they don't require OAuth2 credentials, but instead require a special cookie.
|
* in that it doesn't require OAuth2 credentials, but instead requires a special cookie.
|
||||||
*/
|
*/
|
||||||
@Module
|
@Module
|
||||||
final class RequestFactoryModule {
|
final class RequestFactoryModule {
|
||||||
@@ -61,10 +61,9 @@ final class RequestFactoryModule {
|
|||||||
AUTHORIZATION,
|
AUTHORIZATION,
|
||||||
"Bearer "
|
"Bearer "
|
||||||
+ OidcTokenUtils.createOidcToken(credentialsBundle, oauthClientId));
|
+ 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
|
// needed to support some nomulus commands like updating premium lists that take
|
||||||
// a lot of time to complete.
|
// a lot of time to complete. See
|
||||||
// See
|
|
||||||
// https://developers.google.com/api-client-library/java/google-api-java-client/errors
|
// https://developers.google.com/api-client-library/java/google-api-java-client/errors
|
||||||
request.setConnectTimeout(REQUEST_TIMEOUT_MS);
|
request.setConnectTimeout(REQUEST_TIMEOUT_MS);
|
||||||
request.setReadTimeout(REQUEST_TIMEOUT_MS);
|
request.setReadTimeout(REQUEST_TIMEOUT_MS);
|
||||||
|
|||||||
@@ -47,8 +47,8 @@ import org.json.simple.JSONValue;
|
|||||||
/**
|
/**
|
||||||
* An HTTP connection to a service.
|
* 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
|
* <p>By default - connects the BACKEND service. To create a connection to another service, call the
|
||||||
* Connection to another service, call the {@link #withService} function.
|
* {@link #withService} function.
|
||||||
*/
|
*/
|
||||||
public class ServiceConnection {
|
public class ServiceConnection {
|
||||||
|
|
||||||
|
|||||||
@@ -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.Action.Method.POST;
|
||||||
import static google.registry.request.RequestParameters.PARAM_TLDS;
|
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.ImmutableList;
|
||||||
import com.google.common.collect.ImmutableSet;
|
import com.google.common.collect.ImmutableSet;
|
||||||
import google.registry.model.EppResourceUtils;
|
import google.registry.model.EppResourceUtils;
|
||||||
@@ -31,7 +30,6 @@ import google.registry.request.Action;
|
|||||||
import google.registry.request.Parameter;
|
import google.registry.request.Parameter;
|
||||||
import google.registry.request.auth.Auth;
|
import google.registry.request.auth.Auth;
|
||||||
import google.registry.util.Clock;
|
import google.registry.util.Clock;
|
||||||
import google.registry.util.NonFinalForTesting;
|
|
||||||
import jakarta.inject.Inject;
|
import jakarta.inject.Inject;
|
||||||
|
|
||||||
/** An action that lists domains, for use by the {@code nomulus list_domains} command. */
|
/** 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)
|
auth = Auth.AUTH_ADMIN)
|
||||||
public final class ListDomainsAction extends ListObjectsAction<Domain> {
|
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";
|
public static final String PATH = "/_dr/admin/list/domains";
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
-- monthly GKE logs, searching for all create commands and associating
|
-- monthly GKE logs, searching for all create commands and associating
|
||||||
-- them with their corresponding registrars.
|
-- 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
|
--google.registry.flows.FlowReporter
|
||||||
-- recordToLogs: FLOW-LOG-SIGNATURE-METADATA:
|
-- recordToLogs: FLOW-LOG-SIGNATURE-METADATA:
|
||||||
--{"serverTrid":"oNwL2J2eRya7bh7c9oHIzg==-2360a","clientId":"ipmirror"
|
--{"serverTrid":"oNwL2J2eRya7bh7c9oHIzg==-2360a","clientId":"ipmirror"
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ class PersistenceModuleTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void appengineIsolation() {
|
void connectionIsolation() {
|
||||||
assertThat(PersistenceModule.provideDefaultDatabaseConfigs().get(Environment.ISOLATION))
|
assertThat(PersistenceModule.provideDefaultDatabaseConfigs().get(Environment.ISOLATION))
|
||||||
.isEqualTo(TransactionIsolationLevel.TRANSACTION_SERIALIZABLE.name());
|
.isEqualTo(TransactionIsolationLevel.TRANSACTION_SERIALIZABLE.name());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,12 +52,12 @@ public final class RegistryTestServerMain {
|
|||||||
|
|
||||||
@Parameter(
|
@Parameter(
|
||||||
names = "--login_email",
|
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";
|
private String loginEmail = "Marla.Singer@crr.com";
|
||||||
|
|
||||||
@Parameter(
|
@Parameter(
|
||||||
names = "--login_is_admin",
|
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)
|
arity = 1)
|
||||||
private boolean loginIsAdmin = true;
|
private boolean loginIsAdmin = true;
|
||||||
|
|
||||||
@@ -153,7 +153,6 @@ public final class RegistryTestServerMain {
|
|||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
server.stop();
|
server.stop();
|
||||||
// appEngine.tearDown();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -102,29 +102,6 @@ class ListDomainsActionTest extends ListActionTestCase {
|
|||||||
"^example2.foo$");
|
"^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
|
@Test
|
||||||
void testRun_twoLinesWithIdOnlyNoHeader() {
|
void testRun_twoLinesWithIdOnlyNoHeader() {
|
||||||
action.tlds = ImmutableSet.of("foo");
|
action.tlds = ImmutableSet.of("foo");
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
-- monthly GKE logs, searching for all create commands and associating
|
-- monthly GKE logs, searching for all create commands and associating
|
||||||
-- them with their corresponding registrars.
|
-- 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
|
--google.registry.flows.FlowReporter
|
||||||
-- recordToLogs: FLOW-LOG-SIGNATURE-METADATA:
|
-- recordToLogs: FLOW-LOG-SIGNATURE-METADATA:
|
||||||
--{"serverTrid":"oNwL2J2eRya7bh7c9oHIzg==-2360a","clientId":"ipmirror"
|
--{"serverTrid":"oNwL2J2eRya7bh7c9oHIzg==-2360a","clientId":"ipmirror"
|
||||||
|
|||||||
@@ -3,6 +3,5 @@ handlers = java.util.logging.ConsoleHandler
|
|||||||
|
|
||||||
google.registry.level = FINE
|
google.registry.level = FINE
|
||||||
|
|
||||||
com.google.appengine.api.taskqueue.dev.level = WARNING
|
|
||||||
com.google.apphosting.utils.config.level = WARNING
|
com.google.apphosting.utils.config.level = WARNING
|
||||||
org.quartz.level = WARNING
|
org.quartz.level = WARNING
|
||||||
|
|||||||
@@ -4,8 +4,4 @@
|
|||||||
<Configure id="wac" class="org.eclipse.jetty.ee10.webapp.WebAppContext">
|
<Configure id="wac" class="org.eclipse.jetty.ee10.webapp.WebAppContext">
|
||||||
<Set name="contextPath">/</Set>
|
<Set name="contextPath">/</Set>
|
||||||
<Set name="war">./webapps/nomulus.war</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>
|
</Configure>
|
||||||
|
|||||||
+2
-2
@@ -54,8 +54,8 @@ import javax.net.ssl.SSLSession;
|
|||||||
* <p>The ssl handler added can require client authentication, but it uses an {@link
|
* <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
|
* 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
|
* 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
|
* certificate hash will be passed along to the service as an HTTP header for verification (not
|
||||||
* this handler).
|
* handled by this handler).
|
||||||
*/
|
*/
|
||||||
@Sharable
|
@Sharable
|
||||||
public class SslServerInitializer<C extends Channel> extends ChannelInitializer<C> {
|
public class SslServerInitializer<C extends Channel> extends ChannelInitializer<C> {
|
||||||
|
|||||||
@@ -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
|
* <p>This protocol is associated to a {@link NioSocketChannel} established by the proxy
|
||||||
* connecting to a remote peer.
|
* connecting to a remote peer.
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ import java.util.Queue;
|
|||||||
import java.util.concurrent.TimeoutException;
|
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()} }.
|
* ProxyModule.ProxyComponent#protocols()} }.
|
||||||
*/
|
*/
|
||||||
public class ProxyServer implements Runnable {
|
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.
|
* 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
|
* <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
|
* is closed. The outbound connection to Nomulus is *not* guaranteed to persist. In case that
|
||||||
* outbound connection closes but the inbound connection is still active, the listener calls
|
* 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
|
* 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.
|
* so that the inbound channel knows to relay to the new outbound channel.
|
||||||
*/
|
*/
|
||||||
@@ -226,7 +226,7 @@ public class ProxyServer implements Runnable {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} 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.
|
// inbound connection as well.
|
||||||
logger.atSevere().withCause(future.cause()).log(
|
logger.atSevere().withCause(future.cause()).log(
|
||||||
"Cannot connect to relay channel for %s channel: %s.",
|
"Cannot connect to relay channel for %s channel: %s.",
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ gcpScopes:
|
|||||||
# Cloud KMS and Stackdriver Monitoring APIs.
|
# Cloud KMS and Stackdriver Monitoring APIs.
|
||||||
- https://www.googleapis.com/auth/cloud-platform
|
- 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.
|
# to authenticate.
|
||||||
- https://www.googleapis.com/auth/userinfo.email
|
- https://www.googleapis.com/auth/userinfo.email
|
||||||
|
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ public class BackendMetricsHandler extends ChannelDuplexHandler {
|
|||||||
private Channel relayedChannel;
|
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
|
* <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
|
* for pipelining, in which a client can sent multiple requests without waiting for each
|
||||||
@@ -123,7 +123,7 @@ public class BackendMetricsHandler extends ChannelDuplexHandler {
|
|||||||
.addListener(
|
.addListener(
|
||||||
future -> {
|
future -> {
|
||||||
if (future.isSuccess()) {
|
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);
|
metrics.requestSent(relayedProtocolName, clientCertHash, bytes);
|
||||||
requestSentTimeQueue.add(clock.nowUtc());
|
requestSentTimeQueue.add(clock.nowUtc());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ public abstract class HttpsRelayServiceHandler extends ByteToMessageCodec<FullHt
|
|||||||
super.write(ctx, msg, promise);
|
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 {
|
public static class NonOkHttpResponseException extends Exception {
|
||||||
|
|
||||||
private static final long serialVersionUID = 5340993059579288708L;
|
private static final long serialVersionUID = 5340993059579288708L;
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# 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
|
# artifacts for all services to a designated location, and then creates a
|
||||||
# tarball from there.
|
# tarball from there.
|
||||||
|
|
||||||
|
|||||||
@@ -30,8 +30,6 @@ import (
|
|||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
var gke bool = false
|
|
||||||
|
|
||||||
var projectName string
|
var projectName string
|
||||||
|
|
||||||
var baseDomain string
|
var baseDomain string
|
||||||
@@ -190,18 +188,8 @@ func (manager TasksSyncManager) getArgs(task Task, operationType string) []strin
|
|||||||
description = strings.ReplaceAll(description, "\n", " ")
|
description = strings.ReplaceAll(description, "\n", " ")
|
||||||
|
|
||||||
var service = "backend"
|
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
|
var uri string
|
||||||
if gke {
|
uri = fmt.Sprintf("https://%s.%s%s", service, baseDomain, strings.TrimSpace(task.URL))
|
||||||
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))
|
|
||||||
}
|
|
||||||
|
|
||||||
args := []string{
|
args := []string{
|
||||||
"--project", projectName,
|
"--project", projectName,
|
||||||
@@ -342,8 +330,7 @@ func getExistingEntries(cmd *exec.Cmd) ExistingEntries {
|
|||||||
func main() {
|
func main() {
|
||||||
if len(os.Args) < 4 || os.Args[1] == "" || os.Args[2] == "" || os.Args[3] == "" {
|
if len(os.Args) < 4 || os.Args[1] == "" || os.Args[2] == "" || os.Args[3] == "" {
|
||||||
panic("Error - Invalid Parameters.\n" +
|
panic("Error - Invalid Parameters.\n" +
|
||||||
"Required params: 1 - Nomulus config YAML path; 2 - config XML path; 3 - project name;\n" +
|
"Required params: 1 - Nomulus config YAML path; 2 - config XML path; 3 - project name;\n")
|
||||||
"Optional params: 5 - [--gke]")
|
|
||||||
}
|
}
|
||||||
// Nomulus YAML config file path, used to extract OAuth client ID.
|
// Nomulus YAML config file path, used to extract OAuth client ID.
|
||||||
nomulusConfigFileLocation := os.Args[1]
|
nomulusConfigFileLocation := os.Args[1]
|
||||||
@@ -351,11 +338,6 @@ func main() {
|
|||||||
configFileLocation := os.Args[2]
|
configFileLocation := os.Args[2]
|
||||||
// Project name where to submit the tasks
|
// Project name where to submit the tasks
|
||||||
projectName = os.Args[3]
|
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)
|
log.Default().Println("YAML Filepath " + nomulusConfigFileLocation)
|
||||||
yamlFile, err := os.Open(nomulusConfigFileLocation)
|
yamlFile, err := os.Open(nomulusConfigFileLocation)
|
||||||
|
|||||||
@@ -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'
|
|
||||||
@@ -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'
|
|
||||||
@@ -198,7 +198,6 @@ artifacts:
|
|||||||
- 'core/src/main/java/google/registry/config/files/tasks/cloud-scheduler-tasks-*.xml'
|
- 'core/src/main/java/google/registry/config/files/tasks/cloud-scheduler-tasks-*.xml'
|
||||||
- 'release/cloudbuild-sync-and-tag.yaml'
|
- 'release/cloudbuild-sync-and-tag.yaml'
|
||||||
- 'release/cloudbuild-deploy-*.yaml'
|
- 'release/cloudbuild-deploy-*.yaml'
|
||||||
- 'release/cloudbuild-delete-*.yaml'
|
|
||||||
- 'release/cloudbuild-renew-prober-certs-*.yaml'
|
- 'release/cloudbuild-renew-prober-certs-*.yaml'
|
||||||
- 'release/cloudbuild-schema-deploy-*.yaml'
|
- 'release/cloudbuild-schema-deploy-*.yaml'
|
||||||
- 'release/cloudbuild-schema-verify-*.yaml'
|
- 'release/cloudbuild-schema-verify-*.yaml'
|
||||||
|
|||||||
@@ -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-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-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-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/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/GCP_PROJECT/${PROJECT_ID}/ proxy/kubernetes/proxy-*.yaml
|
||||||
sed -i s/'$${TAG_NAME}'/${TAG_NAME}/g release/cloudbuild-sync-and-tag.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
|
for environment in alpha crash qa sandbox production; do
|
||||||
sed s/'$${_ENV}'/${environment}/g release/cloudbuild-deploy-gke.yaml \
|
sed s/'$${_ENV}'/${environment}/g release/cloudbuild-deploy-gke.yaml \
|
||||||
> release/cloudbuild-deploy-gke-${environment}.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 \
|
sed s/'$${_ENV}'/${environment}/g release/cloudbuild-restart-proxies.yaml \
|
||||||
> release/cloudbuild-restart-proxies-${environment}.yaml
|
> release/cloudbuild-restart-proxies-${environment}.yaml
|
||||||
sed s/'$${_ENV}'/${environment}/g release/cloudbuild-restart-proxies.yaml | \
|
sed s/'$${_ENV}'/${environment}/g release/cloudbuild-restart-proxies.yaml | \
|
||||||
|
|||||||
@@ -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.
|
|
||||||
@@ -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
|
|
||||||
@@ -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()
|
|
||||||
@@ -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()
|
|
||||||
@@ -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()
|
|
||||||
@@ -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]
|
|
||||||
@@ -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()
|
|
||||||
@@ -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)
|
|
||||||
@@ -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()
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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()
|
|
||||||
@@ -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))
|
|
||||||
@@ -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"
|
|
||||||
@@ -32,7 +32,7 @@ import javax.annotation.Nullable;
|
|||||||
/**
|
/**
|
||||||
* JUL formatter that formats log messages in a single-line JSON that Stackdriver logging can parse.
|
* 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
|
* logging agent and automatically ingested by Stackdriver. Certain fields (see below) in the JSON
|
||||||
* will be converted to the corresponding <a
|
* will be converted to the corresponding <a
|
||||||
* href="https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry">{@code Log Entry}</a>
|
* href="https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry">{@code Log Entry}</a>
|
||||||
|
|||||||
@@ -36,9 +36,9 @@ public final class ProxyHttpHeaders {
|
|||||||
/**
|
/**
|
||||||
* Fallback HTTP header name used to pass the client IP address from the proxy to Nomulus.
|
* 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
|
* <p>Note that Java 17's servlet implementation may inject some seemingly unrelated addresses
|
||||||
* unrelated addresses into this header. We only use this as a fallback so the proxy can
|
* into this header. We only use this as a fallback so the proxy can transition to use the above
|
||||||
* transition to use the above header that should not be interfered with.
|
* header that should not be interfered with.
|
||||||
*/
|
*/
|
||||||
public static final String FALLBACK_IP_ADDRESS = HttpHeaders.X_FORWARDED_FOR;
|
public static final String FALLBACK_IP_ADDRESS = HttpHeaders.X_FORWARDED_FOR;
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ public final class TypeUtils {
|
|||||||
public static <T> T instantiate(Class<? extends T> clazz) {
|
public static <T> T instantiate(Class<? extends T> clazz) {
|
||||||
checkArgument(
|
checkArgument(
|
||||||
Modifier.isPublic(clazz.getModifiers()),
|
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 {
|
try {
|
||||||
return clazz.getConstructor().newInstance();
|
return clazz.getConstructor().newInstance();
|
||||||
} catch (ReflectiveOperationException e) {
|
} catch (ReflectiveOperationException e) {
|
||||||
@@ -68,7 +68,7 @@ public final class TypeUtils {
|
|||||||
public static <T, U> T instantiate(Class<? extends T> clazz, U arg) {
|
public static <T, U> T instantiate(Class<? extends T> clazz, U arg) {
|
||||||
checkArgument(
|
checkArgument(
|
||||||
Modifier.isPublic(clazz.getModifiers()),
|
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 {
|
try {
|
||||||
return clazz.getConstructor(arg.getClass()).newInstance(arg);
|
return clazz.getConstructor(arg.getClass()).newInstance(arg);
|
||||||
} catch (ReflectiveOperationException e) {
|
} catch (ReflectiveOperationException e) {
|
||||||
|
|||||||
@@ -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}
|
* <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
|
* 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
|
* {@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
|
* environment-specific settings such as the project ID. The environment-specific configuration can
|
||||||
* configuration can be blank, but it must exist.
|
* be blank, but it must exist.
|
||||||
*/
|
*/
|
||||||
public final class YamlUtils {
|
public final class YamlUtils {
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user