From 3a8c245641cbb8b353d13ec5f34aaa06ef8f5da9 Mon Sep 17 00:00:00 2001 From: Weimin Yu Date: Wed, 7 Apr 2021 16:17:51 -0400 Subject: [PATCH] Add a wipeout action for Datastore in QA (#1064) * Add a wipeout action for Datastore in QA --- .../batch/WipeoutDatastoreAction.java | 115 ++++++++++++++++++ .../env/common/backend/WEB-INF/web.xml | 6 + .../registry/env/qa/default/WEB-INF/cron.xml | 9 ++ .../backend/BackendRequestComponent.java | 3 + .../batch/WipeOutDatastoreActionTest.java | 98 +++++++++++++++ .../module/backend/backend_routing.txt | 1 + 6 files changed, 232 insertions(+) create mode 100644 core/src/main/java/google/registry/batch/WipeoutDatastoreAction.java create mode 100644 core/src/test/java/google/registry/batch/WipeOutDatastoreActionTest.java diff --git a/core/src/main/java/google/registry/batch/WipeoutDatastoreAction.java b/core/src/main/java/google/registry/batch/WipeoutDatastoreAction.java new file mode 100644 index 000000000..4db43dfb9 --- /dev/null +++ b/core/src/main/java/google/registry/batch/WipeoutDatastoreAction.java @@ -0,0 +1,115 @@ +// Copyright 2021 The Nomulus Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package google.registry.batch; + +import static com.google.common.net.MediaType.PLAIN_TEXT_UTF_8; +import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN; +import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR; +import static javax.servlet.http.HttpServletResponse.SC_OK; + +import com.google.api.services.dataflow.Dataflow; +import com.google.api.services.dataflow.model.LaunchFlexTemplateParameter; +import com.google.api.services.dataflow.model.LaunchFlexTemplateRequest; +import com.google.api.services.dataflow.model.LaunchFlexTemplateResponse; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.flogger.FluentLogger; +import google.registry.config.RegistryConfig.Config; +import google.registry.request.Action; +import google.registry.request.Response; +import google.registry.request.auth.Auth; +import javax.inject.Inject; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; + +/** + * Wipes out all Cloud Datastore data in a Nomulus GCP environment. + * + *

This class is created for the QA environment, where migration testing with production data + * will happen. A regularly scheduled wipeout is a prerequisite to using production data there. + */ +@Action( + service = Action.Service.BACKEND, + path = "/_dr/task/wipeOutDatastore", + auth = Auth.AUTH_INTERNAL_OR_ADMIN) +public class WipeoutDatastoreAction implements Runnable { + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + + private static final String PIPELINE_NAME = "bulk_delete_datastore_pipeline"; + + // As a short-lived class, hardcode allowed projects here instead of using config files. + private static final ImmutableSet ALLOWED_PROJECTS = + ImmutableSet.of("domain-registry-qa"); + + private final String projectId; + private final String jobRegion; + private final Response response; + private final Dataflow dataflow; + private final String stagingBucketUrl; + + @Inject + WipeoutDatastoreAction( + @Config("projectId") String projectId, + @Config("defaultJobRegion") String jobRegion, + @Config("beamStagingBucketUrl") String stagingBucketUrl, + Response response, + Dataflow dataflow) { + this.projectId = projectId; + this.jobRegion = jobRegion; + this.stagingBucketUrl = stagingBucketUrl; + this.response = response; + this.dataflow = dataflow; + } + + @Override + public void run() { + response.setContentType(PLAIN_TEXT_UTF_8); + + if (!ALLOWED_PROJECTS.contains(projectId)) { + response.setStatus(SC_FORBIDDEN); + response.setPayload("Wipeout is not allowed in " + projectId); + return; + } + + try { + LaunchFlexTemplateParameter parameters = + new LaunchFlexTemplateParameter() + // Job name must be unique and in [-a-z0-9]. + .setJobName( + "bulk-delete-datastore-" + + DateTime.now(DateTimeZone.UTC).toString("yyyy-MM-dd'T'HH-mm-ss'Z'")) + .setContainerSpecGcsPath( + String.format("%s/%s_metadata.json", stagingBucketUrl, PIPELINE_NAME)) + .setParameters(ImmutableMap.of("kindsToDelete", "*")); + LaunchFlexTemplateResponse launchResponse = + dataflow + .projects() + .locations() + .flexTemplates() + .launch( + projectId, + jobRegion, + new LaunchFlexTemplateRequest().setLaunchParameter(parameters)) + .execute(); + response.setStatus(SC_OK); + response.setPayload("Launched " + launchResponse.getJob().getName()); + } catch (Exception e) { + String msg = String.format("Failed to launch %s.", PIPELINE_NAME); + logger.atSevere().withCause(e).log(msg); + response.setStatus(SC_INTERNAL_SERVER_ERROR); + response.setPayload(msg); + } + } +} diff --git a/core/src/main/java/google/registry/env/common/backend/WEB-INF/web.xml b/core/src/main/java/google/registry/env/common/backend/WEB-INF/web.xml index 76e88279b..85ddceac8 100644 --- a/core/src/main/java/google/registry/env/common/backend/WEB-INF/web.xml +++ b/core/src/main/java/google/registry/env/common/backend/WEB-INF/web.xml @@ -385,6 +385,12 @@ /_dr/task/wipeOutCloudSql + + + backend-servlet + /_dr/task/wipeOutDatastore + + diff --git a/core/src/main/java/google/registry/env/qa/default/WEB-INF/cron.xml b/core/src/main/java/google/registry/env/qa/default/WEB-INF/cron.xml index 179e2d936..783af1db9 100644 --- a/core/src/main/java/google/registry/env/qa/default/WEB-INF/cron.xml +++ b/core/src/main/java/google/registry/env/qa/default/WEB-INF/cron.xml @@ -91,4 +91,13 @@ backend + + + + This job runs an action that deletes all data in Cloud Datastore. + + every saturday 03:07 + backend + + diff --git a/core/src/main/java/google/registry/module/backend/BackendRequestComponent.java b/core/src/main/java/google/registry/module/backend/BackendRequestComponent.java index ef94a30aa..756b90ae7 100644 --- a/core/src/main/java/google/registry/module/backend/BackendRequestComponent.java +++ b/core/src/main/java/google/registry/module/backend/BackendRequestComponent.java @@ -31,6 +31,7 @@ import google.registry.batch.RelockDomainAction; import google.registry.batch.ResaveAllEppResourcesAction; import google.registry.batch.ResaveEntityAction; import google.registry.batch.WipeOutCloudSqlAction; +import google.registry.batch.WipeoutDatastoreAction; import google.registry.cron.CommitLogFanoutAction; import google.registry.cron.CronModule; import google.registry.cron.TldFanoutAction; @@ -208,6 +209,8 @@ interface BackendRequestComponent { WipeOutCloudSqlAction wipeOutCloudSqlAction(); + WipeoutDatastoreAction wipeoutDatastoreAction(); + @Subcomponent.Builder abstract class Builder implements RequestComponentBuilder { diff --git a/core/src/test/java/google/registry/batch/WipeOutDatastoreActionTest.java b/core/src/test/java/google/registry/batch/WipeOutDatastoreActionTest.java new file mode 100644 index 000000000..3af391c2a --- /dev/null +++ b/core/src/test/java/google/registry/batch/WipeOutDatastoreActionTest.java @@ -0,0 +1,98 @@ +// Copyright 2021 The Nomulus Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package google.registry.batch; + +import static com.google.common.truth.Truth.assertThat; +import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN; +import static org.apache.http.HttpStatus.SC_INTERNAL_SERVER_ERROR; +import static org.apache.http.HttpStatus.SC_OK; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import com.google.api.services.dataflow.Dataflow; +import com.google.api.services.dataflow.model.Job; +import com.google.api.services.dataflow.model.LaunchFlexTemplateRequest; +import com.google.api.services.dataflow.model.LaunchFlexTemplateResponse; +import google.registry.testing.FakeResponse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +/** Unit tests for {@link WipeoutDatastoreAction}. */ +@ExtendWith(MockitoExtension.class) +class WipeOutDatastoreActionTest { + + @Mock private Dataflow dataflow; + @Mock private Dataflow.Projects projects; + @Mock private Dataflow.Projects.Locations locations; + @Mock private Dataflow.Projects.Locations.FlexTemplates flexTemplates; + @Mock private Dataflow.Projects.Locations.FlexTemplates.Launch launch; + private LaunchFlexTemplateResponse launchResponse = + new LaunchFlexTemplateResponse().setJob(new Job()); + + private FakeResponse response = new FakeResponse(); + + @BeforeEach + void beforeEach() throws Exception { + lenient().when(dataflow.projects()).thenReturn(projects); + lenient().when(projects.locations()).thenReturn(locations); + lenient().when(locations.flexTemplates()).thenReturn(flexTemplates); + lenient() + .when(flexTemplates.launch(anyString(), anyString(), any(LaunchFlexTemplateRequest.class))) + .thenReturn(launch); + lenient().when(launch.execute()).thenReturn(launchResponse); + } + + @Test + void run_projectNotAllowed() { + WipeoutDatastoreAction action = + new WipeoutDatastoreAction( + "domain-registry", "us-central1", "gs://some-bucket", response, dataflow); + action.run(); + assertThat(response.getStatus()).isEqualTo(SC_FORBIDDEN); + verifyNoInteractions(dataflow); + } + + @Test + void run_projectAllowed() throws Exception { + WipeoutDatastoreAction action = + new WipeoutDatastoreAction( + "domain-registry-qa", "us-central1", "gs://some-bucket", response, dataflow); + action.run(); + assertThat(response.getStatus()).isEqualTo(SC_OK); + verify(launch, times(1)).execute(); + verifyNoMoreInteractions(launch); + } + + @Test + void run_failure() throws Exception { + when(launch.execute()).thenThrow(new RuntimeException()); + WipeoutDatastoreAction action = + new WipeoutDatastoreAction( + "domain-registry-qa", "us-central1", "gs://some-bucket", response, dataflow); + action.run(); + assertThat(response.getStatus()).isEqualTo(SC_INTERNAL_SERVER_ERROR); + verify(launch, times(1)).execute(); + verifyNoMoreInteractions(launch); + } +} diff --git a/core/src/test/resources/google/registry/module/backend/backend_routing.txt b/core/src/test/resources/google/registry/module/backend/backend_routing.txt index abcc34c4a..bb9c4c485 100644 --- a/core/src/test/resources/google/registry/module/backend/backend_routing.txt +++ b/core/src/test/resources/google/registry/module/backend/backend_routing.txt @@ -44,3 +44,4 @@ PATH CLASS METHOD /_dr/task/updateSnapshotView UpdateSnapshotViewAction POST n INTERNAL,API APP ADMIN /_dr/task/uploadDatastoreBackup UploadDatastoreBackupAction POST n INTERNAL,API APP ADMIN /_dr/task/wipeOutCloudSql WipeOutCloudSqlAction GET n INTERNAL,API APP ADMIN +/_dr/task/wipeOutDatastore WipeoutDatastoreAction GET n INTERNAL,API APP ADMIN