Compare commits

...
Author SHA1 Message Date
Rachel GuanandGitHub 43074ea32f Send expiring notification emails to admins if no tech emails are on file (#1387)
* Send emails to admin if tech emails are not present

* Improve test cases and comments
2021-10-21 12:59:31 -04:00
Weimin YuandGitHub 1a4a31569e Alt entity model for fast JPA bulk query (#1398)
* Alt entity model for fast JPA bulk query

Defined an alternative JPA entity model that allows fast bulk loading of
multi-level entities, DomainBase and DomainHistory. The idea is to bulk
the base table as well as the child tables separately, and assemble them
into the target entity in memory in a pipeline.

For DomainBase:

- Defined a DomainBaseLite class that models the "Domain" table only.

- Defined a DomainHost class that models the "DomainHost" table
  (nsHosts field).

- Exposed ID fields in GracePeriod so that they can be mapped to domains
  after being loaded into memory.

For DomainHistory:

- Defined a DomainHistoryLite class that models the "DomainHistory"
  table only.

- Defined a DomainHistoryHost class that models its namesake table.

- Exposed ID fields in GracePeriodHistory and DomainDsDataHistory
  classes so that they can be mapped to DomainHistory after being
  loaded into memory.

In PersistenceModule, provisioned a JpaTransactionManager that uses
the alternative entity model.

Also added a pipeline option that specifies which JpaTransactionManager
to use in a pipeline.
2021-10-20 16:48:56 -04:00
gbrodmanandGitHub c7f50dae92 Use READ_COMMITTED serialization level in CreateSyntheticHEA (#1395)
I observed an instance in which a couple queries from this action were,
for whatever reason, hanging around as idle for >30 minutes. Assuming
the behavior that we saw before where "an open idle serializable
transaction means all pg read-locks stick around forever" still holds,
that's the reason why the amount of read-locks in use spirals out of
control.

I'm not sure why those queries aren't timing out, but that's a separate
issue.
2021-10-19 11:36:15 -04:00
Michael MullerandGitHub 7344c424d1 Fix problems with the format tasks (#1390)
* Fix problems with the format tasks

The format check is using python2, and if "python" doesn't exist on the path
(or isn't python 2, or there is any other error in the python code or in the
shell script...) the format check just succeeds.

This change:
- Refactors out the gradle code that finds a python3 executable and use it
  to get the python executable to be used for the format check.
- Upgrades google-java-format-diff.py to python3 and removes #! line.
- Fixes shell script to ensure that failures are propagated.
- Suppresses error output when checking for python commands.

Tested:
- verified that python errors cause the build to fail
- verified that introducing a bad format diff causes check to fail
- verified that javaIncrementalFormatDryRun shows the diffs that would be
  introduced.
- verified that javaIncrementalFormatApply reformats a file.
- verified that well formatted code passes the format check.
- verified that an invalid or missing PYTHON env var causes
  google-java-format-git-diff.sh to fail with the appropriate error.

* Fix presubmit issues

Omit the format presubmit when not in a git repo and remove unused "string"
import.
2021-10-18 08:10:09 -04:00
gbrodmanandGitHub 969fa2b68c Fix weird flake (#1394) 2021-10-15 18:00:46 -04:00
gbrodmanandGitHub 9a569198fb Ignore class visibility in EntityTest (#1389) 2021-10-15 17:08:51 -04:00
gbrodmanandGitHub 8a53edd57b Use multiple transactions in IcannReportingUploadAction (#1386)
Relevant error log message: https://pantheon.corp.google.com/logs/viewer?project=domain-registry&minLogLevel=0&expandAll=false&timestamp=2021-10-11T15:28:01.047783000Z&customFacets=&limitCustomFacetWidth=true&dateRangeEnd=2021-10-11T20:51:40.591Z&interval=PT1H&resource=gae_app&logName=projects%2Fdomain-registry%2Flogs%2Fappengine.googleapis.com%252Frequest_log&scrollTimestamp=2021-10-11T15:10:23.174336000Z&filters=text:icannReportingUpload&dateRangeUnbound=backwardInTime&advancedFilter=resource.type%3D%22gae_app%22%0AlogName%3D%22projects%2Fdomain-registry%2Flogs%2Fappengine.googleapis.com%252Frequest_log%22%0A%22icannReportingUpload%22%0Aoperation.id%3D%22616453df00ff02a873d26cedb40001737e646f6d61696e2d726567697374727900016261636b656e643a6e6f6d756c75732d76303233000100%22

note the "invalid handle" bit

From https://cloud.google.com/datastore/docs/concepts/transactions:
"Transactions expire after 270 seconds or if idle for 60 seconds."

From b/202309933: "There is a 60 second timeout on Datastore operations
after which they will automatically rollback and the handles become
invalid."

From the logs we can see that the action is lasting significantly longer
than 270 seconds -- roughly 480 seconds in the linked log (more or
less). My running theory is that ICANN is, for some reason, now being
significantly more slow to respond than they used to be. Some uploads in
the log linked above are taking upwards of 10 seconds, especially when
they have to retry. Because we have >=45 TLDs, it's not surprising that
the action is taking >400 seconds to run.

The fix here is to perform each per-TLD operation in its own
transaction. The only reason why we need the transactions is for the
cursors anyway, and we can just grab and store those at the beginning of
the transaction.
2021-10-15 15:38:37 -04:00
Lai JiangandGitHub d25d4073f5 Add a beam pipeline to create synthetic history entries in SQL (#1383)
* Add a beam pipeline to create synthetic history entries in SQL

The logic is mostly lifted from CreateSyntheticHistoryEntriesAction. We
do not need to test for the existence of an embedded EPP resource in the
history entry before create a synthetic one because after
InitSqlPipeline runs it is guaranteed that no embedded resource exists.
2021-10-15 14:51:01 -04:00
Ben McIlwainandGitHub 6ffe84e93d Add a scrap command to hard-delete a host resource (#1391) 2021-10-15 12:28:18 -04:00
Ben McIlwainandGitHub a451524010 Add tests for obscure hostname canonicalization rule (#1388)
Also correctly configures Gradle for the util subproject (it wasn't possible to
run tests in IntelliJ without these changes).
2021-10-14 14:53:28 -04:00
Rachel GuanandGitHub bb8988ee4e Set payload in success response after sending notification emails (#1377)
* Set payload in success response after sending expiring certificate notification emails

* Modify log message and test cases for run() in sendExpiringCertificateNotificationEmailAction
2021-10-13 15:58:25 -04:00
Rachel GuanandGitHub 2aff72b3b6 Add reason and requestedByRegistrar to domain renew flow (#1378)
* Resolve merge conflict

* Include reason and requestedByRegistrar in URS test file

* Modify test cases for new parameters in renew flow

* Add reason and registrar_request to renew domain command

* Update comments for new params in renew flow

* Make changes based on feedback
2021-10-13 11:41:02 -04:00
Weimin YuandGitHub 35fd61f771 Update parameter to Datastore wipe pipeline (#1385)
* Update parameter to Datastore wipe pipeline

Add the newly required RegistryEnvironment parameter to
BulkDeleteDatastorePipeline.

Remove the nullable annotation for this parameter in options
class.

Update metadata files regarding this parameter.
2021-10-11 17:31:50 -04:00
Michael MullerandGitHub 13cb17e9a4 Implement several fixes affecting test flakiness (#1379)
* Implement several fixes affecting test flakiness

- Continued to do transaction manager cleanups on reply failure (lack of this
  may be causing cascading failures.
- Fix UpdateDomainCommandTest's output check (the test was checking for error
  output in standard error, but the command writes its output to the logs.
  Apparently, these may or may not be reflected in standard error depending on
  current global state)
- Remove unnecessary locking and incorrect comment in CommandTestCase.  The
  JUnit tests are not run in parallel in the same JVM and, in general, there
  are much bigger obstacles to this than standard output stream locking.

* Fix bad log message check
2021-10-11 12:54:03 -04:00
Ben McIlwainandGitHub 4f1c317bbc Revert update auto timestamp non-transactional fallback (#1380)
This was added recently in PR #1341 as an attempted fix for our test flakiness,
but it turns out that it didn't address the root issue (whereas PR #1361
did). So this removes the fallback, as there's no reason this should ever be
called outside of a transactional context.
2021-10-08 16:44:45 -04:00
gbrodmanandGitHub c8aa32ef05 Include more info in host/domain name failures (#1346)
We're seeing some of these in CreateSyntheticHistoryEntriesAction and I
can't tell why from the logs (it doesn't appear to print the repo ID or
domain/host name)
2021-10-08 15:17:22 -04:00
gbrodmanandGitHub 95a1bbf66a Temporarily disable SQL->DS replay in all tests (#1363) 2021-10-08 14:15:57 -04:00
Rachel GuanandGitHub 23aa16469e Add WipeOutContactHistoryPiiAction to prod (#1356) 2021-10-08 11:46:26 -04:00
Ben McIlwainandGitHub 0277c5c25a Add TmOverrideExtension for more safe TM overrides in tests (#1382)
* Add TmOverrideExtension for more safe TM overrides in tests

This is safer to use than calling setTmForTest() directly because this extension
also handles the corresponding call to removeTmOverrideForTest() automatically,
the forgetting of which has been a source of test flakiness/instability in the
past.

There are now broadly two ways to get tests to run in JPA: either use
DualDatabaseTest, an AppEngineExtension, and the corresponding JPA-specific
@Test annotations, OR use this override alongside a
JpaTransactionManagerExtension.
2021-10-07 19:26:25 -04:00
Ben McIlwainandGitHub b1b0589281 Elaborate on database read-only error message (#1355)
* Elaborate on database read-only error message
2021-10-07 13:25:24 -04:00
Ben McIlwainandGitHub 28628564cc Set response payload when wiping out contact history PII (#1376)
Also uses smaller batches in tests so that they don't take so long.
2021-10-07 12:43:41 -04:00
Michael MullerandGitHub 835f93f555 Add a reference to RDAP conformance checker (#1358)
* Add a reference to RDAP conformance checker

Make a note of the RDAP conformance checker for the next time that we deal
with the RDAP code - would be nice to have this in the test suite.

* Reformat comment
2021-10-07 12:34:41 -04:00
Ben McIlwainandGitHub 276c188e9d Canonicalize domain/host names in initial import script (#1347)
* Canonicalize domain/host names in initial import script

* Add tests and make reduce some method visibility
2021-10-07 11:59:46 -04:00
Rachel GuanandGitHub 34ecc6fbe7 Add new parameter renew_one_year to URS (#1364)
* Add autorenews to URS (#1343)

* Add autorenews to URS

* Add autorenews to existing xml files for test cases

* Harmonize domain.get() in existing code

* Fix typo in test case name

* Modify existing test helper method to allow testing with different domain bases
2021-10-06 20:40:43 -04:00
gbrodmanandGitHub 0f4156c563 Use a more efficient query to find resources in histories (#1354) 2021-10-06 15:20:31 -04:00
Michael MullerandGitHub e1827ab939 Defer python discovery until presubmit task (#1352)
* Customize LGTM build command

Our presubmit requires a version of python that is more recent than what
lgtm.com's build environments have installed.  Instead of trying to upgrade
them or downgrade our python version, just do the steps of the build that LGTM
needs (i.e. just build the main classes and test classes).
2021-10-06 10:09:13 -04:00
Ben McIlwainandGitHub 51b2887709 Fix BigQuery data set name handling in activity reporting (#1361)
* Fix BigQuery data set name handling in activity reporting

This is not a constant (as it depends on runtime state), so it can't be named
using UPPER_SNAKE_CASE. Additionally, it's not good practice to use field
initialization when there's logic depending on runtime state involved. So this
PR changes the class to use constructor injection and moves the logic into the
constructor.

* Add fix for ICANN reporting provide

* Extract out ICANN reporting data set

* Inject TransactionManager

* Make TransactionInfo static (per Mike)

* Use ofyTm() in BackupTestStore

* Revert extraneous formatting

* Use auditedOfy in CommitLogMutationTest
2021-10-05 15:11:03 -04:00
Lai JiangandGitHub 62eb8801c5 Finish RDE pipeline implementation in SQL mode (#1330)
This PR adds the final step in RDE pipeline (enqueueing the next action
  to Cloud Tasks) and makes some necessary changes, namely by making all
  CloudTasksUtils related classes serializable, so that they can be used
  on Beam.

<!-- Reviewable:start -->
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/google/nomulus/1330)
<!-- Reviewable:end -->
2021-10-04 21:02:44 -04:00
Lai JiangandGitHub f6920454f6 Fix the beam staging script, take 3 (#1370)
The number of arguments changed in https://github.com/google/nomulus/pull/1369, so the check needs to change as well.

<!-- Reviewable:start -->
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/google/nomulus/1370)
<!-- Reviewable:end -->
2021-10-04 16:44:32 -04:00
Lai JiangandGitHub 9103216a46 Fix beam deployment script again. (#1369)
uberjar task and uberjar name are now different (beamPipelineCommon and
beam_pipeline_common, respectively). This is more idiomatic with regard
to naming conventions but we need to take two different variables now.

<!-- Reviewable:start -->
---
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/google/nomulus/1369)
<!-- Reviewable:end -->
2021-10-04 14:23:28 -04:00
Lai JiangandGitHub c6705d1956 Fix sandbox cron (#1366)
* Fix sandbox cron

"synchronized" can only be used to specify a 24h time range that is
evenly divided by the interval value, e. g. "every 2 hours
synchronized".

* Change to a different time
2021-10-04 11:09:55 -04:00
Lai JiangandGitHub 737f65bd33 Change Beam uber jar name in Nomulu release GCB config (#1367)
The uber jar name was changed in #1351.

<!-- Reviewable:start -->
---
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/google/nomulus/1367)
<!-- Reviewable:end -->
2021-10-04 10:47:27 -04:00
Lai JiangandGitHub c8caa8f80b Remove the use of AppEngineEnvironment in Spec11Pipeline (#1365)
After #1348 it is no longer necessary to use AppEngineEnvironment in
Beam pipelines. In tests it is taken care of by the
DatastoreEntityExtension whereas on Dataflow the
RegistryPipelineWorkerInitializer does the same initialization for Ofy.
2021-10-02 19:23:09 -04:00
Rachel GuanandGitHub 65ef18052b Add autorenews to URS (#1343)
* Add autorenews to URS

* Add autorenews to existing xml files for test cases
2021-10-01 19:11:46 -04:00
Lai JiangandGitHub f7938e80f7 Streamline how to fake an App Engine environment (#1348)
Both `DatastoreEntityExtension.PlaceholderEnvironment` and `AppEngineEnvironment` does the same thing, so there is no point having both of them exist. To use `AppEngineEnvionrment` as an autoclosable requires the user to be mindful of where a fake App Engine environment is required. It is better to set this either in the `DatastoreEntityExtension` for tests, or in the worker initializer in Beam. It also makes it easier to remove the fake environment when we are completely datastore free.

Also made a change to how `IdService` allocate Ids in Beam.
<!-- Reviewable:start -->
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/google/nomulus/1348)
<!-- Reviewable:end -->
2021-10-01 16:46:46 -04:00
Lai JiangandGitHub d8b3a30a20 Rename UpdateKmsKeyringCommand (#1353)
This brings it in line with GetKeyringSecretCommand. We still need to
remove the rest of remaining Cloud KMS related code in the future.

<!-- Reviewable:start -->
---
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/google/nomulus/1353)
<!-- Reviewable:end -->
2021-10-01 16:45:45 -04:00
sarahcaseybotandGitHub 93715c6f9e Add VKey workaround to spec11 pipeline (#1339)
* Add VKey workaround to spec11 pipeline

* Parallelize entity loading
2021-10-01 15:21:16 -04:00
Rachel GuanandGitHub 90cf4519c5 Add a cron job to periodically empty out fields on deleted entities t… (#1303)
* Add a cron job to periodically empty out fields on deleted entities that are at least 18 months old

* Process ContactHistory entities via batching

* Improve test cases by not making assertions in a loop
2021-09-30 15:17:37 -04:00
Michael MullerandGitHub 3a177f36b1 Make :core:cleanTest depend on FilterTests (#1342)
* Make :core:cleanTest depend on FilterTests

The "cleanTest" target doesn't work for our specialized tests derived from
FilterTest.  Make them all explicit dependencies of cleanTest so we can reset
the tests from a single target.
2021-09-30 10:46:36 -04:00
158 changed files with 4732 additions and 1001 deletions
+40 -25
View File
@@ -200,28 +200,37 @@ rootProject.ext {
pyver = { exe ->
try {
ext.execInBash(
exe + " -c 'import sys; print(sys.hexversion)'", "/") as Integer
exe + " -c 'import sys; print(sys.hexversion)' 2>/dev/null",
"/") as Integer
} catch (org.gradle.process.internal.ExecException e) {
return -1;
}
}
// Return the path to a usable python3 executable.
getPythonExecutable = {
// Find a python version greater than 3.7.3 (this is somewhat arbitrary, we
// know we'd like at least 3.6, but 3.7.3 is the latest that ships with
// Debian so it seems like that should be available anywhere).
def MIN_PY_VER = 0x3070300
if (pyver('python') >= MIN_PY_VER) {
return 'python'
} else if (pyver('/usr/bin/python3') >= MIN_PY_VER) {
return '/usr/bin/python3'
} else {
throw new GradleException("No usable Python version found (build " +
"requires at least python 3.7.3)");
}
}
}
task runPresubmits(type: Exec) {
// Find a python version greater than 3.7.3 (this is somewhat arbitrary, we
// know we'd like at least 3.6, but 3.7.3 is the latest that ships with
// Debian so it seems like that should be available anywhere).
def MIN_PY_VER = 0x3070300
if (pyver('python') >= MIN_PY_VER) {
executable 'python'
} else if (pyver('/usr/bin/python3') >= MIN_PY_VER) {
executable '/usr/bin/python3'
} else {
throw new GradleException("No usable Python version found (build " +
"requires at least python 3.7.3)");
}
args('config/presubmits.py')
doFirst {
executable getPythonExecutable()
}
}
def javadocSource = []
@@ -435,9 +444,10 @@ rootProject.ext {
? "${rootDir}/.."
: rootDir
def formatDiffScript = "${scriptDir}/google-java-format-git-diff.sh"
def pythonExe = getPythonExecutable()
return ext.execInBash(
"${formatDiffScript} ${action}", "${workingDir}")
"PYTHON=${pythonExe} ${formatDiffScript} ${action}", "${workingDir}")
}
}
@@ -445,18 +455,23 @@ rootProject.ext {
// Note that this task checks modified Java files in the entire repository.
task javaIncrementalFormatCheck {
doLast {
def checkResult = invokeJavaDiffFormatScript("check")
if (checkResult == 'true') {
throw new IllegalStateException(
"Some Java files need to be reformatted. You may use the "
+ "'javaIncrementalFormatDryRun' task to review\n "
+ "the changes, or the 'javaIncrementalFormatApply' task "
+ "to reformat.")
} else if (checkResult != 'false') {
throw new RuntimeException(
"Failed to invoke format check script:\n" + checkResult)
// We can only do this in a git tree.
if (new File("${rootDir}/.git").exists()) {
def checkResult = invokeJavaDiffFormatScript("check")
if (checkResult == 'true') {
throw new IllegalStateException(
"Some Java files need to be reformatted. You may use the "
+ "'javaIncrementalFormatDryRun' task to review\n "
+ "the changes, or the 'javaIncrementalFormatApply' task "
+ "to reformat.")
} else if (checkResult != 'false') {
throw new RuntimeException(
"Failed to invoke format check script:\n" + checkResult)
}
println("Incremental Java format check ok.")
} else {
println("Omitting format check: not in a git directory.")
}
println("Incremental Java format check ok.")
}
}
+1
View File
@@ -213,6 +213,7 @@ PRESUBMITS = {
{"src/test", "ActivityReportingQueryBuilder.java",
# This class contains helper method to make queries in Beam.
"RegistryJpaIO.java",
"CreateSyntheticHistoryEntriesAction.java",
# TODO(b/179158393): Remove everything below, which should be done
# using Criteria
"JpaTransactionManager.java",
+14 -6
View File
@@ -705,7 +705,11 @@ createToolTask(
createToolTask(
'jpaDemoPipeline', 'google.registry.beam.common.JpaDemoPipeline')
'jpaDemoPipeline', 'google.registry.beam.common.JpaDemoPipeline')
createToolTask(
'createSyntheticHistoryEntries',
'google.registry.tools.javascrap.CreateSyntheticHistoryEntriesPipeline')
project.tasks.create('initSqlPipeline', JavaExec) {
main = 'google.registry.beam.initsql.InitSqlPipeline'
@@ -783,27 +787,27 @@ createUberJar(
// User should install gcloud and login to GCP before invoking this tasks.
if (environment == 'alpha') {
def pipelines = [
InitSql :
initSql :
[
mainClass: 'google.registry.beam.initsql.InitSqlPipeline',
metaData : 'google/registry/beam/init_sql_pipeline_metadata.json'
],
BulkDeleteDatastore:
bulkDeleteDatastore:
[
mainClass: 'google.registry.beam.datastore.BulkDeleteDatastorePipeline',
metaData : 'google/registry/beam/bulk_delete_datastore_pipeline_metadata.json'
],
Spec11 :
spec11 :
[
mainClass: 'google.registry.beam.spec11.Spec11Pipeline',
metaData : 'google/registry/beam/spec11_pipeline_metadata.json'
],
Invoicing :
invoicing :
[
mainClass: 'google.registry.beam.invoicing.InvoicingPipeline',
metaData : 'google/registry/beam/invoicing_pipeline_metadata.json'
],
Rde :
rde :
[
mainClass: 'google.registry.beam.rde.RdePipeline',
metaData : 'google/registry/beam/rde_pipeline_metadata.json'
@@ -1111,6 +1115,10 @@ test {
// TODO(weiminyu): Remove dependency on sqlIntegrationTest
}.dependsOn(fragileTest, outcastTest, standardTest, registryToolIntegrationTest, sqlIntegrationTest)
// When we override tests, we also break the cleanTest command.
cleanTest.dependsOn(cleanFragileTest, cleanOutcastTest, cleanStandardTest,
cleanRegistryToolIntegrationTest, cleanSqlIntegrationTest)
project.build.dependsOn devtool
project.build.dependsOn buildToolImage
project.build.dependsOn ':stage'
@@ -55,10 +55,9 @@ public final class CommitLogImports {
* represents the changes in one transaction. The {@code CommitLogManifest} contains deleted
* entity keys, whereas each {@code CommitLogMutation} contains one whole entity.
*/
public static ImmutableList<ImmutableList<VersionedEntity>> loadEntitiesByTransaction(
static ImmutableList<ImmutableList<VersionedEntity>> loadEntitiesByTransaction(
InputStream inputStream) {
try (AppEngineEnvironment appEngineEnvironment = new AppEngineEnvironment();
InputStream input = new BufferedInputStream(inputStream)) {
try (InputStream input = new BufferedInputStream(inputStream)) {
Iterator<ImmutableObject> commitLogs = createDeserializingIterator(input, false);
checkState(commitLogs.hasNext());
checkState(commitLogs.next() instanceof CommitLogCheckpoint);
@@ -105,7 +104,7 @@ public final class CommitLogImports {
* represents the changes in one transaction. The {@code CommitLogManifest} contains deleted
* entity keys, whereas each {@code CommitLogMutation} contains one whole entity.
*/
public static ImmutableList<VersionedEntity> loadEntities(InputStream inputStream) {
static ImmutableList<VersionedEntity> loadEntities(InputStream inputStream) {
return loadEntitiesByTransaction(inputStream).stream()
.flatMap(ImmutableList::stream)
.collect(toImmutableList());
@@ -105,29 +105,29 @@ public abstract class VersionedEntity implements Serializable {
* VersionedEntity VersionedEntities}. See {@link CommitLogImports#loadEntities} for more
* information.
*/
public static Stream<VersionedEntity> fromManifest(CommitLogManifest manifest) {
static Stream<VersionedEntity> fromManifest(CommitLogManifest manifest) {
long commitTimeMillis = manifest.getCommitTime().getMillis();
return manifest.getDeletions().stream()
.map(com.googlecode.objectify.Key::getRaw)
.map(key -> builder().commitTimeMills(commitTimeMillis).key(key).build());
.map(key -> newBuilder().commitTimeMills(commitTimeMillis).key(key).build());
}
/* Converts a {@link CommitLogMutation} to a {@link VersionedEntity}. */
public static VersionedEntity fromMutation(CommitLogMutation mutation) {
static VersionedEntity fromMutation(CommitLogMutation mutation) {
return from(
com.googlecode.objectify.Key.create(mutation).getParent().getId(),
mutation.getEntityProtoBytes());
}
public static VersionedEntity from(long commitTimeMillis, byte[] entityProtoBytes) {
return builder()
return newBuilder()
.entityProtoBytes(entityProtoBytes)
.key(EntityTranslator.createFromPbBytes(entityProtoBytes).getKey())
.commitTimeMills(commitTimeMillis)
.build();
}
static Builder builder() {
private static Builder newBuilder() {
return new AutoValue_VersionedEntity.Builder();
}
@@ -142,7 +142,7 @@ public abstract class VersionedEntity implements Serializable {
public abstract VersionedEntity build();
public Builder entityProtoBytes(byte[] bytes) {
Builder entityProtoBytes(byte[] bytes) {
return entityProtoBytes(new ImmutableBytes(bytes));
}
}
@@ -55,6 +55,7 @@ import org.joda.time.format.DateTimeFormatter;
path = SendExpiringCertificateNotificationEmailAction.PATH,
auth = Auth.AUTH_INTERNAL_OR_ADMIN)
public class SendExpiringCertificateNotificationEmailAction implements Runnable {
public static final String PATH = "/_dr/task/sendExpiringCertificateNotificationEmail";
/**
* Used as an offset when storing the last notification email sent date.
@@ -96,8 +97,13 @@ public class SendExpiringCertificateNotificationEmailAction implements Runnable
public void run() {
response.setContentType(MediaType.PLAIN_TEXT_UTF_8);
try {
sendNotificationEmails();
int numEmailsSent = sendNotificationEmails();
String message =
String.format(
"Done. Sent %d expiring certificate notification emails in total.", numEmailsSent);
logger.atInfo().log(message);
response.setStatus(SC_OK);
response.setPayload(message);
} catch (Exception e) {
logger.atWarning().withCause(e).log(
"Exception thrown when sending expiring certificate notification emails.");
@@ -107,9 +113,11 @@ public class SendExpiringCertificateNotificationEmailAction implements Runnable
}
/**
* Returns a list of registrars that should receive expiring notification emails. There are two
* certificates that should be considered (the main certificate and failOver certificate). The
* registrars should receive notifications if one of the certificate checks returns true.
* Returns a list of registrars that should receive expiring notification emails.
*
* <p>There are two certificates that should be considered (the main certificate and failOver
* certificate). The registrars should receive notifications if one of the certificate checks
* returns true.
*/
@VisibleForTesting
ImmutableList<RegistrarInfo> getRegistrarsWithExpiringCertificates() {
@@ -151,15 +159,17 @@ public class SendExpiringCertificateNotificationEmailAction implements Runnable
}
try {
ImmutableSet<InternetAddress> recipients = getEmailAddresses(registrar, Type.TECH);
ImmutableSet<InternetAddress> ccs = getEmailAddresses(registrar, Type.ADMIN);
Date expirationDate = certificateChecker.getCertificate(certificate.get()).getNotAfter();
logger.atInfo().log(
"Registrar %s should receive an email that its %s SSL certificate will expire on %s.",
registrar.getRegistrarName(),
" %s SSL certificate of registrar '%s' will expire on %s.",
certificateType.getDisplayName(),
registrar.getRegistrarName(),
expirationDate.toString());
if (recipients.isEmpty()) {
if (recipients.isEmpty() && ccs.isEmpty()) {
logger.atWarning().log(
"Registrar %s contains no email addresses to receive notification email.",
"Registrar %s contains no TECH nor ADMIN email addresses to receive notification"
+ " email.",
registrar.getRegistrarName());
return false;
}
@@ -174,7 +184,7 @@ public class SendExpiringCertificateNotificationEmailAction implements Runnable
expirationDate,
registrar.getRegistrarId()))
.setRecipients(recipients)
.setCcs(getEmailAddresses(registrar, Type.ADMIN))
.setCcs(ccs)
.build());
/*
* A duration time offset is used here to ensure that date comparison between two
@@ -243,32 +253,32 @@ public class SendExpiringCertificateNotificationEmailAction implements Runnable
/** Sends notification emails to registrars with expiring certificates. */
@VisibleForTesting
int sendNotificationEmails() {
int emailsSent = 0;
int numEmailsSent = 0;
for (RegistrarInfo registrarInfo : getRegistrarsWithExpiringCertificates()) {
Registrar registrar = registrarInfo.registrar();
if (registrarInfo.isCertExpiring()) {
sendNotificationEmail(
registrar,
registrar.getLastExpiringCertNotificationSentDate(),
CertificateType.PRIMARY,
registrar.getClientCertificate());
emailsSent++;
if (registrarInfo.isCertExpiring()
&& sendNotificationEmail(
registrar,
registrar.getLastExpiringCertNotificationSentDate(),
CertificateType.PRIMARY,
registrar.getClientCertificate())) {
numEmailsSent++;
}
if (registrarInfo.isFailOverCertExpiring()) {
sendNotificationEmail(
registrar,
registrar.getLastExpiringFailoverCertNotificationSentDate(),
CertificateType.FAILOVER,
registrar.getFailoverClientCertificate());
emailsSent++;
if (registrarInfo.isFailOverCertExpiring()
&& sendNotificationEmail(
registrar,
registrar.getLastExpiringFailoverCertNotificationSentDate(),
CertificateType.FAILOVER,
registrar.getFailoverClientCertificate())) {
numEmailsSent++;
}
}
logger.atInfo().log(
"Attempted to send %d expiring certificate notification emails.", emailsSent);
return emailsSent;
return numEmailsSent;
}
/** Returns a list of email addresses of the registrar that should receive a notification email */
/**
* Returns a list of email addresses of the registrar that should receive a notification email.
*/
@VisibleForTesting
ImmutableSet<InternetAddress> getEmailAddresses(Registrar registrar, Type contactType) {
ImmutableSortedSet<RegistrarContact> contacts = registrar.getContactsOfType(contactType);
@@ -327,6 +337,7 @@ public class SendExpiringCertificateNotificationEmailAction implements Runnable
@AutoValue
public abstract static class RegistrarInfo {
static RegistrarInfo create(
Registrar registrar, boolean isCertExpiring, boolean isFailOverCertExpiring) {
return new AutoValue_SendExpiringCertificateNotificationEmailAction_RegistrarInfo(
@@ -0,0 +1,138 @@
// 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 google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static org.apache.http.HttpStatus.SC_INTERNAL_SERVER_ERROR;
import static org.apache.http.HttpStatus.SC_OK;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.flogger.FluentLogger;
import com.google.common.net.MediaType;
import google.registry.config.RegistryConfig.Config;
import google.registry.model.contact.ContactHistory;
import google.registry.request.Action;
import google.registry.request.Action.Service;
import google.registry.request.Response;
import google.registry.request.auth.Auth;
import google.registry.util.Clock;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Stream;
import javax.inject.Inject;
import org.joda.time.DateTime;
/**
* An action that wipes out Personal Identifiable Information (PII) fields of {@link ContactHistory}
* entities.
*
* <p>ContactHistory entities should be retained in the database for only certain amount of time.
* This periodic wipe out action only applies to SQL.
*/
@Action(
service = Service.BACKEND,
path = WipeOutContactHistoryPiiAction.PATH,
auth = Auth.AUTH_INTERNAL_OR_ADMIN)
public class WipeOutContactHistoryPiiAction implements Runnable {
public static final String PATH = "/_dr/task/wipeOutContactHistoryPii";
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private final Clock clock;
private final Response response;
private final int minMonthsBeforeWipeOut;
private final int wipeOutQueryBatchSize;
@Inject
public WipeOutContactHistoryPiiAction(
Clock clock,
@Config("minMonthsBeforeWipeOut") int minMonthsBeforeWipeOut,
@Config("wipeOutQueryBatchSize") int wipeOutQueryBatchSize,
Response response) {
this.clock = clock;
this.response = response;
this.minMonthsBeforeWipeOut = minMonthsBeforeWipeOut;
this.wipeOutQueryBatchSize = wipeOutQueryBatchSize;
}
@Override
public void run() {
response.setContentType(MediaType.PLAIN_TEXT_UTF_8);
try {
int totalNumOfWipedEntities = 0;
DateTime wipeOutTime = clock.nowUtc().minusMonths(minMonthsBeforeWipeOut);
logger.atInfo().log(
"About to wipe out all PII of contact history entities prior to %s.", wipeOutTime);
int numOfWipedEntities = 0;
do {
numOfWipedEntities =
jpaTm()
.transact(
() ->
wipeOutContactHistoryData(
getNextContactHistoryEntitiesWithPiiBatch(wipeOutTime)));
totalNumOfWipedEntities += numOfWipedEntities;
} while (numOfWipedEntities > 0);
String msg =
String.format(
"Done. Wiped out PII of %d ContactHistory entities in total.",
totalNumOfWipedEntities);
logger.atInfo().log(msg);
response.setPayload(msg);
response.setStatus(SC_OK);
} catch (Exception e) {
logger.atSevere().withCause(e).log(
"Exception thrown during the process of wiping out contact history PII.");
response.setStatus(SC_INTERNAL_SERVER_ERROR);
response.setPayload(
String.format(
"Exception thrown during the process of wiping out contact history PII with cause"
+ ": %s",
e));
}
}
/**
* Returns a stream of up to {@link #wipeOutQueryBatchSize} {@link ContactHistory} entities
* containing PII that are prior to @param wipeOutTime.
*/
@VisibleForTesting
Stream<ContactHistory> getNextContactHistoryEntitiesWithPiiBatch(DateTime wipeOutTime) {
// email is one of the required fields in EPP, meaning it's initially not null.
// Therefore, checking if it's null is one way to avoid processing contact history entities
// that have been processed previously. Refer to RFC 5733 for more information.
return jpaTm()
.query(
"FROM ContactHistory WHERE modificationTime < :wipeOutTime " + "AND email IS NOT NULL",
ContactHistory.class)
.setParameter("wipeOutTime", wipeOutTime)
.setMaxResults(wipeOutQueryBatchSize)
.getResultStream();
}
/** Wipes out the PII of each of the {@link ContactHistory} entities in the stream. */
@VisibleForTesting
int wipeOutContactHistoryData(Stream<ContactHistory> contactHistoryEntities) {
AtomicInteger numOfEntities = new AtomicInteger(0);
contactHistoryEntities.forEach(
contactHistoryEntity -> {
jpaTm().update(contactHistoryEntity.asBuilder().wipeOutPii().build());
numOfEntities.incrementAndGet();
});
logger.atInfo().log(
"Wiped out all PII fields of %d ContactHistory entities.", numOfEntities.get());
return numOfEntities.get();
}
}
@@ -92,7 +92,12 @@ public class WipeoutDatastoreAction implements Runnable {
.setJobName(createJobName("bulk-delete-datastore-", clock))
.setContainerSpecGcsPath(
String.format("%s/%s_metadata.json", stagingBucketUrl, PIPELINE_NAME))
.setParameters(ImmutableMap.of("kindsToDelete", "*"));
.setParameters(
ImmutableMap.of(
"kindsToDelete",
"*",
"registryEnvironment",
RegistryEnvironment.get().name()));
LaunchFlexTemplateResponse launchResponse =
dataflow
.projects()
@@ -17,7 +17,6 @@ package google.registry.beam.common;
import static com.google.common.base.Verify.verify;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import google.registry.backup.AppEngineEnvironment;
import google.registry.model.contact.ContactResource;
import google.registry.persistence.transaction.CriteriaQueryBuilder;
import google.registry.persistence.transaction.JpaTransactionManager;
@@ -59,18 +58,16 @@ public class JpaDemoPipeline implements Serializable {
public void processElement() {
// AppEngineEnvironment is needed as long as JPA entity classes still depends
// on Objectify.
try (AppEngineEnvironment allowOfyEntity = new AppEngineEnvironment()) {
int result =
(Integer)
jpaTm()
.transact(
() ->
jpaTm()
.getEntityManager()
.createNativeQuery("select 1;")
.getSingleResult());
verify(result == 1, "Expecting 1, got %s.", result);
}
int result =
(Integer)
jpaTm()
.transact(
() ->
jpaTm()
.getEntityManager()
.createNativeQuery("select 1;")
.getSingleResult());
verify(result == 1, "Expecting 1, got %s.", result);
counter.inc();
}
}));
@@ -20,11 +20,9 @@ import static org.apache.beam.sdk.values.TypeDescriptors.integers;
import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Streams;
import google.registry.backup.AppEngineEnvironment;
import google.registry.beam.common.RegistryQuery.CriteriaQuerySupplier;
import google.registry.model.UpdateAutoTimestamp;
import google.registry.model.UpdateAutoTimestamp.DisableAutoUpdateResource;
import google.registry.model.ofy.ObjectifyService;
import google.registry.model.replay.SqlEntity;
import google.registry.persistence.transaction.JpaTransactionManager;
import google.registry.persistence.transaction.TransactionManagerFactory;
@@ -211,14 +209,9 @@ public final class RegistryJpaIO {
@ProcessElement
public void processElement(OutputReceiver<T> outputReceiver) {
// AppEngineEnvironment is need for handling VKeys, which involve Ofy keys. Unlike
// SqlBatchWriter, it is unnecessary to initialize ObjectifyService in this class.
try (AppEngineEnvironment env = new AppEngineEnvironment()) {
// TODO(b/187210388): JpaTransactionManager should support non-transactional query.
jpaTm()
.transactNoRetry(
() -> query.stream().map(resultMapper::apply).forEach(outputReceiver::output));
}
jpaTm()
.transactNoRetry(
() -> query.stream().map(resultMapper::apply).forEach(outputReceiver::output));
}
}
}
@@ -364,16 +357,6 @@ public final class RegistryJpaIO {
this.withAutoTimestamp = withAutoTimestamp;
}
@Setup
public void setup() {
// AppEngineEnvironment is needed as long as Objectify keys are still involved in the handling
// of SQL entities (e.g., in VKeys). ObjectifyService needs to be initialized when conversion
// between Ofy entity and Datastore entity is needed.
try (AppEngineEnvironment env = new AppEngineEnvironment()) {
ObjectifyService.initOfy();
}
}
@ProcessElement
public void processElement(@Element KV<ShardedKey<Integer>, Iterable<T>> kv) {
if (withAutoTimestamp) {
@@ -386,19 +369,17 @@ public final class RegistryJpaIO {
}
private void actuallyProcessElement(@Element KV<ShardedKey<Integer>, Iterable<T>> kv) {
try (AppEngineEnvironment env = new AppEngineEnvironment()) {
ImmutableList<Object> entities =
Streams.stream(kv.getValue())
.map(this.jpaConverter::apply)
// TODO(b/177340730): post migration delete the line below.
.filter(Objects::nonNull)
.collect(ImmutableList.toImmutableList());
try {
jpaTm().transact(() -> jpaTm().putAll(entities));
counter.inc(entities.size());
} catch (RuntimeException e) {
processSingly(entities);
}
ImmutableList<Object> entities =
Streams.stream(kv.getValue())
.map(this.jpaConverter::apply)
// TODO(b/177340730): post migration delete the line below.
.filter(Objects::nonNull)
.collect(ImmutableList.toImmutableList());
try {
jpaTm().transact(() -> jpaTm().putAll(entities));
counter.inc(entities.size());
} catch (RuntimeException e) {
processSingly(entities);
}
}
@@ -21,6 +21,7 @@ import google.registry.config.CredentialModule;
import google.registry.config.RegistryConfig.Config;
import google.registry.config.RegistryConfig.ConfigModule;
import google.registry.persistence.PersistenceModule;
import google.registry.persistence.PersistenceModule.BeamBulkQueryJpaTm;
import google.registry.persistence.PersistenceModule.BeamJpaTm;
import google.registry.persistence.PersistenceModule.TransactionIsolationLevel;
import google.registry.persistence.transaction.JpaTransactionManager;
@@ -45,9 +46,19 @@ public interface RegistryPipelineComponent {
@Config("projectId")
String getProjectId();
/** Returns the regular {@link JpaTransactionManager} for general use. */
@BeamJpaTm
Lazy<JpaTransactionManager> getJpaTransactionManager();
/**
* Returns a {@link JpaTransactionManager} optimized for bulk loading multi-level JPA entities
* ({@link google.registry.model.domain.DomainBase} and {@link
* google.registry.model.domain.DomainHistory}). Please refer to {@link
* google.registry.model.bulkquery.BulkQueryEntities} for more information.
*/
@BeamBulkQueryJpaTm
Lazy<JpaTransactionManager> getBulkQueryJpaTransactionManager();
@Component.Builder
interface Builder {
@@ -16,6 +16,7 @@ package google.registry.beam.common;
import google.registry.beam.common.RegistryJpaIO.Write;
import google.registry.config.RegistryEnvironment;
import google.registry.persistence.PersistenceModule.JpaTransactionManagerType;
import google.registry.persistence.PersistenceModule.TransactionIsolationLevel;
import java.util.Objects;
import javax.annotation.Nullable;
@@ -34,7 +35,6 @@ import org.apache.beam.sdk.options.Description;
public interface RegistryPipelineOptions extends GcpOptions {
@Description("The Registry environment.")
@Nullable
RegistryEnvironment getRegistryEnvironment();
void setRegistryEnvironment(RegistryEnvironment environment);
@@ -45,6 +45,12 @@ public interface RegistryPipelineOptions extends GcpOptions {
void setIsolationOverride(TransactionIsolationLevel isolationOverride);
@Description("The JPA Transaction Manager to use.")
@Default.Enum(value = "REGULAR")
JpaTransactionManagerType getJpaTransactionManagerType();
void setJpaTransactionManagerType(JpaTransactionManagerType jpaTransactionManagerType);
@Description("The number of entities to write to the SQL database in one operation.")
@Default.Integer(20)
int getSqlWriteBatchSize();
@@ -20,6 +20,8 @@ import com.google.auto.service.AutoService;
import com.google.common.flogger.FluentLogger;
import dagger.Lazy;
import google.registry.config.RegistryEnvironment;
import google.registry.config.SystemPropertySetter;
import google.registry.model.AppEngineEnvironment;
import google.registry.persistence.transaction.JpaTransactionManager;
import google.registry.persistence.transaction.TransactionManagerFactory;
import org.apache.beam.sdk.harness.JvmInitializer;
@@ -35,18 +37,35 @@ import org.apache.beam.sdk.options.PipelineOptions;
@AutoService(JvmInitializer.class)
public class RegistryPipelineWorkerInitializer implements JvmInitializer {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
public static final String PROPERTY = "google.registry.beam";
@Override
public void beforeProcessing(PipelineOptions options) {
RegistryPipelineOptions registryOptions = options.as(RegistryPipelineOptions.class);
RegistryEnvironment environment = registryOptions.getRegistryEnvironment();
if (environment == null || environment.equals(RegistryEnvironment.UNITTEST)) {
return;
throw new RuntimeException(
"A registry environment must be specified in the pipeline options.");
}
logger.atInfo().log("Setting up RegistryEnvironment %s.", environment);
environment.setup();
Lazy<JpaTransactionManager> transactionManagerLazy =
toRegistryPipelineComponent(registryOptions).getJpaTransactionManager();
RegistryPipelineComponent registryPipelineComponent =
toRegistryPipelineComponent(registryOptions);
Lazy<JpaTransactionManager> transactionManagerLazy;
switch (registryOptions.getJpaTransactionManagerType()) {
case BULK_QUERY:
transactionManagerLazy = registryPipelineComponent.getBulkQueryJpaTransactionManager();
break;
case REGULAR:
default:
transactionManagerLazy = registryPipelineComponent.getJpaTransactionManager();
}
TransactionManagerFactory.setJpaTmOnBeamWorker(transactionManagerLazy::get);
// Masquerade all threads as App Engine threads so we can create Ofy keys in the pipeline. Also
// loads all ofy entities.
new AppEngineEnvironment("Beam").setEnvironmentForAllThreads();
// Set the system property so that we can call IdService.allocateId() without access to
// datastore.
SystemPropertySetter.PRODUCTION_IMPL.setProperty(PROPERTY, "true");
}
}
@@ -25,6 +25,7 @@ import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.flogger.FluentLogger;
import com.google.datastore.v1.Entity;
import google.registry.config.RegistryEnvironment;
import java.util.Iterator;
import java.util.Map;
import org.apache.beam.sdk.Pipeline;
@@ -308,6 +309,11 @@ public class BulkDeleteDatastorePipeline {
public interface BulkDeletePipelineOptions extends GcpOptions {
@Description("The Registry environment.")
RegistryEnvironment getRegistryEnvironment();
void setRegistryEnvironment(RegistryEnvironment environment);
@Description(
"The Datastore KINDs to be deleted. The format may be:\n"
+ "\t- The list of kinds to be deleted as a comma-separated string, or\n"
@@ -20,7 +20,6 @@ import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.googlecode.objectify.Key;
import google.registry.backup.AppEngineEnvironment;
import google.registry.backup.VersionedEntity;
import google.registry.beam.common.RegistryJpaIO;
import google.registry.beam.initsql.Transforms.RemoveDomainBaseForeignKeys;
@@ -230,9 +229,7 @@ public class InitSqlPipeline implements Serializable {
}
private static ImmutableList<String> toKindStrings(Collection<Class<?>> entityClasses) {
try (AppEngineEnvironment env = new AppEngineEnvironment()) {
return entityClasses.stream().map(Key::getKind).collect(ImmutableList.toImmutableList());
}
return entityClasses.stream().map(Key::getKind).collect(ImmutableList.toImmutableList());
}
public static void main(String[] args) {
@@ -22,6 +22,7 @@ import static google.registry.beam.initsql.BackupPaths.getExportFilePatterns;
import static google.registry.model.ofy.ObjectifyService.auditedOfy;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static google.registry.util.DateTimeUtils.isBeforeOrAt;
import static google.registry.util.DomainNameUtils.canonicalizeDomainName;
import static java.util.Comparator.comparing;
import static org.apache.beam.sdk.values.TypeDescriptors.kvs;
import static org.apache.beam.sdk.values.TypeDescriptors.strings;
@@ -277,7 +278,7 @@ public final class Transforms {
// Prober contacts referencing phantom registrars. They and their associated history entries can
// be safely ignored.
private static final ImmutableSet IGNORED_CONTACTS =
private static final ImmutableSet<String> IGNORED_CONTACTS =
ImmutableSet.of(
"1_WJ0TEST-GOOGLE", "1_WJ1TEST-GOOGLE", "1_WJ2TEST-GOOGLE", "1_WJ3TEST-GOOGLE");
@@ -320,7 +321,8 @@ public final class Transforms {
return true;
}
private static Entity repairBadData(Entity entity) {
@VisibleForTesting
static Entity repairBadData(Entity entity) {
if (entity.getKind().equals("Cancellation")
&& Objects.equals(entity.getProperty("reason"), "AUTO_RENEW")) {
// AUTO_RENEW has been moved from 'reason' to flags. Change reason to RENEW and add the
@@ -328,6 +330,15 @@ public final class Transforms {
// instead of append. See b/185954992.
entity.setUnindexedProperty("reason", Reason.RENEW.name());
entity.setUnindexedProperty("flags", ImmutableList.of(Flag.AUTO_RENEW.name()));
} else if (entity.getKind().equals("DomainBase")) {
// Canonicalize old domain/host names from 2016 and earlier before we were enforcing this.
entity.setIndexedProperty(
"fullyQualifiedDomainName",
canonicalizeDomainName((String) entity.getProperty("fullyQualifiedDomainName")));
} else if (entity.getKind().equals("HostResource")) {
entity.setIndexedProperty(
"fullyQualifiedHostName",
canonicalizeDomainName((String) entity.getProperty("fullyQualifiedHostName")));
}
return entity;
}
@@ -365,7 +376,8 @@ public final class Transforms {
* Returns a {@link PTransform} that produces a {@link PCollection} containing all elements in the
* given {@link Iterable}.
*/
static PTransform<PBegin, PCollection<String>> toStringPCollection(Iterable<String> strings) {
private static PTransform<PBegin, PCollection<String>> toStringPCollection(
Iterable<String> strings) {
return Create.of(strings).withCoder(StringUtf8Coder.of());
}
@@ -373,7 +385,7 @@ public final class Transforms {
* Returns a {@link PTransform} from file {@link Metadata} to {@link VersionedEntity} using
* caller-provided {@code transformer}.
*/
static PTransform<PCollection<Metadata>, PCollection<VersionedEntity>> processFiles(
private static PTransform<PCollection<Metadata>, PCollection<VersionedEntity>> processFiles(
DoFn<ReadableFile, VersionedEntity> transformer) {
return new PTransform<PCollection<Metadata>, PCollection<VersionedEntity>>() {
@Override
@@ -389,7 +401,7 @@ public final class Transforms {
private final DateTime fromTime;
private final DateTime toTime;
public FilterCommitLogFileByTime(DateTime fromTime, DateTime toTime) {
FilterCommitLogFileByTime(DateTime fromTime, DateTime toTime) {
checkNotNull(fromTime, "fromTime");
checkNotNull(toTime, "toTime");
checkArgument(
@@ -19,10 +19,13 @@ import static com.google.common.base.Verify.verify;
import static google.registry.model.common.Cursor.getCursorTimeOrStartOfTime;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.persistence.transaction.TransactionManagerUtil.transactIfJpaTm;
import static google.registry.rde.RdeModule.BRDA_QUEUE;
import static google.registry.rde.RdeModule.RDE_UPLOAD_QUEUE;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.auto.value.AutoValue;
import com.google.cloud.storage.BlobId;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.flogger.FluentLogger;
import google.registry.gcs.GcsUtils;
import google.registry.keyring.api.PgpHelper;
@@ -31,14 +34,20 @@ import google.registry.model.rde.RdeMode;
import google.registry.model.rde.RdeNamingUtils;
import google.registry.model.rde.RdeRevision;
import google.registry.model.tld.Registry;
import google.registry.rde.BrdaCopyAction;
import google.registry.rde.DepositFragment;
import google.registry.rde.Ghostryde;
import google.registry.rde.PendingDeposit;
import google.registry.rde.RdeCounter;
import google.registry.rde.RdeMarshaller;
import google.registry.rde.RdeModule;
import google.registry.rde.RdeResourceType;
import google.registry.rde.RdeUploadAction;
import google.registry.rde.RdeUtil;
import google.registry.request.Action.Service;
import google.registry.request.RequestParameters;
import google.registry.tldconfig.idn.IdnTableEnum;
import google.registry.util.CloudTasksUtils;
import google.registry.xjc.rdeheader.XjcRdeHeader;
import google.registry.xjc.rdeheader.XjcRdeHeaderElement;
import google.registry.xml.ValidationMode;
@@ -68,6 +77,8 @@ public class RdeIO {
abstract GcsUtils gcsUtils();
abstract CloudTasksUtils cloudTasksUtils();
abstract String rdeBucket();
// It's OK to return a primitive array because we are only using it to construct the
@@ -83,7 +94,9 @@ public class RdeIO {
@AutoValue.Builder
abstract static class Builder {
abstract Builder setGcsUtils(GcsUtils gcsUtils);
abstract Builder setGcsUtils(GcsUtils value);
abstract Builder setCloudTasksUtils(CloudTasksUtils value);
abstract Builder setRdeBucket(String value);
@@ -100,7 +113,8 @@ public class RdeIO {
.apply(
"Write to GCS",
ParDo.of(new RdeWriter(gcsUtils(), rdeBucket(), stagingKeyBytes(), validationMode())))
.apply("Update cursors", ParDo.of(new CursorUpdater()));
.apply("Update cursors", ParDo.of(new CursorUpdater()))
.apply("Enqueue upload action", ParDo.of(new UploadEnqueuer(cloudTasksUtils())));
return PDone.in(input.getPipeline());
}
}
@@ -236,11 +250,12 @@ public class RdeIO {
}
}
private static class CursorUpdater extends DoFn<KV<PendingDeposit, Integer>, Void> {
private static class CursorUpdater extends DoFn<KV<PendingDeposit, Integer>, PendingDeposit> {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@ProcessElement
public void processElement(@Element KV<PendingDeposit, Integer> input) {
public void processElement(
@Element KV<PendingDeposit, Integer> input, OutputReceiver<PendingDeposit> outputReceiver) {
tm().transact(
() -> {
PendingDeposit key = input.getKey();
@@ -268,6 +283,45 @@ public class RdeIO {
"Rolled forward %s on %s cursor to %s.", key.cursor(), key.tld(), newPosition);
RdeRevision.saveRevision(key.tld(), key.watermark(), key.mode(), revision);
});
outputReceiver.output(input.getKey());
}
}
private static class UploadEnqueuer extends DoFn<PendingDeposit, Void> {
private final CloudTasksUtils cloudTasksUtils;
private UploadEnqueuer(CloudTasksUtils cloudTasksUtils) {
this.cloudTasksUtils = cloudTasksUtils;
}
@ProcessElement
public void processElement(@Element PendingDeposit input, PipelineOptions options) {
if (input.mode() == RdeMode.FULL) {
cloudTasksUtils.enqueue(
RDE_UPLOAD_QUEUE,
CloudTasksUtils.createPostTask(
RdeUploadAction.PATH,
Service.BACKEND.getServiceId(),
ImmutableMultimap.of(
RequestParameters.PARAM_TLD,
input.tld(),
RdeModule.PARAM_PREFIX,
options.getJobName() + '/')));
} else {
cloudTasksUtils.enqueue(
BRDA_QUEUE,
CloudTasksUtils.createPostTask(
BrdaCopyAction.PATH,
Service.BACKEND.getServiceId(),
ImmutableMultimap.of(
RequestParameters.PARAM_TLD,
input.tld(),
RdeModule.PARAM_WATERMARK,
input.watermark().toString(),
RdeModule.PARAM_PREFIX,
options.getJobName() + '/')));
}
}
}
}
@@ -27,6 +27,7 @@ import com.google.common.io.BaseEncoding;
import dagger.BindsInstance;
import dagger.Component;
import google.registry.beam.common.RegistryJpaIO;
import google.registry.config.CloudTasksUtilsModule;
import google.registry.config.CredentialModule;
import google.registry.config.RegistryConfig.ConfigModule;
import google.registry.gcs.GcsUtils;
@@ -44,6 +45,8 @@ import google.registry.rde.PendingDeposit;
import google.registry.rde.PendingDeposit.PendingDepositCoder;
import google.registry.rde.RdeFragmenter;
import google.registry.rde.RdeMarshaller;
import google.registry.util.CloudTasksUtils;
import google.registry.util.UtilsModule;
import google.registry.xml.ValidationMode;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
@@ -66,7 +69,6 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory;
import org.apache.beam.sdk.transforms.FlatMapElements;
import org.apache.beam.sdk.transforms.Flatten;
import org.apache.beam.sdk.transforms.GroupByKey;
import org.apache.beam.sdk.transforms.Reshuffle;
import org.apache.beam.sdk.values.KV;
import org.apache.beam.sdk.values.PCollection;
import org.apache.beam.sdk.values.PCollectionList;
@@ -93,6 +95,7 @@ public class RdePipeline implements Serializable {
private final String rdeBucket;
private final byte[] stagingKeyBytes;
private final GcsUtils gcsUtils;
private final CloudTasksUtils cloudTasksUtils;
// Registrars to be excluded from data escrow. Not including the sandbox-only OTE type so that
// if sneaks into production we would get an extra signal.
@@ -111,13 +114,14 @@ public class RdePipeline implements Serializable {
}
@Inject
RdePipeline(RdePipelineOptions options, GcsUtils gcsUtils) {
RdePipeline(RdePipelineOptions options, GcsUtils gcsUtils, CloudTasksUtils cloudTasksUtils) {
this.options = options;
this.mode = ValidationMode.valueOf(options.getValidationMode());
this.pendings = decodePendings(options.getPendings());
this.rdeBucket = options.getGcsBucket();
this.rdeBucket = options.getRdeStagingBucket();
this.stagingKeyBytes = BaseEncoding.base64Url().decode(options.getStagingKey());
this.gcsUtils = gcsUtils;
this.cloudTasksUtils = cloudTasksUtils;
}
PipelineResult run() {
@@ -140,10 +144,11 @@ public class RdePipeline implements Serializable {
void persistData(PCollection<KV<PendingDeposit, Iterable<DepositFragment>>> input) {
input.apply(
"Write to GCS and update cursors",
"Write to GCS, update cursors, and enqueue upload tasks",
RdeIO.Write.builder()
.setRdeBucket(rdeBucket)
.setGcsUtils(gcsUtils)
.setCloudTasksUtils(cloudTasksUtils)
.setValidationMode(mode)
.setStagingKeyBytes(stagingKeyBytes)
.build());
@@ -177,18 +182,13 @@ public class RdePipeline implements Serializable {
}));
}
@SuppressWarnings("deprecation") // Reshuffle is still recommended by Dataflow.
<T extends EppResource>
PCollection<KV<PendingDeposit, DepositFragment>> processNonRegistrarEntities(
Pipeline pipeline, Class<T> clazz) {
return createInputs(pipeline, clazz)
.apply("Marshal " + clazz.getSimpleName() + " into DepositFragment", mapToFragments(clazz))
.setCoder(KvCoder.of(PendingDepositCoder.of(), SerializableCoder.of(DepositFragment.class)))
.apply(
"Reshuffle KV<PendingDeposit, DepositFragment> of "
+ clazz.getSimpleName()
+ " to prevent fusion",
Reshuffle.of());
.setCoder(
KvCoder.of(PendingDepositCoder.of(), SerializableCoder.of(DepositFragment.class)));
}
<T extends EppResource> PCollection<VKey<T>> createInputs(Pipeline pipeline, Class<T> clazz) {
@@ -202,7 +202,7 @@ public class RdePipeline implements Serializable {
String.class,
// TODO: consider adding coders for entities and pass them directly instead of using
// VKeys.
x -> VKey.create(clazz, x)));
x -> VKey.createSql(clazz, x)));
}
<T extends EppResource>
@@ -270,7 +270,7 @@ public class RdePipeline implements Serializable {
* Encodes the TLD to pending deposit map in an URL safe string that is sent to the pipeline
* worker by the pipeline launcher as a pipeline option.
*/
static String encodePendings(ImmutableSetMultimap<String, PendingDeposit> pendings)
public static String encodePendings(ImmutableSetMultimap<String, PendingDeposit> pendings)
throws IOException {
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
ObjectOutputStream oos = new ObjectOutputStream(baos);
@@ -282,13 +282,24 @@ public class RdePipeline implements Serializable {
public static void main(String[] args) throws IOException, ClassNotFoundException {
PipelineOptionsFactory.register(RdePipelineOptions.class);
RdePipelineOptions options = PipelineOptionsFactory.fromArgs(args).as(RdePipelineOptions.class);
RdePipelineOptions options =
PipelineOptionsFactory.fromArgs(args).withValidation().as(RdePipelineOptions.class);
// RegistryPipelineWorkerInitializer only initializes before pipeline executions, after the
// main() function constructed the graph. We need the registry environment set up so that we
// can create a CloudTasksUtils which uses the environment-dependent config file.
options.getRegistryEnvironment().setup();
options.setIsolationOverride(TransactionIsolationLevel.TRANSACTION_READ_COMMITTED);
DaggerRdePipeline_RdePipelineComponent.builder().options(options).build().rdePipeline().run();
}
@Singleton
@Component(modules = {CredentialModule.class, ConfigModule.class})
@Component(
modules = {
CredentialModule.class,
ConfigModule.class,
CloudTasksUtilsModule.class,
UtilsModule.class
})
interface RdePipelineComponent {
RdePipeline rdePipeline();
@@ -31,9 +31,9 @@ public interface RdePipelineOptions extends RegistryPipelineOptions {
void setValidationMode(String value);
@Description("The GCS bucket where the encrypted RDE deposits will be uploaded to.")
String getGcsBucket();
String getRdeStagingBucket();
void setGcsBucket(String value);
void setRdeStagingBucket(String value);
@Description("The Base64-encoded PGP public key to encrypt the deposits.")
String getStagingKey();
@@ -16,6 +16,7 @@ package google.registry.beam.spec11;
import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.beam.BeamUtils.getQueryFromFile;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableSet;
@@ -30,6 +31,7 @@ import google.registry.model.domain.DomainBase;
import google.registry.model.reporting.Spec11ThreatMatch;
import google.registry.model.reporting.Spec11ThreatMatch.ThreatType;
import google.registry.persistence.PersistenceModule.TransactionIsolationLevel;
import google.registry.persistence.VKey;
import google.registry.util.Retrier;
import google.registry.util.SqlTemplate;
import google.registry.util.UtilsModule;
@@ -41,6 +43,7 @@ import org.apache.beam.sdk.coders.SerializableCoder;
import org.apache.beam.sdk.io.TextIO;
import org.apache.beam.sdk.io.gcp.bigquery.BigQueryIO;
import org.apache.beam.sdk.options.PipelineOptionsFactory;
import org.apache.beam.sdk.transforms.DoFn;
import org.apache.beam.sdk.transforms.GroupByKey;
import org.apache.beam.sdk.transforms.MapElements;
import org.apache.beam.sdk.transforms.ParDo;
@@ -113,15 +116,43 @@ public class Spec11Pipeline implements Serializable {
}
static PCollection<DomainNameInfo> readFromCloudSql(Pipeline pipeline) {
Read<Object[], DomainNameInfo> read =
Read<Object[], KV<String, String>> read =
RegistryJpaIO.read(
"select d, r.emailAddress from Domain d join Registrar r on"
+ " d.currentSponsorClientId = r.clientIdentifier where r.type = 'REAL'"
+ " and d.deletionTime > now()",
"select d.repoId, r.emailAddress from Domain d join Registrar r on"
+ " d.currentSponsorClientId = r.clientIdentifier where r.type = 'REAL' and"
+ " d.deletionTime > now()",
false,
Spec11Pipeline::parseRow);
return pipeline.apply("Read active domains from Cloud SQL", read);
return pipeline
.apply("Read active domains from Cloud SQL", read)
.apply(
"Build DomainNameInfo",
ParDo.of(
new DoFn<KV<String, String>, DomainNameInfo>() {
@ProcessElement
public void processElement(
@Element KV<String, String> input, OutputReceiver<DomainNameInfo> output) {
DomainBase domainBase =
jpaTm()
.transact(
() ->
jpaTm()
.loadByKey(
VKey.createSql(DomainBase.class, input.getKey())));
String emailAddress = input.getValue();
if (emailAddress == null) {
emailAddress = "";
}
DomainNameInfo domainNameInfo =
DomainNameInfo.create(
domainBase.getDomainName(),
domainBase.getRepoId(),
domainBase.getCurrentSponsorRegistrarId(),
emailAddress);
output.output(domainNameInfo);
}
}));
}
static PCollection<DomainNameInfo> readFromBigQuery(
@@ -142,17 +173,8 @@ public class Spec11Pipeline implements Serializable {
.withTemplateCompatibility());
}
private static DomainNameInfo parseRow(Object[] row) {
DomainBase domainBase = (DomainBase) row[0];
String emailAddress = (String) row[1];
if (emailAddress == null) {
emailAddress = "";
}
return DomainNameInfo.create(
domainBase.getDomainName(),
domainBase.getRepoId(),
domainBase.getCurrentSponsorRegistrarId(),
emailAddress);
private static KV<String, String> parseRow(Object[] row) {
return KV.of((String) row[0], (String) row[1]);
}
static void saveToSql(
@@ -22,10 +22,13 @@ import dagger.Provides;
import google.registry.config.CredentialModule.DefaultCredential;
import google.registry.config.RegistryConfig.Config;
import google.registry.util.CloudTasksUtils;
import google.registry.util.CloudTasksUtils.GcpCloudTasksClient;
import google.registry.util.CloudTasksUtils.SerializableCloudTasksClient;
import google.registry.util.GoogleCredentialsBundle;
import google.registry.util.Retrier;
import java.io.IOException;
import javax.inject.Provider;
import java.io.Serializable;
import java.util.function.Supplier;
import javax.inject.Singleton;
/**
@@ -42,24 +45,35 @@ public abstract class CloudTasksUtilsModule {
public static CloudTasksUtils provideCloudTasksUtils(
@Config("projectId") String projectId,
@Config("locationId") String locationId,
// Use a provider so that we can use try-with-resources with the client, which implements
// Autocloseable.
Provider<CloudTasksClient> clientProvider,
SerializableCloudTasksClient client,
Retrier retrier) {
return new CloudTasksUtils(retrier, projectId, locationId, clientProvider);
return new CloudTasksUtils(retrier, projectId, locationId, client);
}
// Provides a supplier instead of using a Dagger @Provider because the latter is not serializable.
@Provides
public static Supplier<CloudTasksClient> provideCloudTasksClientSupplier(
@DefaultCredential GoogleCredentialsBundle credentials) {
return (Supplier<CloudTasksClient> & Serializable)
() -> {
CloudTasksClient client;
try {
client =
CloudTasksClient.create(
CloudTasksSettings.newBuilder()
.setCredentialsProvider(
FixedCredentialsProvider.create(credentials.getGoogleCredentials()))
.build());
} catch (IOException e) {
throw new RuntimeException(e);
}
return client;
};
}
@Provides
public static CloudTasksClient provideCloudTasksClient(
@DefaultCredential GoogleCredentialsBundle credentials) {
try {
return CloudTasksClient.create(
CloudTasksSettings.newBuilder()
.setCredentialsProvider(
FixedCredentialsProvider.create(credentials.getGoogleCredentials()))
.build());
} catch (IOException e) {
throw new RuntimeException(e);
}
public static SerializableCloudTasksClient provideSerializableCloudTasksClient(
final Supplier<CloudTasksClient> clientSupplier) {
return new GcpCloudTasksClient(clientSupplier);
}
}
@@ -1306,6 +1306,18 @@ public final class RegistryConfig {
public static ImmutableSet<String> provideAllowedEcdsaCurves(RegistryConfigSettings config) {
return ImmutableSet.copyOf(config.sslCertificateValidation.allowedEcdsaCurves);
}
@Provides
@Config("minMonthsBeforeWipeOut")
public static int provideMinMonthsBeforeWipeOut(RegistryConfigSettings config) {
return config.contactHistory.minMonthsBeforeWipeOut;
}
@Provides
@Config("wipeOutQueryBatchSize")
public static int provideWipeOutQueryBatchSize(RegistryConfigSettings config) {
return config.contactHistory.wipeOutQueryBatchSize;
}
}
/** Returns the App Engine project ID, which is based off the environment name. */
@@ -41,6 +41,7 @@ public class RegistryConfigSettings {
public Keyring keyring;
public RegistryTool registryTool;
public SslCertificateValidation sslCertificateValidation;
public ContactHistory contactHistory;
/** Configuration options that apply to the entire App Engine project. */
public static class AppEngine {
@@ -234,4 +235,10 @@ public class RegistryConfigSettings {
public String expirationWarningEmailBodyText;
public String expirationWarningEmailSubjectText;
}
/** Configuration for contact history. */
public static class ContactHistory {
public int minMonthsBeforeWipeOut;
public int wipeOutQueryBatchSize;
}
}
@@ -442,6 +442,13 @@ registryTool:
# OAuth client secret used by the tool.
clientSecret: YOUR_CLIENT_SECRET
# Configuration options for handling contact history.
contactHistory:
# The number of months that a ContactHistory entity should be stored in the database.
minMonthsBeforeWipeOut: 18
# The batch size for querying ContactHistory table in the database.
wipeOutQueryBatchSize: 500
# Configuration options for checking SSL certificates.
sslCertificateValidation:
# A map specifying the maximum amount of days the certificate can be valid.
@@ -391,6 +391,13 @@
<url-pattern>/_dr/task/relockDomain</url-pattern>
</servlet-mapping>
<!-- Background action to wipe out PII fields of ContactHistory entities that
have been in the database for a certain period of time. -->
<servlet-mapping>
<servlet-name>backend-servlet</servlet-name>
<url-pattern>/_dr/task/wipeOutContactHistoryPii</url-pattern>
</servlet-mapping>
<!-- Action to wipeout Cloud SQL data -->
<servlet-mapping>
<servlet-name>backend-servlet</servlet-name>
@@ -348,4 +348,14 @@
<schedule>every 3 minutes</schedule>
<target>backend</target>
</cron>
<cron>
<url><![CDATA[/_dr/task/wipeOutContactHistoryPii]]></url>
<description>
This job runs weekly to wipe out PII fields of ContactHistory entities
that have been in the database for a certain period of time.
</description>
<schedule>every monday 15:00</schedule>
<target>backend</target>
</cron>
</cronentries>
@@ -246,4 +246,14 @@
<schedule>every 3 minutes</schedule>
<target>backend</target>
</cron>
<cron>
<url><![CDATA[/_dr/task/wipeOutContactHistoryPii]]></url>
<description>
This job runs weekly to wipe out PII fields of ContactHistory entities
that have been in the database for a certain period of time.
</description>
<schedule>every monday 15:00</schedule>
<target>backend</target>
</cron>
</cronentries>
@@ -229,6 +229,15 @@ public final class DomainRenewFlow implements TransactionalFlow {
private DomainHistory buildDomainHistory(
DomainBase newDomain, DateTime now, Period period, Duration renewGracePeriod) {
Optional<MetadataExtension> metadataExtensionOpt =
eppInput.getSingleExtension(MetadataExtension.class);
if (metadataExtensionOpt.isPresent()) {
MetadataExtension metadataExtension = metadataExtensionOpt.get();
if (metadataExtension.getReason() != null) {
historyBuilder.setReason(metadataExtension.getReason());
}
historyBuilder.setRequestedByRegistrar(metadataExtension.getRequestedByRegistrar());
}
return historyBuilder
.setType(DOMAIN_RENEW)
.setPeriod(period)
@@ -12,12 +12,13 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.backup;
package google.registry.model;
import com.google.apphosting.api.ApiProxy;
import com.google.apphosting.api.ApiProxy.Environment;
import com.google.common.collect.ImmutableMap;
import java.io.Closeable;
import google.registry.model.ofy.ObjectifyService;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
@@ -38,26 +39,39 @@ import java.lang.reflect.Proxy;
* <p>Note that conversion from Objectify objects to Datastore {@code Entities} still requires the
* Datastore service.
*/
public class AppEngineEnvironment implements Closeable {
public class AppEngineEnvironment {
private boolean isPlaceHolderNeeded;
private Environment environment;
public AppEngineEnvironment() {
this("PlaceholderAppId");
}
public AppEngineEnvironment(String appId) {
isPlaceHolderNeeded = ApiProxy.getCurrentEnvironment() == null;
// isPlaceHolderNeeded may be true when we are invoked in a test with AppEngineExtension.
if (isPlaceHolderNeeded) {
ApiProxy.setEnvironmentForCurrentThread(createAppEngineEnvironment(appId));
}
environment = createAppEngineEnvironment(appId);
}
@Override
public void close() {
if (isPlaceHolderNeeded) {
ApiProxy.setEnvironmentForCurrentThread(null);
public void setEnvironmentForCurrentThread() {
ApiProxy.setEnvironmentForCurrentThread(environment);
ObjectifyService.initOfy();
}
public void setEnvironmentForAllThreads() {
setEnvironmentForCurrentThread();
ApiProxy.setEnvironmentFactory(() -> environment);
}
public void unsetEnvironmentForCurrentThread() {
ApiProxy.clearEnvironmentForCurrentThread();
}
public void unsetEnvironmentForAllThreads() {
try {
Method method = ApiProxy.class.getDeclaredMethod("clearEnvironmentFactory");
method.setAccessible(true);
method.invoke(null);
} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@@ -14,6 +14,7 @@
package google.registry.model;
import google.registry.util.PreconditionsUtils;
import javax.persistence.Access;
import javax.persistence.AccessType;
import javax.persistence.AttributeOverride;
@@ -49,4 +50,14 @@ public abstract class BackupGroupRoot extends ImmutableObject {
public UpdateAutoTimestamp getUpdateTimestamp() {
return updateTimestamp;
}
/**
* Copies {@link #updateTimestamp} from another entity.
*
* <p>This method is for the few cases when {@code updateTimestamp} is copied between different
* types of entities. Use {@link #clone} for same-type copying.
*/
protected void copyUpdateTimestamp(BackupGroupRoot other) {
this.updateTimestamp = PreconditionsUtils.checkArgumentNotNull(other, "other").updateTimestamp;
}
}
@@ -18,13 +18,14 @@ import static com.google.common.base.Preconditions.checkState;
import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.common.annotations.VisibleForTesting;
import google.registry.beam.common.RegistryPipelineWorkerInitializer;
import google.registry.config.RegistryEnvironment;
import java.util.concurrent.atomic.AtomicLong;
/**
* Allocates a globally unique {@link Long} number to use as an Ofy {@code @Id}.
*
* <p>In non-test environments the Id is generated by Datastore, whereas in tests it's from an
* <p>In non-test, non-beam environments the Id is generated by Datastore, otherwise it's from an
* atomic long number that's incremented every time this method is called.
*/
public final class IdService {
@@ -35,13 +36,25 @@ public final class IdService {
*/
private static final String APP_WIDE_ALLOCATION_KIND = "common";
/** Counts of used ids for use in unit tests. Outside tests this is never used. */
private static final AtomicLong nextTestId = new AtomicLong(1); // ids cannot be zero
/**
* Counts of used ids for use in unit tests or Beam.
*
* <p>Note that one should only use self-allocate Ids in Beam for entities whose Ids are not
* important and are not persisted back to the database, i. e. nowhere the uniqueness of the ID is
* required.
*/
private static final AtomicLong nextSelfAllocatedId = new AtomicLong(1); // ids cannot be zero
private static final boolean isSelfAllocated() {
return RegistryEnvironment.UNITTEST.equals(RegistryEnvironment.get())
|| "true".equals(System.getProperty(RegistryPipelineWorkerInitializer.PROPERTY, "false"));
}
/** Allocates an id. */
// TODO(b/201547855): Find a way to allocate a unique ID without datastore.
public static long allocateId() {
return RegistryEnvironment.UNITTEST.equals(RegistryEnvironment.get())
? nextTestId.getAndIncrement()
return isSelfAllocated()
? nextSelfAllocatedId.getAndIncrement()
: DatastoreServiceFactory.getDatastoreService()
.allocateIds(APP_WIDE_ALLOCATION_KIND, 1)
.iterator()
@@ -49,13 +62,11 @@ public final class IdService {
.getId();
}
/** Resets the global test id counter (i.e. sets the next id to 1). */
/** Resets the global self-allocated id counter (i.e. sets the next id to 1). */
@VisibleForTesting
public static void resetNextTestId() {
public static void resetSelfAllocatedId() {
checkState(
RegistryEnvironment.UNITTEST.equals(RegistryEnvironment.get()),
"Can't call resetTestIdCounts() from RegistryEnvironment.%s",
RegistryEnvironment.get());
nextTestId.set(1); // ids cannot be zero
isSelfAllocated(), "Can only call resetSelfAllocatedId() in unit tests or Beam pipelines");
nextSelfAllocatedId.set(1); // ids cannot be zero
}
}
@@ -15,12 +15,8 @@
package google.registry.model;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.persistence.transaction.TransactionManagerFactory.ofyTm;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static org.joda.time.DateTimeZone.UTC;
import com.google.common.flogger.FluentLogger;
import com.google.common.flogger.StackSize;
import com.googlecode.objectify.annotation.Ignore;
import com.googlecode.objectify.annotation.OnLoad;
import google.registry.model.translators.UpdateAutoTimestampTranslatorFactory;
@@ -44,12 +40,10 @@ import org.joda.time.DateTime;
@Embeddable
public class UpdateAutoTimestamp extends ImmutableObject {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
// When set to true, database converters/translators should do the auto update. When set to
// false, auto update should be suspended (this exists to allow us to preserve the original value
// during a replay).
private static ThreadLocal<Boolean> autoUpdateEnabled = ThreadLocal.withInitial(() -> true);
private static final ThreadLocal<Boolean> autoUpdateEnabled = ThreadLocal.withInitial(() -> true);
@Transient DateTime timestamp;
@@ -63,16 +57,7 @@ public class UpdateAutoTimestamp extends ImmutableObject {
@PrePersist
@PreUpdate
void setTimestamp() {
// On the off chance that this is called outside of a transaction, log it instead of failing
// with an exception from attempting to call jpaTm().getTransactionTime(), and then fall back
// to DateTime.now(UTC).
if (!jpaTm().inTransaction()) {
logger.atSevere().withStackTrace(StackSize.MEDIUM).log(
"Failed to update automatic timestamp because this wasn't called in a JPA transaction%s.",
ofyTm().inTransaction() ? " (but there is an open Ofy transaction)" : "");
timestamp = DateTime.now(UTC);
lastUpdateTime = DateTimeUtils.toZonedDateTime(timestamp);
} else if (autoUpdateEnabled() || lastUpdateTime == null) {
if (autoUpdateEnabled() || lastUpdateTime == null) {
timestamp = jpaTm().getTransactionTime();
lastUpdateTime = DateTimeUtils.toZonedDateTime(timestamp);
}
@@ -0,0 +1,109 @@
// 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.model.bulkquery;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.DomainContent;
import google.registry.model.domain.DomainHistory;
import google.registry.model.domain.GracePeriod;
import google.registry.model.domain.GracePeriod.GracePeriodHistory;
import google.registry.model.domain.secdns.DelegationSignerData;
import google.registry.model.domain.secdns.DomainDsDataHistory;
import google.registry.model.host.HostResource;
import google.registry.model.reporting.DomainTransactionRecord;
import google.registry.persistence.VKey;
import google.registry.persistence.transaction.JpaTransactionManager;
/**
* Utilities for managing an alternative JPA entity model optimized for bulk loading multi-level
* entities such as {@link DomainBase} and {@link DomainHistory}.
*
* <p>In a bulk query for a multi-level JPA entity type, the JPA framework only generates a bulk
* query (SELECT * FROM table) for the base table. Then, for each row in the base table, additional
* queries are issued to load associated rows in child tables. This can be very slow when an entity
* type has multiple child tables.
*
* <p>We have defined an alternative entity model for {@code DomainBase} and {@code DomainHistory},
* where the base table as well as the child tables are mapped to single-level entity types. The
* idea is to load each of these types using a bulk query, and assemble them into the target type in
* memory in a pipeline. The main use case is Datastore-Cloud SQL validation during the Registry
* database migration, where we will need the full database snapshots frequently.
*/
public class BulkQueryEntities {
/**
* The JPA entity classes in persistence.xml to replace when creating the {@link
* JpaTransactionManager} for bulk query.
*/
public static final ImmutableMap<String, String> JPA_ENTITIES_REPLACEMENTS =
ImmutableMap.of(
DomainBase.class.getCanonicalName(),
DomainBaseLite.class.getCanonicalName(),
DomainHistory.class.getCanonicalName(),
DomainHistoryLite.class.getCanonicalName());
/* The JPA entity classes that are not included in persistence.xml and need to be added to
* the {@link JpaTransactionManager} for bulk query.*/
public static final ImmutableList<String> JPA_ENTITIES_NEW =
ImmutableList.of(
DomainHost.class.getCanonicalName(), DomainHistoryHost.class.getCanonicalName());
public static DomainBase assembleDomainBase(
DomainBaseLite domainBaseLite,
ImmutableSet<GracePeriod> gracePeriods,
ImmutableSet<DelegationSignerData> delegationSignerData,
ImmutableSet<VKey<HostResource>> nsHosts) {
DomainBase.Builder builder = new DomainBase.Builder();
builder.copyFrom(domainBaseLite);
builder.setGracePeriods(gracePeriods);
builder.setDsData(delegationSignerData);
builder.setNameservers(nsHosts);
return builder.build();
}
public static DomainHistory assembleDomainHistory(
DomainHistoryLite domainHistoryLite,
ImmutableSet<DomainDsDataHistory> dsDataHistories,
ImmutableSet<VKey<HostResource>> domainHistoryHosts,
ImmutableSet<GracePeriodHistory> gracePeriodHistories,
ImmutableSet<DomainTransactionRecord> transactionRecords) {
DomainHistory.Builder builder = new DomainHistory.Builder();
builder.copyFrom(domainHistoryLite);
DomainContent rawDomainContent = domainHistoryLite.domainContent;
if (rawDomainContent != null) {
DomainContent newDomainContent =
domainHistoryLite
.domainContent
.asBuilder()
.setNameservers(domainHistoryHosts)
.setGracePeriods(
gracePeriodHistories.stream()
.map(GracePeriod::createFromHistory)
.collect(toImmutableSet()))
.setDsData(
dsDataHistories.stream()
.map(DelegationSignerData::create)
.collect(toImmutableSet()))
.build();
builder.setDomain(newDomainContent);
}
return builder.buildAndAssemble(
dsDataHistories, domainHistoryHosts, gracePeriodHistories, transactionRecords);
}
}
@@ -0,0 +1,49 @@
// 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.model.bulkquery;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.DomainContent;
import google.registry.model.replay.SqlOnlyEntity;
import google.registry.persistence.VKey;
import google.registry.persistence.WithStringVKey;
import javax.persistence.Access;
import javax.persistence.AccessType;
import javax.persistence.Entity;
/**
* A 'light' version of {@link DomainBase} with only base table ("Domain") attributes, which allows
* fast bulk loading. They are used in in-memory assembly of {@code DomainBase} instances along with
* bulk-loaded child entities ({@code GracePeriod} etc). The in-memory assembly achieves much higher
* performance than loading {@code DomainBase} directly.
*
* <p>Please refer to {@link BulkQueryEntities} for more information.
*/
@Entity(name = "Domain")
@WithStringVKey
@Access(AccessType.FIELD)
public class DomainBaseLite extends DomainContent implements SqlOnlyEntity {
@Override
@javax.persistence.Id
@Access(AccessType.PROPERTY)
public String getRepoId() {
return super.getRepoId();
}
public static VKey<DomainBaseLite> createVKey(String repoId) {
return VKey.createSql(DomainBaseLite.class, repoId);
}
}
@@ -0,0 +1,50 @@
// 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.model.bulkquery;
import google.registry.model.domain.DomainHistory.DomainHistoryId;
import google.registry.model.host.HostResource;
import google.registry.model.replay.SqlOnlyEntity;
import google.registry.persistence.VKey;
import java.io.Serializable;
import javax.persistence.Access;
import javax.persistence.AccessType;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.IdClass;
/**
* A name server host referenced by a {@link google.registry.model.domain.DomainHistory} record.
* Please refer to {@link BulkQueryEntities} for usage.
*/
@Entity
@Access(AccessType.FIELD)
@IdClass(DomainHistoryHost.class)
public class DomainHistoryHost implements Serializable, SqlOnlyEntity {
@Id private Long domainHistoryHistoryRevisionId;
@Id private String domainHistoryDomainRepoId;
@Id private String hostRepoId;
private DomainHistoryHost() {}
public DomainHistoryId getDomainHistoryId() {
return new DomainHistoryId(domainHistoryDomainRepoId, domainHistoryHistoryRevisionId);
}
public VKey<HostResource> getHostVKey() {
return VKey.create(HostResource.class, hostRepoId);
}
}
@@ -0,0 +1,122 @@
// 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.model.bulkquery;
import com.googlecode.objectify.Key;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.DomainContent;
import google.registry.model.domain.DomainHistory;
import google.registry.model.domain.DomainHistory.DomainHistoryId;
import google.registry.model.domain.Period;
import google.registry.model.replay.SqlOnlyEntity;
import google.registry.model.reporting.HistoryEntry;
import google.registry.persistence.VKey;
import javax.annotation.Nullable;
import javax.persistence.Access;
import javax.persistence.AccessType;
import javax.persistence.AttributeOverride;
import javax.persistence.AttributeOverrides;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.IdClass;
import javax.persistence.PostLoad;
/**
* A 'light' version of {@link DomainHistory} with only base table ("DomainHistory") attributes,
* which allows fast bulk loading. They are used in in-memory assembly of {@code DomainHistory}
* instances along with bulk-loaded child entities ({@code GracePeriodHistory} etc). The in-memory
* assembly achieves much higher performance than loading {@code DomainHistory} directly.
*
* <p>Please refer to {@link BulkQueryEntities} for more information.
*
* <p>This class is adapted from {@link DomainHistory} by removing the {@code dsDataHistories},
* {@code gracePeriodHistories}, and {@code nsHosts} fields and associated methods.
*/
@Entity(name = "DomainHistory")
@Access(AccessType.FIELD)
@IdClass(DomainHistoryId.class)
public class DomainHistoryLite extends HistoryEntry implements SqlOnlyEntity {
// Store DomainContent instead of DomainBase so we don't pick up its @Id
// Nullable for the sake of pre-Registry-3.0 history objects
@Nullable DomainContent domainContent;
@Id
@Access(AccessType.PROPERTY)
public String getDomainRepoId() {
// We need to handle null case here because Hibernate sometimes accesses this method before
// parent gets initialized
return parent == null ? null : parent.getName();
}
/** This method is private because it is only used by Hibernate. */
@SuppressWarnings("unused")
private void setDomainRepoId(String domainRepoId) {
parent = Key.create(DomainBase.class, domainRepoId);
}
@Override
@Nullable
@Access(AccessType.PROPERTY)
@AttributeOverrides({
@AttributeOverride(name = "unit", column = @Column(name = "historyPeriodUnit")),
@AttributeOverride(name = "value", column = @Column(name = "historyPeriodValue"))
})
public Period getPeriod() {
return super.getPeriod();
}
/**
* For transfers, the id of the other registrar.
*
* <p>For requests and cancels, the other registrar is the losing party (because the registrar
* sending the EPP transfer command is the gaining party). For approves and rejects, the other
* registrar is the gaining party.
*/
@Nullable
@Access(AccessType.PROPERTY)
@Column(name = "historyOtherRegistrarId")
@Override
public String getOtherRegistrarId() {
return super.getOtherRegistrarId();
}
@Id
@Column(name = "historyRevisionId")
@Access(AccessType.PROPERTY)
@Override
public long getId() {
return super.getId();
}
/** The key to the {@link DomainBase} this is based off of. */
public VKey<DomainBase> getParentVKey() {
return VKey.create(DomainBase.class, getDomainRepoId());
}
@PostLoad
void postLoad() {
if (domainContent == null) {
return;
}
// See inline comments in DomainHistory.postLoad for reasons for the following lines.
if (domainContent.getDomainName() == null) {
domainContent = null;
} else if (domainContent.getRepoId() == null) {
domainContent.setRepoId(parent.getName());
}
}
}
@@ -0,0 +1,46 @@
// 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.model.bulkquery;
import google.registry.model.host.HostResource;
import google.registry.model.replay.SqlOnlyEntity;
import google.registry.persistence.VKey;
import java.io.Serializable;
import javax.persistence.Access;
import javax.persistence.AccessType;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.IdClass;
/** A name server host of a domain. Please refer to {@link BulkQueryEntities} for usage. */
@Entity
@Access(AccessType.FIELD)
@IdClass(DomainHost.class)
public class DomainHost implements Serializable, SqlOnlyEntity {
@Id private String domainRepoId;
@Id private String hostRepoId;
DomainHost() {}
public String getDomainRepoId() {
return domainRepoId;
}
public VKey<HostResource> getHostVKey() {
return VKey.create(HostResource.class, hostRepoId);
}
}
@@ -227,5 +227,11 @@ public class ContactHistory extends HistoryEntry implements SqlEntity {
getInstance().parent = Key.create(ContactResource.class, contactRepoId);
return this;
}
public Builder wipeOutPii() {
getInstance().contactBase =
getInstance().getContactBase().get().asBuilder().wipeOut().build();
return this;
}
}
}
@@ -189,6 +189,7 @@ public class DomainBase extends DomainContent
}
public Builder copyFrom(DomainContent domainContent) {
this.getInstance().copyUpdateTimestamp(domainContent);
return this.setAuthInfo(domainContent.getAuthInfo())
.setAutorenewPollMessage(domainContent.getAutorenewPollMessage())
.setAutorenewBillingEvent(domainContent.getAutorenewBillingEvent())
@@ -865,7 +865,8 @@ public class DomainContent extends EppResource
public B setDomainName(String domainName) {
checkArgument(
domainName.equals(canonicalizeDomainName(domainName)),
"Domain name must be in puny-coded, lower-case form");
"Domain name %s not in puny-coded, lower-case form",
domainName);
getInstance().fullyQualifiedDomainName = domainName;
return thisCastToDerived();
}
@@ -216,6 +216,10 @@ public class DomainHistory extends HistoryEntry implements SqlEntity {
return super.getId();
}
public DomainHistoryId getDomainHistoryId() {
return new DomainHistoryId(getDomainRepoId(), getId());
}
/** Returns keys to the {@link HostResource} that are the nameservers for the domain. */
public Set<VKey<HostResource>> getNsHosts() {
return nsHosts;
@@ -314,6 +318,8 @@ public class DomainHistory extends HistoryEntry implements SqlEntity {
nullToEmptyImmutableCopy(domainHistory.domainContent.getGracePeriods()).stream()
.map(gracePeriod -> GracePeriodHistory.createFrom(domainHistory.id, gracePeriod))
.collect(toImmutableSet());
} else {
domainHistory.nsHosts = ImmutableSet.of();
}
}
@@ -393,8 +399,16 @@ public class DomainHistory extends HistoryEntry implements SqlEntity {
if (domainContent == null) {
return this;
}
// TODO(b/203609982): if actual type of domainContent is DomainBase, convert to DomainContent
// Note: a DomainHistory fetched by JPA has DomainContent in this field. Allowing DomainBase
// in the setter makes equality checks messy.
getInstance().domainContent = domainContent;
return super.setParent(domainContent);
if (domainContent instanceof DomainBase) {
super.setParent(domainContent);
} else {
super.setParent(Key.create(DomainBase.class, domainContent.getRepoId()));
}
return this;
}
public Builder setDomainRepoId(String domainRepoId) {
@@ -412,5 +426,19 @@ public class DomainHistory extends HistoryEntry implements SqlEntity {
fillAuxiliaryFieldsFromDomain(instance);
return instance;
}
public DomainHistory buildAndAssemble(
ImmutableSet<DomainDsDataHistory> dsDataHistories,
ImmutableSet<VKey<HostResource>> domainHistoryHosts,
ImmutableSet<GracePeriodHistory> gracePeriodHistories,
ImmutableSet<DomainTransactionRecord> transactionRecords) {
DomainHistory instance = super.build();
instance.dsDataHistories = dsDataHistories;
instance.nsHosts = domainHistoryHosts;
instance.gracePeriodHistories = gracePeriodHistories;
instance.domainTransactionRecords = transactionRecords;
instance.hashCode = null;
return instance;
}
}
}
@@ -22,8 +22,10 @@ import com.google.common.annotations.VisibleForTesting;
import com.googlecode.objectify.annotation.Embed;
import google.registry.model.billing.BillingEvent;
import google.registry.model.billing.BillingEvent.Recurring;
import google.registry.model.domain.DomainHistory.DomainHistoryId;
import google.registry.model.domain.rgp.GracePeriodStatus;
import google.registry.model.replay.DatastoreAndSqlEntity;
import google.registry.model.replay.SqlOnlyEntity;
import google.registry.persistence.BillingVKey.BillingEventVKey;
import google.registry.persistence.BillingVKey.BillingRecurrenceVKey;
import google.registry.persistence.VKey;
@@ -202,7 +204,7 @@ public class GracePeriod extends GracePeriodBase implements DatastoreAndSqlEntit
/** Entity class to represent a historic {@link GracePeriod}. */
@Entity(name = "GracePeriodHistory")
@Table(indexes = @Index(columnList = "domainRepoId"))
static class GracePeriodHistory extends GracePeriodBase {
public static class GracePeriodHistory extends GracePeriodBase implements SqlOnlyEntity {
@Id Long gracePeriodHistoryRevisionId;
/** ID for the associated {@link DomainHistory} entity. */
@@ -214,6 +216,10 @@ public class GracePeriod extends GracePeriodBase implements DatastoreAndSqlEntit
return super.getGracePeriodId();
}
public DomainHistoryId getDomainHistoryId() {
return new DomainHistoryId(getDomainRepoId(), domainHistoryRevisionId);
}
static GracePeriodHistory createFrom(long historyRevisionId, GracePeriod gracePeriod) {
GracePeriodHistory instance = new GracePeriodHistory();
instance.gracePeriodHistoryRevisionId = allocateId();
@@ -17,6 +17,7 @@ package google.registry.model.domain.secdns;
import static google.registry.model.IdService.allocateId;
import google.registry.model.domain.DomainHistory;
import google.registry.model.domain.DomainHistory.DomainHistoryId;
import google.registry.model.replay.SqlOnlyEntity;
import javax.persistence.Access;
import javax.persistence.AccessType;
@@ -53,6 +54,10 @@ public class DomainDsDataHistory extends DomainDsDataBase implements SqlOnlyEnti
return instance;
}
public DomainHistory.DomainHistoryId getDomainHistoryId() {
return new DomainHistoryId(getDomainRepoId(), domainHistoryRevisionId);
}
@Override
@Access(AccessType.PROPERTY)
public String getDomainRepoId() {
@@ -196,7 +196,8 @@ public class HostBase extends EppResource {
public B setHostName(String hostName) {
checkArgument(
hostName.equals(canonicalizeDomainName(hostName)),
"Host name must be in puny-coded, lower-case form");
"Host name %s not in puny-coded, lower-case form",
hostName);
getInstance().fullyQualifiedHostName = hostName;
return thisCastToDerived();
}
@@ -22,6 +22,7 @@ import com.googlecode.objectify.annotation.Embed;
import com.googlecode.objectify.annotation.Ignore;
import google.registry.model.Buildable;
import google.registry.model.ImmutableObject;
import google.registry.model.domain.DomainHistory.DomainHistoryId;
import google.registry.model.replay.DatastoreAndSqlEntity;
import javax.persistence.Column;
import javax.persistence.Entity;
@@ -49,7 +50,6 @@ public class DomainTransactionRecord extends ImmutableObject
@Id
@Ignore
@ImmutableObject.DoNotCompare
@GeneratedValue(strategy = GenerationType.IDENTITY)
@ImmutableObject.Insignificant
Long id;
@@ -58,6 +58,14 @@ public class DomainTransactionRecord extends ImmutableObject
@Column(nullable = false)
String tld;
// The following two fields are exposed in this entity to support bulk-loading in Cloud SQL by the
// Datastore-SQL validation. They are excluded from equality check since they are not set in
// Datastore.
// TODO(b/203609782): post migration, decide whether to keep these two fields.
@Ignore @ImmutableObject.Insignificant String domainRepoId;
@Ignore @ImmutableObject.Insignificant Long historyRevisionId;
/**
* The time this Transaction takes effect (counting grace periods and other nuances).
*
@@ -174,6 +182,10 @@ public class DomainTransactionRecord extends ImmutableObject
}
}
public DomainHistoryId getDomainHistoryId() {
return new DomainHistoryId(domainRepoId, historyRevisionId);
}
public DateTime getReportingTime() {
return reportingTime;
}
@@ -15,7 +15,7 @@
package google.registry.model.tmch;
import google.registry.model.ImmutableObject;
import google.registry.model.replay.NonReplicatedEntity;
import google.registry.model.replay.SqlOnlyEntity;
import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Entity;
@@ -30,7 +30,7 @@ import javax.persistence.Id;
* work.
*/
@Entity(name = "ClaimsEntry")
class ClaimsEntry extends ImmutableObject implements NonReplicatedEntity, Serializable {
class ClaimsEntry extends ImmutableObject implements SqlOnlyEntity, Serializable {
@Id private Long revisionId;
@Id private String domainLabel;
@@ -33,6 +33,7 @@ import google.registry.batch.ResaveAllEppResourcesAction;
import google.registry.batch.ResaveEntityAction;
import google.registry.batch.SendExpiringCertificateNotificationEmailAction;
import google.registry.batch.WipeOutCloudSqlAction;
import google.registry.batch.WipeOutContactHistoryPiiAction;
import google.registry.batch.WipeoutDatastoreAction;
import google.registry.cron.CommitLogFanoutAction;
import google.registry.cron.CronModule;
@@ -219,6 +220,8 @@ interface BackendRequestComponent {
WipeoutDatastoreAction wipeoutDatastoreAction();
WipeOutContactHistoryPiiAction wipeOutContactHistoryPiiAction();
@Subcomponent.Builder
abstract class Builder implements RequestComponentBuilder<BackendRequestComponent> {
@@ -0,0 +1,65 @@
// 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.persistence;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Streams;
import google.registry.model.bulkquery.BulkQueryEntities;
import google.registry.persistence.transaction.JpaTransactionManager;
import google.registry.persistence.transaction.JpaTransactionManagerImpl;
import google.registry.util.Clock;
import java.util.List;
import javax.persistence.EntityManagerFactory;
import org.hibernate.jpa.boot.internal.ParsedPersistenceXmlDescriptor;
import org.hibernate.jpa.boot.spi.Bootstrap;
/**
* Defines factory method for instantiating the bulk-query optimized {@link JpaTransactionManager}.
*/
public final class BulkQueryJpaFactory {
private BulkQueryJpaFactory() {}
static EntityManagerFactory createBulkQueryEntityManagerFactory(
ImmutableMap<String, String> cloudSqlConfigs) {
ParsedPersistenceXmlDescriptor descriptor =
PersistenceXmlUtility.getParsedPersistenceXmlDescriptor();
List<String> updatedManagedClasses =
Streams.concat(
descriptor.getManagedClassNames().stream(),
BulkQueryEntities.JPA_ENTITIES_NEW.stream())
.map(
name -> {
if (BulkQueryEntities.JPA_ENTITIES_REPLACEMENTS.containsKey(name)) {
return BulkQueryEntities.JPA_ENTITIES_REPLACEMENTS.get(name);
}
return name;
})
.collect(ImmutableList.toImmutableList());
descriptor.getManagedClassNames().clear();
descriptor.getManagedClassNames().addAll(updatedManagedClasses);
return Bootstrap.getEntityManagerFactoryBuilder(descriptor, cloudSqlConfigs).build();
}
public static JpaTransactionManager createBulkQueryJpaTransactionManager(
ImmutableMap<String, String> cloudSqlConfigs, Clock clock) {
return new JpaTransactionManagerImpl(
createBulkQueryEntityManagerFactory(cloudSqlConfigs), clock);
}
}
@@ -21,6 +21,7 @@ import static google.registry.config.RegistryConfig.getHibernateHikariIdleTimeou
import static google.registry.config.RegistryConfig.getHibernateHikariMaximumPoolSize;
import static google.registry.config.RegistryConfig.getHibernateHikariMinimumIdle;
import static google.registry.config.RegistryConfig.getHibernateLogSqlQueries;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.google.api.client.auth.oauth2.Credential;
import com.google.common.annotations.VisibleForTesting;
@@ -34,6 +35,7 @@ import google.registry.config.RegistryConfig.Config;
import google.registry.persistence.transaction.CloudSqlCredentialSupplier;
import google.registry.persistence.transaction.JpaTransactionManager;
import google.registry.persistence.transaction.JpaTransactionManagerImpl;
import google.registry.persistence.transaction.TransactionManager;
import google.registry.privileges.secretmanager.SqlCredential;
import google.registry.privileges.secretmanager.SqlCredentialStore;
import google.registry.privileges.secretmanager.SqlUser;
@@ -150,13 +152,36 @@ public abstract class PersistenceModule {
@Singleton
@BeamPipelineCloudSqlConfigs
static ImmutableMap<String, String> provideBeamPipelineCloudSqlConfigs(
@Config("beamCloudSqlJdbcUrl") String jdbcUrl,
@Config("beamCloudSqlInstanceConnectionName") String instanceConnectionName,
@DefaultHibernateConfigs ImmutableMap<String, String> defaultConfigs,
SqlCredentialStore credentialStore,
@Config("instanceConnectionNameOverride")
Optional<Provider<String>> instanceConnectionNameOverride,
@Config("beamIsolationOverride")
Optional<Provider<TransactionIsolationLevel>> isolationOverride) {
return createPartialSqlConfigs(
jdbcUrl, instanceConnectionName, defaultConfigs, isolationOverride);
Optional<Provider<TransactionIsolationLevel>> isolationOverride,
@PartialCloudSqlConfigs ImmutableMap<String, String> cloudSqlConfigs) {
HashMap<String, String> overrides = Maps.newHashMap(cloudSqlConfigs);
// TODO(b/175700623): make sql username configurable from config file.
SqlCredential credential = credentialStore.getCredential(new RobotUser(RobotId.NOMULUS));
overrides.put(Environment.USER, credential.login());
overrides.put(Environment.PASS, credential.password());
// Override the default minimum which is tuned for the Registry server. A worker VM should
// release all connections if it no longer interacts with the database.
overrides.put(HIKARI_MINIMUM_IDLE, "0");
/**
* Disable Hikari's maxPoolSize limit check by setting it to an absurdly large number. The
* effective (and desirable) limit is the number of pipeline threads on the pipeline worker,
* which can be configured using pipeline options. See {@link RegistryPipelineOptions} for more
* information.
*/
overrides.put(HIKARI_MAXIMUM_POOL_SIZE, String.valueOf(Integer.MAX_VALUE));
instanceConnectionNameOverride
.map(Provider::get)
.ifPresent(
instanceConnectionName ->
overrides.put(HIKARI_DS_CLOUD_SQL_INSTANCE, instanceConnectionName));
isolationOverride
.map(Provider::get)
.ifPresent(isolation -> overrides.put(Environment.ISOLATION, isolation.name()));
return ImmutableMap.copyOf(overrides);
}
@VisibleForTesting
@@ -206,6 +231,12 @@ public abstract class PersistenceModule {
}
}
@Provides
@Singleton
static TransactionManager provideTransactionManager() {
return tm();
}
@Provides
@Singleton
@AppEngineJpaTm
@@ -222,37 +253,17 @@ public abstract class PersistenceModule {
@Singleton
@BeamJpaTm
static JpaTransactionManager provideBeamJpaTm(
SqlCredentialStore credentialStore,
@Config("instanceConnectionNameOverride")
Optional<Provider<String>> instanceConnectionNameOverride,
@Config("beamIsolationOverride")
Optional<Provider<TransactionIsolationLevel>> isolationOverride,
@PartialCloudSqlConfigs ImmutableMap<String, String> cloudSqlConfigs,
Clock clock) {
HashMap<String, String> overrides = Maps.newHashMap(cloudSqlConfigs);
// TODO(b/175700623): make sql username configurable from config file.
SqlCredential credential = credentialStore.getCredential(new RobotUser(RobotId.NOMULUS));
overrides.put(Environment.USER, credential.login());
overrides.put(Environment.PASS, credential.password());
// Override the default minimum which is tuned for the Registry server. A worker VM should
// release all connections if it no longer interacts with the database.
overrides.put(HIKARI_MINIMUM_IDLE, "0");
/**
* Disable Hikari's maxPoolSize limit check by setting it to an absurdly large number. The
* effective (and desirable) limit is the number of pipeline threads on the pipeline worker,
* which can be configured using pipeline options. See {@link RegistryPipelineOptions} for more
* information.
*/
overrides.put(HIKARI_MAXIMUM_POOL_SIZE, String.valueOf(Integer.MAX_VALUE));
instanceConnectionNameOverride
.map(Provider::get)
.ifPresent(
instanceConnectionName ->
overrides.put(HIKARI_DS_CLOUD_SQL_INSTANCE, instanceConnectionName));
isolationOverride
.map(Provider::get)
.ifPresent(isolation -> overrides.put(Environment.ISOLATION, isolation.name()));
return new JpaTransactionManagerImpl(create(overrides), clock);
@BeamPipelineCloudSqlConfigs ImmutableMap<String, String> beamCloudSqlConfigs, Clock clock) {
return new JpaTransactionManagerImpl(create(beamCloudSqlConfigs), clock);
}
@Provides
@Singleton
@BeamBulkQueryJpaTm
static JpaTransactionManager provideBeamBulkQueryJpaTm(
@BeamPipelineCloudSqlConfigs ImmutableMap<String, String> beamCloudSqlConfigs, Clock clock) {
return new JpaTransactionManagerImpl(
BulkQueryJpaFactory.createBulkQueryEntityManagerFactory(beamCloudSqlConfigs), clock);
}
@Provides
@@ -338,6 +349,17 @@ public abstract class PersistenceModule {
}
}
/** Types of {@link JpaTransactionManager JpaTransactionManagers}. */
public enum JpaTransactionManagerType {
/** The regular {@link JpaTransactionManager} for general use. */
REGULAR,
/**
* The {@link JpaTransactionManager} optimized for bulk loading multi-level JPA entities. Please
* see {@link google.registry.model.bulkquery.BulkQueryEntities} for more information.
*/
BULK_QUERY
}
/** Dagger qualifier for JDBC {@link Connection} with schema management privilege. */
@Qualifier
@Documented
@@ -349,11 +371,18 @@ public abstract class PersistenceModule {
@interface AppEngineJpaTm {}
/** Dagger qualifier for {@link JpaTransactionManager} used inside BEAM pipelines. */
// Note: @SocketFactoryJpaTm will be phased out in favor of this qualifier.
@Qualifier
@Documented
public @interface BeamJpaTm {}
/**
* Dagger qualifier for {@link JpaTransactionManager} that uses an alternative entity model for
* faster bulk queries.
*/
@Qualifier
@Documented
public @interface BeamBulkQueryJpaTm {}
/** Dagger qualifier for {@link JpaTransactionManager} used for Nomulus tool. */
@Qualifier
@Documented
@@ -98,14 +98,16 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
// EntityManagerFactory is thread safe.
private final EntityManagerFactory emf;
private final Clock clock;
// TODO(b/177588434): Investigate alternatives for managing transaction information. ThreadLocal
// adds an unnecessary restriction that each request has to be processed by one thread
// synchronously.
private final ThreadLocal<TransactionInfo> transactionInfo =
private static final ThreadLocal<TransactionInfo> transactionInfo =
ThreadLocal.withInitial(TransactionInfo::new);
// If this value is present, use it to determine whether or not to replay SQL transactions to
// Datastore, rather than using the schedule stored in Datastore.
private static ThreadLocal<Optional<Boolean>> replaySqlToDatastoreOverrideForTest =
private static final ThreadLocal<Optional<Boolean>> replaySqlToDatastoreOverrideForTest =
ThreadLocal.withInitial(Optional::empty);
public JpaTransactionManagerImpl(EntityManagerFactory emf, Clock clock) {
@@ -132,13 +132,16 @@ public class TransactionManagerFactory {
/**
* Sets the return of {@link #tm()} to the given instance of {@link TransactionManager}.
*
* <p>DO NOT CALL THIS DIRECTLY IF POSSIBLE. Strongly prefer the use of <code>TmOverrideExtension
* </code> in test code instead.
*
* <p>Used when overriding the per-test transaction manager for dual-database tests. Should be
* matched with a corresponding invocation of {@link #removeTmOverrideForTest()} either at the end
* of the test or in an <code>@AfterEach</code> handler.
*/
@VisibleForTesting
public static void setTmForTest(TransactionManager newTm) {
tmForTest = Optional.of(newTm);
public static void setTmOverrideForTest(TransactionManager newTmOverride) {
tmForTest = Optional.of(newTmOverride);
}
/** Resets the overridden transaction manager post-test. */
@@ -152,10 +155,10 @@ public class TransactionManagerFactory {
}
}
/** Thrown when a write is attempted when the DB is in read-only mode. */
/** Registry is currently undergoing maintenance and is in read-only mode. */
public static class ReadOnlyModeException extends IllegalStateException {
public ReadOnlyModeException() {
super("Registry is currently in read-only mode");
ReadOnlyModeException() {
super("Registry is currently undergoing maintenance and is in read-only mode");
}
}
}
@@ -31,6 +31,7 @@ import google.registry.request.auth.Auth;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Optional;
import javax.inject.Inject;
import org.bouncycastle.openpgp.PGPKeyPair;
import org.bouncycastle.openpgp.PGPPrivateKey;
@@ -60,7 +61,7 @@ import org.joda.time.DateTime;
auth = Auth.AUTH_INTERNAL_OR_ADMIN)
public final class BrdaCopyAction implements Runnable {
static final String PATH = "/_dr/task/brdaCopy";
public static final String PATH = "/_dr/task/brdaCopy";
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@@ -69,6 +70,7 @@ public final class BrdaCopyAction implements Runnable {
@Inject @Config("rdeBucket") String stagingBucket;
@Inject @Parameter(RequestParameters.PARAM_TLD) String tld;
@Inject @Parameter(RdeModule.PARAM_WATERMARK) DateTime watermark;
@Inject @Parameter(RdeModule.PARAM_PREFIX) Optional<String> prefix;
@Inject @Key("brdaReceiverKey") PGPPublicKey receiverKey;
@Inject @Key("brdaSigningKey") PGPKeyPair signingKey;
@Inject @Key("rdeStagingDecryptionKey") PGPPrivateKey stagingDecryptionKey;
@@ -84,11 +86,12 @@ public final class BrdaCopyAction implements Runnable {
}
private void copyAsRyde() throws IOException {
String prefix = RdeNamingUtils.makeRydeFilename(tld, watermark, THIN, 1, 0);
BlobId xmlFilename = BlobId.of(stagingBucket, prefix + ".xml.ghostryde");
BlobId xmlLengthFilename = BlobId.of(stagingBucket, prefix + ".xml.length");
BlobId rydeFile = BlobId.of(brdaBucket, prefix + ".ryde");
BlobId sigFile = BlobId.of(brdaBucket, prefix + ".sig");
String nameWithoutPrefix = RdeNamingUtils.makeRydeFilename(tld, watermark, THIN, 1, 0);
String name = prefix.orElse("") + nameWithoutPrefix;
BlobId xmlFilename = BlobId.of(stagingBucket, name + ".xml.ghostryde");
BlobId xmlLengthFilename = BlobId.of(stagingBucket, name + ".xml.length");
BlobId rydeFile = BlobId.of(brdaBucket, nameWithoutPrefix + ".ryde");
BlobId sigFile = BlobId.of(brdaBucket, nameWithoutPrefix + ".sig");
long xmlLength = readXmlLength(xmlLengthFilename);
@@ -97,11 +100,12 @@ public final class BrdaCopyAction implements Runnable {
InputStream ghostrydeDecoder = Ghostryde.decoder(gcsInput, stagingDecryptionKey);
OutputStream rydeOut = gcsUtils.openOutputStream(rydeFile);
OutputStream sigOut = gcsUtils.openOutputStream(sigFile);
RydeEncoder rydeEncoder = new RydeEncoder.Builder()
.setRydeOutput(rydeOut, receiverKey)
.setSignatureOutput(sigOut, signingKey)
.setFileMetadata(prefix, xmlLength, watermark)
.build()) {
RydeEncoder rydeEncoder =
new RydeEncoder.Builder()
.setRydeOutput(rydeOut, receiverKey)
.setSignatureOutput(sigOut, signingKey)
.setFileMetadata(nameWithoutPrefix, xmlLength, watermark)
.build()) {
ByteStreams.copy(ghostrydeDecoder, rydeEncoder);
}
}
@@ -49,6 +49,10 @@ public abstract class RdeModule {
public static final String PARAM_MODE = "mode";
public static final String PARAM_REVISION = "revision";
public static final String PARAM_LENIENT = "lenient";
public static final String PARAM_PREFIX = "prefix";
public static final String RDE_UPLOAD_QUEUE = "rde-upload";
public static final String RDE_REPORT_QUEUE = "rde-report";
public static final String BRDA_QUEUE = "brda";
@Provides
@Parameter(PARAM_WATERMARK)
@@ -92,10 +96,11 @@ public abstract class RdeModule {
return extractBooleanParameter(req, PARAM_LENIENT);
}
// TODO (jianglai): Make it a required parameter once we migrate to Cloud SQL.
@Provides
@Named("brda")
static Queue provideQueueBrda() {
return getQueue("brda");
@Parameter(PARAM_PREFIX)
static Optional<String> providePrefix(HttpServletRequest req) {
return extractOptionalParameter(req, PARAM_PREFIX);
}
@Provides
@@ -68,6 +68,7 @@ public final class RdeReportAction implements Runnable, EscrowTask {
@Inject Response response;
@Inject RdeReporter reporter;
@Inject @Parameter(RequestParameters.PARAM_TLD) String tld;
@Inject @Parameter(RdeModule.PARAM_PREFIX) Optional<String> prefix;
@Inject @Config("rdeBucket") String bucket;
@Inject @Config("rdeInterval") Duration interval;
@Inject @Config("rdeReportLockTimeout") Duration timeout;
@@ -96,8 +97,9 @@ public final class RdeReportAction implements Runnable, EscrowTask {
RdeRevision.getCurrentRevision(tld, watermark, FULL)
.orElseThrow(
() -> new IllegalStateException("RdeRevision was not set on generated deposit"));
String prefix = RdeNamingUtils.makeRydeFilename(tld, watermark, FULL, 1, revision);
BlobId reportFilename = BlobId.of(bucket, prefix + "-report.xml.ghostryde");
String name =
prefix.orElse("") + RdeNamingUtils.makeRydeFilename(tld, watermark, FULL, 1, revision);
BlobId reportFilename = BlobId.of(bucket, name + "-report.xml.ghostryde");
verify(gcsUtils.existsAndNotEmpty(reportFilename), "Missing file: %s", reportFilename);
reporter.send(readReportFromGcs(reportFilename));
response.setContentType(PLAIN_TEXT_UTF_8);
@@ -14,20 +14,33 @@
package google.registry.rde;
import static google.registry.beam.BeamUtils.createJobName;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.request.Action.Method.GET;
import static google.registry.request.Action.Method.POST;
import static google.registry.xml.ValidationMode.LENIENT;
import static google.registry.xml.ValidationMode.STRICT;
import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT;
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.base.Ascii;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.collect.Multimaps;
import com.google.common.flogger.FluentLogger;
import com.google.common.io.BaseEncoding;
import google.registry.beam.rde.RdePipeline;
import google.registry.config.RegistryConfig.Config;
import google.registry.config.RegistryEnvironment;
import google.registry.gcs.GcsUtils;
import google.registry.keyring.api.KeyModule.Key;
import google.registry.mapreduce.MapreduceRunner;
import google.registry.mapreduce.inputs.EppResourceInputs;
import google.registry.mapreduce.inputs.NullInput;
@@ -47,6 +60,7 @@ import google.registry.request.Response;
import google.registry.request.auth.Auth;
import google.registry.util.Clock;
import google.registry.xml.ValidationMode;
import java.io.IOException;
import java.util.Optional;
import javax.inject.Inject;
import org.joda.time.DateTime;
@@ -201,6 +215,7 @@ public final class RdeStagingAction implements Runnable {
public static final String PATH = "/_dr/task/rdeStaging";
private static final String PIPELINE_NAME = "rde_pipeline";
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@Inject Clock clock;
@@ -209,7 +224,11 @@ public final class RdeStagingAction implements Runnable {
@Inject Response response;
@Inject GcsUtils gcsUtils;
@Inject MapreduceRunner mrRunner;
@Inject @Config("projectId") String projectId;
@Inject @Config("defaultJobRegion") String jobRegion;
@Inject @Config("transactionCooldown") Duration transactionCooldown;
@Inject @Config("beamStagingBucketUrl") String stagingBucketUrl;
@Inject @Config("rdeBucket") String rdeBucket;
@Inject @Parameter(RdeModule.PARAM_MANUAL) boolean manual;
@Inject @Parameter(RdeModule.PARAM_DIRECTORY) Optional<String> directory;
@Inject @Parameter(RdeModule.PARAM_MODE) ImmutableSet<String> modeStrings;
@@ -217,7 +236,8 @@ public final class RdeStagingAction implements Runnable {
@Inject @Parameter(RdeModule.PARAM_WATERMARKS) ImmutableSet<DateTime> watermarks;
@Inject @Parameter(RdeModule.PARAM_REVISION) Optional<Integer> revision;
@Inject @Parameter(RdeModule.PARAM_LENIENT) boolean lenient;
@Inject @Key("rdeStagingEncryptionKey") byte[] stagingKeyBytes;
@Inject Dataflow dataflow;
@Inject RdeStagingAction() {}
@Override
@@ -228,27 +248,66 @@ public final class RdeStagingAction implements Runnable {
String message = "Nothing needs to be deposited.";
logger.atInfo().log(message);
response.setStatus(SC_NO_CONTENT);
response.setPayload(message);
// No need to set payload as HTTP 204 response status code does not allow a payload.
return;
}
for (PendingDeposit pending : pendings.values()) {
logger.atInfo().log("Pending deposit: %s", pending);
}
ValidationMode validationMode = lenient ? LENIENT : STRICT;
RdeStagingMapper mapper = new RdeStagingMapper(validationMode, pendings);
RdeStagingReducer reducer = reducerFactory.create(validationMode, gcsUtils);
mrRunner
.setJobName("Stage escrow deposits for all TLDs")
.setModuleName("backend")
.setDefaultReduceShards(pendings.size())
.runMapreduce(
mapper,
reducer,
ImmutableList.of(
// Add an extra shard that maps over a null resource. See the mapper code for why.
new NullInput<>(), EppResourceInputs.createEntityInput(EppResource.class)))
.sendLinkToMapreduceConsole(response);
if (tm().isOfy()) {
RdeStagingMapper mapper = new RdeStagingMapper(validationMode, pendings);
RdeStagingReducer reducer = reducerFactory.create(validationMode, gcsUtils);
mrRunner
.setJobName("Stage escrow deposits for all TLDs")
.setModuleName("backend")
.setDefaultReduceShards(pendings.size())
.runMapreduce(
mapper,
reducer,
ImmutableList.of(
// Add an extra shard that maps over a null resource. See the mapper code for why.
new NullInput<>(), EppResourceInputs.createEntityInput(EppResource.class)))
.sendLinkToMapreduceConsole(response);
} else {
try {
LaunchFlexTemplateParameter parameter =
new LaunchFlexTemplateParameter()
.setJobName(createJobName("rde", clock))
.setContainerSpecGcsPath(
String.format("%s/%s_metadata.json", stagingBucketUrl, PIPELINE_NAME))
.setParameters(
ImmutableMap.of(
"pendings",
RdePipeline.encodePendings(pendings),
"validationMode",
validationMode.name(),
"rdeStagingBucket",
rdeBucket,
"stagingKey",
BaseEncoding.base64Url().omitPadding().encode(stagingKeyBytes),
"registryEnvironment",
RegistryEnvironment.get().name()));
LaunchFlexTemplateResponse launchResponse =
dataflow
.projects()
.locations()
.flexTemplates()
.launch(
projectId,
jobRegion,
new LaunchFlexTemplateRequest().setLaunchParameter(parameter))
.execute();
logger.atInfo().log("Got response: %s", launchResponse.getJob().toPrettyString());
response.setStatus(SC_OK);
response.setPayload(
String.format("Launched RDE pipeline: %s", launchResponse.getJob().getId()));
} catch (IOException e) {
logger.atWarning().withCause(e).log("Pipeline Launch failed");
response.setStatus(SC_INTERNAL_SERVER_ERROR);
response.setPayload(String.format("Pipeline launch failed: %s", e.getMessage()));
}
}
}
private ImmutableSetMultimap<String, PendingDeposit> getStandardPendingDeposits() {
@@ -14,7 +14,6 @@
package google.registry.rde;
import static com.google.appengine.api.taskqueue.TaskOptions.Builder.withUrl;
import static com.google.common.base.Verify.verify;
import static com.google.common.net.MediaType.PLAIN_TEXT_UTF_8;
import static com.jcraft.jsch.ChannelSftp.OVERWRITE;
@@ -24,14 +23,15 @@ import static google.registry.model.common.Cursor.getCursorTimeOrStartOfTime;
import static google.registry.model.rde.RdeMode.FULL;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.persistence.transaction.TransactionManagerUtil.transactIfJpaTm;
import static google.registry.rde.RdeModule.RDE_REPORT_QUEUE;
import static google.registry.request.Action.Method.POST;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static google.registry.util.DateTimeUtils.isBeforeOrAt;
import static java.util.Arrays.asList;
import com.google.appengine.api.taskqueue.Queue;
import com.google.cloud.storage.BlobId;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.HashMultimap;
import com.google.common.flogger.FluentLogger;
import com.google.common.io.ByteStreams;
import com.jcraft.jsch.JSch;
@@ -49,14 +49,15 @@ import google.registry.model.tld.Registry;
import google.registry.rde.EscrowTaskRunner.EscrowTask;
import google.registry.rde.JSchSshSession.JSchSshSessionFactory;
import google.registry.request.Action;
import google.registry.request.Action.Service;
import google.registry.request.HttpException.NoContentException;
import google.registry.request.Parameter;
import google.registry.request.RequestParameters;
import google.registry.request.Response;
import google.registry.request.auth.Auth;
import google.registry.util.Clock;
import google.registry.util.CloudTasksUtils;
import google.registry.util.Retrier;
import google.registry.util.TaskQueueUtils;
import google.registry.util.TeeOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
@@ -66,7 +67,6 @@ import java.io.OutputStream;
import java.net.URI;
import java.util.Optional;
import javax.inject.Inject;
import javax.inject.Named;
import org.bouncycastle.openpgp.PGPKeyPair;
import org.bouncycastle.openpgp.PGPPrivateKey;
import org.bouncycastle.openpgp.PGPPublicKey;
@@ -90,7 +90,7 @@ import org.joda.time.Duration;
auth = Auth.AUTH_INTERNAL_OR_ADMIN)
public final class RdeUploadAction implements Runnable, EscrowTask {
static final String PATH = "/_dr/task/rdeUpload";
public static final String PATH = "/_dr/task/rdeUpload";
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@@ -109,9 +109,10 @@ public final class RdeUploadAction implements Runnable, EscrowTask {
@Inject JSchSshSessionFactory jschSshSessionFactory;
@Inject Response response;
@Inject SftpProgressMonitor sftpProgressMonitor;
@Inject TaskQueueUtils taskQueueUtils;
@Inject CloudTasksUtils cloudTasksUtils;
@Inject Retrier retrier;
@Inject @Parameter(RequestParameters.PARAM_TLD) String tld;
@Inject @Parameter(RdeModule.PARAM_PREFIX) Optional<String> prefix;
@Inject @Config("rdeBucket") String bucket;
@Inject @Config("rdeInterval") Duration interval;
@Inject @Config("rdeUploadLockTimeout") Duration timeout;
@@ -120,15 +121,21 @@ public final class RdeUploadAction implements Runnable, EscrowTask {
@Inject @Key("rdeReceiverKey") PGPPublicKey receiverKey;
@Inject @Key("rdeSigningKey") PGPKeyPair signingKey;
@Inject @Key("rdeStagingDecryptionKey") PGPPrivateKey stagingDecryptionKey;
@Inject @Named("rde-report") Queue reportQueue;
@Inject RdeUploadAction() {}
@Override
public void run() {
logger.atInfo().log("Attempting to acquire RDE upload lock for TLD '%s'.", tld);
runner.lockRunAndRollForward(this, Registry.get(tld), timeout, CursorType.RDE_UPLOAD, interval);
taskQueueUtils.enqueue(
reportQueue, withUrl(RdeReportAction.PATH).param(RequestParameters.PARAM_TLD, tld));
HashMultimap<String, String> params = HashMultimap.create();
params.put(RequestParameters.PARAM_TLD, tld);
if (prefix.isPresent()) {
params.put(RdeModule.PARAM_PREFIX, prefix.get());
}
cloudTasksUtils.enqueue(
RDE_REPORT_QUEUE,
CloudTasksUtils.createPostTask(
RdeReportAction.PATH, Service.BACKEND.getServiceId(), params));
}
@Override
@@ -164,7 +171,9 @@ public final class RdeUploadAction implements Runnable, EscrowTask {
RdeRevision.getCurrentRevision(tld, watermark, FULL)
.orElseThrow(
() -> new IllegalStateException("RdeRevision was not set on generated deposit"));
final String name = RdeNamingUtils.makeRydeFilename(tld, watermark, FULL, 1, revision);
final String nameWithoutPrefix =
RdeNamingUtils.makeRydeFilename(tld, watermark, FULL, 1, revision);
final String name = prefix.orElse("") + nameWithoutPrefix;
final BlobId xmlFilename = BlobId.of(bucket, name + ".xml.ghostryde");
final BlobId xmlLengthFilename = BlobId.of(bucket, name + ".xml.length");
BlobId reportFilename = BlobId.of(bucket, name + "-report.xml.ghostryde");
@@ -174,7 +183,8 @@ public final class RdeUploadAction implements Runnable, EscrowTask {
logger.atInfo().log("Commencing RDE upload for TLD '%s' to '%s'.", tld, uploadUrl);
final long xmlLength = readXmlLength(xmlLengthFilename);
retrier.callWithRetry(
() -> upload(xmlFilename, xmlLength, watermark, name), JSchException.class);
() -> upload(xmlFilename, xmlLength, watermark, name, nameWithoutPrefix),
JSchException.class);
logger.atInfo().log(
"Updating RDE cursor '%s' for TLD '%s' following successful upload.", RDE_UPLOAD_SFTP, tld);
tm().transact(
@@ -210,7 +220,8 @@ public final class RdeUploadAction implements Runnable, EscrowTask {
* }</pre>
*/
@VisibleForTesting
protected void upload(BlobId xmlFile, long xmlLength, DateTime watermark, String name)
protected void upload(
BlobId xmlFile, long xmlLength, DateTime watermark, String name, String nameWithoutPrefix)
throws Exception {
logger.atInfo().log("Uploading XML file '%s' to remote path '%s'.", xmlFile, uploadUrl);
try (InputStream gcsInput = gcsUtils.openInputStream(xmlFile);
@@ -218,8 +229,8 @@ public final class RdeUploadAction implements Runnable, EscrowTask {
try (JSchSshSession session = jschSshSessionFactory.create(lazyJsch.get(), uploadUrl);
JSchSftpChannel ftpChan = session.openSftpChannel()) {
ByteArrayOutputStream sigOut = new ByteArrayOutputStream();
String rydeFilename = name + ".ryde";
BlobId rydeGcsFilename = BlobId.of(bucket, rydeFilename);
String rydeFilename = nameWithoutPrefix + ".ryde";
BlobId rydeGcsFilename = BlobId.of(bucket, name + ".ryde");
try (OutputStream ftpOutput =
ftpChan.get().put(rydeFilename, sftpProgressMonitor, OVERWRITE);
OutputStream gcsOutput = gcsUtils.openOutputStream(rydeGcsFilename);
@@ -228,14 +239,15 @@ public final class RdeUploadAction implements Runnable, EscrowTask {
new RydeEncoder.Builder()
.setRydeOutput(teeOutput, receiverKey)
.setSignatureOutput(sigOut, signingKey)
.setFileMetadata(name, xmlLength, watermark)
.setFileMetadata(nameWithoutPrefix, xmlLength, watermark)
.build()) {
long bytesCopied = ByteStreams.copy(ghostrydeDecoder, rydeEncoder);
logger.atInfo().log("Uploaded %,d bytes to path '%s'.", bytesCopied, rydeFilename);
}
String sigFilename = name + ".sig";
String sigFilename = nameWithoutPrefix + ".sig";
BlobId sigGcsFilename = BlobId.of(bucket, name + ".sig");
byte[] signature = sigOut.toByteArray();
gcsUtils.createFromBytes(BlobId.of(bucket, sigFilename), signature);
gcsUtils.createFromBytes(sigGcsFilename, signature);
ftpChan.get().put(new ByteArrayInputStream(signature), sigFilename);
logger.atInfo().log("Uploaded %,d bytes to path '%s'.", signature.length, sigFilename);
}
@@ -30,6 +30,7 @@ import com.google.common.collect.ImmutableMap;
import com.google.common.flogger.FluentLogger;
import com.google.common.net.MediaType;
import google.registry.config.RegistryConfig.Config;
import google.registry.config.RegistryEnvironment;
import google.registry.model.common.DatabaseMigrationStateSchedule.PrimaryDatabase;
import google.registry.reporting.ReportingModule;
import google.registry.request.Action;
@@ -127,7 +128,9 @@ public class GenerateInvoicesAction implements Runnable {
"database",
database.name(),
"billingBucketUrl",
billingBucketUrl));
billingBucketUrl,
"registryEnvironment",
RegistryEnvironment.get().name()));
LaunchFlexTemplateResponse launchResponse =
dataflow
.projects()
@@ -16,6 +16,7 @@ package google.registry.reporting.icann;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.reporting.icann.IcannReportingModule.DATASTORE_EXPORT_DATA_SET;
import static google.registry.reporting.icann.IcannReportingModule.ICANN_REPORTING_DATA_SET;
import static google.registry.reporting.icann.QueryBuilderUtils.getQueryFromFile;
import static google.registry.reporting.icann.QueryBuilderUtils.getTableName;
@@ -23,6 +24,7 @@ import com.google.common.collect.ImmutableMap;
import google.registry.config.RegistryConfig.Config;
import google.registry.util.SqlTemplate;
import javax.inject.Inject;
import javax.inject.Named;
import org.joda.time.LocalDate;
import org.joda.time.YearMonth;
import org.joda.time.format.DateTimeFormat;
@@ -38,29 +40,32 @@ public final class ActivityReportingQueryBuilder implements QueryBuilder {
static final String EPP_METRICS = "epp_metrics";
static final String WHOIS_COUNTS = "whois_counts";
static final String ACTIVITY_REPORT_AGGREGATION = "activity_report_aggregation";
final String BIGQUERY_DATA_SET = tm().isOfy() ? "icann_reporting" : "cloud_sql_icann_reporting";
private final String projectId;
private final DnsCountQueryCoordinator dnsCountQueryCoordinator;
private final String icannReportingDataSet;
@Inject
@Config("projectId")
String projectId;
@Inject DnsCountQueryCoordinator dnsCountQueryCoordinator;
@Inject
ActivityReportingQueryBuilder() {}
ActivityReportingQueryBuilder(
@Config("projectId") String projectId,
@Named(ICANN_REPORTING_DATA_SET) String icannReportingDataSet,
DnsCountQueryCoordinator dnsCountQueryCoordinator) {
this.projectId = projectId;
this.dnsCountQueryCoordinator = dnsCountQueryCoordinator;
this.icannReportingDataSet = icannReportingDataSet;
}
/** Returns the aggregate query which generates the activity report from the saved view. */
@Override
public String getReportQuery(YearMonth yearMonth) {
return String.format(
"#standardSQL\nSELECT * FROM `%s.%s.%s`",
projectId, BIGQUERY_DATA_SET, getTableName(ACTIVITY_REPORT_AGGREGATION, yearMonth));
projectId, icannReportingDataSet, getTableName(ACTIVITY_REPORT_AGGREGATION, yearMonth));
}
/** Sets the month we're doing activity reporting for, and returns the view query map. */
@Override
public ImmutableMap<String, String> getViewQueryMap(YearMonth yearMonth) {
LocalDate firstDayOfMonth = yearMonth.toLocalDate(1);
// The pattern-matching is inclusive, so we subtract 1 day to only report that month's data.
LocalDate lastDayOfMonth = yearMonth.toLocalDate(1).plusMonths(1).minusDays(1);
@@ -102,7 +107,7 @@ public final class ActivityReportingQueryBuilder implements QueryBuilder {
String eppQuery =
SqlTemplate.create(getQueryFromFile("epp_metrics.sql"))
.put("PROJECT_ID", projectId)
.put("ICANN_REPORTING_DATA_SET", BIGQUERY_DATA_SET)
.put("ICANN_REPORTING_DATA_SET", icannReportingDataSet)
.put("MONTHLY_LOGS_TABLE", getTableName(MONTHLY_LOGS, yearMonth))
// All metadata logs for reporting come from google.registry.flows.FlowReporter.
.put(
@@ -114,7 +119,7 @@ public final class ActivityReportingQueryBuilder implements QueryBuilder {
String whoisQuery =
SqlTemplate.create(getQueryFromFile("whois_counts.sql"))
.put("PROJECT_ID", projectId)
.put("ICANN_REPORTING_DATA_SET", BIGQUERY_DATA_SET)
.put("ICANN_REPORTING_DATA_SET", icannReportingDataSet)
.put("MONTHLY_LOGS_TABLE", getTableName(MONTHLY_LOGS, yearMonth))
.build();
queriesBuilder.put(getTableName(WHOIS_COUNTS, yearMonth), whoisQuery);
@@ -129,7 +134,7 @@ public final class ActivityReportingQueryBuilder implements QueryBuilder {
.put(
"REGISTRAR_OPERATING_STATUS_TABLE",
getTableName(REGISTRAR_OPERATING_STATUS, yearMonth))
.put("ICANN_REPORTING_DATA_SET", BIGQUERY_DATA_SET)
.put("ICANN_REPORTING_DATA_SET", icannReportingDataSet)
.put("DNS_COUNTS_TABLE", getTableName(DNS_COUNTS, yearMonth))
.put("EPP_METRICS_TABLE", getTableName(EPP_METRICS, yearMonth))
.put("WHOIS_COUNTS_TABLE", getTableName(WHOIS_COUNTS, yearMonth));
@@ -147,7 +152,7 @@ public final class ActivityReportingQueryBuilder implements QueryBuilder {
return queriesBuilder.build();
}
public void prepareForQuery(YearMonth yearMonth) throws InterruptedException {
void prepareForQuery(YearMonth yearMonth) throws InterruptedException {
dnsCountQueryCoordinator.prepareForQuery(yearMonth);
}
}
@@ -14,7 +14,6 @@
package google.registry.reporting.icann;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.request.RequestParameters.extractOptionalParameter;
import static google.registry.request.RequestParameters.extractRequiredParameter;
import static google.registry.request.RequestParameters.extractSetOfEnumParameters;
@@ -24,9 +23,11 @@ import com.google.common.util.concurrent.MoreExecutors;
import dagger.Module;
import dagger.Provides;
import google.registry.bigquery.BigqueryConnection;
import google.registry.persistence.transaction.TransactionManager;
import google.registry.request.HttpException.BadRequestException;
import google.registry.request.Parameter;
import java.util.Optional;
import javax.inject.Named;
import javax.servlet.http.HttpServletRequest;
import org.joda.time.Duration;
@@ -42,8 +43,7 @@ public final class IcannReportingModule {
static final String PARAM_SUBDIR = "subdir";
static final String PARAM_REPORT_TYPES = "reportTypes";
static final String ICANN_REPORTING_DATA_SET =
tm().isOfy() ? "icann_reporting" : "cloud_sql_icann_reporting";
static final String ICANN_REPORTING_DATA_SET = "icannReportingDataSet";
static final String DATASTORE_EXPORT_DATA_SET = "latest_datastore_export";
static final String MANIFEST_FILE_NAME = "MANIFEST.txt";
@@ -88,11 +88,12 @@ public final class IcannReportingModule {
*/
@Provides
static BigqueryConnection provideBigqueryConnection(
BigqueryConnection.Builder bigQueryConnectionBuilder) {
BigqueryConnection.Builder bigQueryConnectionBuilder,
@Named(ICANN_REPORTING_DATA_SET) String icannReportingDataSet) {
try {
return bigQueryConnectionBuilder
.setExecutorService(MoreExecutors.newDirectExecutorService())
.setDatasetId(ICANN_REPORTING_DATA_SET)
.setDatasetId(icannReportingDataSet)
.setOverwrite(true)
.setPollInterval(Duration.standardSeconds(1))
.build();
@@ -100,4 +101,10 @@ public final class IcannReportingModule {
throw new RuntimeException("Could not initialize BigqueryConnection!", e);
}
}
@Provides
@Named(ICANN_REPORTING_DATA_SET)
static String provideIcannReportingDataSet(TransactionManager tm) {
return tm.isOfy() ? "icann_reporting" : "cloud_sql_icann_reporting";
}
}
@@ -14,7 +14,6 @@
package google.registry.reporting.icann;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.net.MediaType.PLAIN_TEXT_UTF_8;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.request.Action.Method.POST;
@@ -46,7 +45,6 @@ import google.registry.util.SendEmailService;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.inject.Inject;
@@ -78,6 +76,7 @@ public final class IcannReportingUploadAction implements Runnable {
static final String PATH = "/_dr/task/icannReportingUpload";
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private static final String LOCK_NAME = "IcannReportingUploadAction";
@Inject
@Config("reportingBucket")
@@ -98,48 +97,33 @@ public final class IcannReportingUploadAction implements Runnable {
@Override
public void run() {
Runnable transactional =
() -> {
ImmutableMap.Builder<String, Boolean> reportSummaryBuilder = new ImmutableMap.Builder<>();
ImmutableMap<Cursor, String> cursors = loadCursors();
// If cursor time is before now, upload the corresponding report
cursors.entrySet().stream()
.filter(entry -> entry.getKey().getCursorTime().isBefore(clock.nowUtc()))
.forEach(
entry -> {
DateTime cursorTime = entry.getKey().getCursorTime();
uploadReport(
cursorTime,
entry.getKey().getType(),
entry.getValue(),
reportSummaryBuilder);
});
// Send email of which reports were uploaded
emailUploadResults(reportSummaryBuilder.build());
response.setStatus(SC_OK);
response.setContentType(PLAIN_TEXT_UTF_8);
};
Callable<Void> lockRunner =
() -> {
tm().transact(transactional);
return null;
};
String lockname = "IcannReportingUploadAction";
if (!lockHandler.executeWithLocks(lockRunner, null, Duration.standardHours(2), lockname)) {
throw new ServiceUnavailableException("Lock for IcannReportingUploadAction already in use");
if (!lockHandler.executeWithLocks(
this::runWithLock, null, Duration.standardHours(2), LOCK_NAME)) {
throw new ServiceUnavailableException(String.format("Lock for %s already in use", LOCK_NAME));
}
}
private Void runWithLock() {
ImmutableMap.Builder<String, Boolean> reportSummaryBuilder = new ImmutableMap.Builder<>();
ImmutableMap<Cursor, String> cursors = tm().transact(this::loadCursors);
// If cursor time is before now, upload the corresponding report
cursors.entrySet().stream()
.filter(entry -> entry.getKey().getCursorTime().isBefore(clock.nowUtc()))
.forEach(entry -> uploadReport(entry.getKey(), entry.getValue(), reportSummaryBuilder));
// Send email of which reports were uploaded
emailUploadResults(reportSummaryBuilder.build());
response.setStatus(SC_OK);
response.setContentType(PLAIN_TEXT_UTF_8);
return null;
}
/** Uploads the report and rolls forward the cursor for that report. */
private void uploadReport(
DateTime cursorTime,
CursorType cursorType,
String tldStr,
ImmutableMap.Builder<String, Boolean> reportSummaryBuilder) {
Cursor cursor, String tldStr, ImmutableMap.Builder<String, Boolean> reportSummaryBuilder) {
DateTime cursorTime = cursor.getCursorTime();
CursorType cursorType = cursor.getType();
DateTime cursorTimeMinusMonth = cursorTime.withDayOfMonth(1).minusMonths(1);
String reportSubdir =
String.format(
@@ -150,17 +134,16 @@ public final class IcannReportingUploadAction implements Runnable {
BlobId.of(reportingBucket, String.format("%s/%s", reportSubdir, filename));
logger.atInfo().log("Reading ICANN report %s from bucket '%s'.", filename, reportingBucket);
// Check that the report exists
try {
verifyFileExists(gcsFilename);
} catch (IllegalArgumentException e) {
if (!gcsUtils.existsAndNotEmpty(gcsFilename)) {
String logMessage =
String.format(
"Could not upload %s report for %s because file %s did not exist.",
cursorType, tldStr, filename);
"Could not upload %s report for %s because file %s (object %s in bucket %s) did not"
+ " exist.",
cursorType, tldStr, filename, gcsFilename.getName(), gcsFilename.getBucket());
if (clock.nowUtc().dayOfMonth().get() == 1) {
logger.atInfo().withCause(e).log(logMessage + " This report may not have been staged yet.");
logger.atInfo().log(logMessage + " This report may not have been staged yet.");
} else {
logger.atSevere().withCause(e).log(logMessage);
logger.atSevere().log(logMessage);
}
reportSummaryBuilder.put(filename, false);
return;
@@ -179,7 +162,6 @@ public final class IcannReportingUploadAction implements Runnable {
} catch (RuntimeException e) {
logger.atWarning().withCause(e).log("Upload to %s failed.", gcsFilename);
}
reportSummaryBuilder.put(filename, success);
// Set cursor to first day of next month if the upload succeeded
if (success) {
@@ -188,8 +170,24 @@ public final class IcannReportingUploadAction implements Runnable {
cursorType,
cursorTime.withTimeAtStartOfDay().withDayOfMonth(1).plusMonths(1),
Registry.get(tldStr));
tm().put(newCursor);
// In order to keep the transactions short-lived, we load all of the cursors in a single
// transaction then later use per-cursor transactions when checking + saving the cursors. We
// run behind a lock so the cursors shouldn't be changed, but double check to be sure.
success =
tm().transact(
() -> {
Cursor fromDb = tm().transact(() -> tm().loadByEntity(cursor));
if (!cursor.equals(fromDb)) {
logger.atSevere().log(
"Expected previously-loaded cursor %s to equal current cursor %s",
cursor, fromDb);
return false;
}
tm().put(newCursor);
return true;
});
}
reportSummaryBuilder.put(filename, success);
}
private String getFileName(CursorType cursorType, DateTime cursorTime, String tld) {
@@ -303,13 +301,4 @@ public final class IcannReportingUploadAction implements Runnable {
return ByteStreams.toByteArray(gcsInput);
}
}
private void verifyFileExists(BlobId gcsFilename) {
checkArgument(
gcsUtils.existsAndNotEmpty(gcsFilename),
"Object %s in bucket %s not found",
gcsFilename.getName(),
gcsFilename.getBucket());
}
}
@@ -23,6 +23,7 @@ import com.google.common.collect.ImmutableMap;
import google.registry.config.RegistryConfig.Config;
import google.registry.util.SqlTemplate;
import javax.inject.Inject;
import javax.inject.Named;
import org.joda.time.DateTime;
import org.joda.time.LocalTime;
import org.joda.time.YearMonth;
@@ -34,9 +35,16 @@ import org.joda.time.format.DateTimeFormatter;
*/
public final class TransactionsReportingQueryBuilder implements QueryBuilder {
@Inject @Config("projectId") String projectId;
final String projectId;
private final String icannReportingDataSet;
@Inject TransactionsReportingQueryBuilder() {}
@Inject
TransactionsReportingQueryBuilder(
@Config("projectId") String projectId,
@Named(ICANN_REPORTING_DATA_SET) String icannReportingDataSet) {
this.projectId = projectId;
this.icannReportingDataSet = icannReportingDataSet;
}
static final String TRANSACTIONS_REPORT_AGGREGATION = "transactions_report_aggregation";
static final String REGISTRAR_IANA_ID = "registrar_iana_id";
@@ -51,9 +59,7 @@ public final class TransactionsReportingQueryBuilder implements QueryBuilder {
public String getReportQuery(YearMonth yearMonth) {
return String.format(
"#standardSQL\nSELECT * FROM `%s.%s.%s`",
projectId,
ICANN_REPORTING_DATA_SET,
getTableName(TRANSACTIONS_REPORT_AGGREGATION, yearMonth));
projectId, icannReportingDataSet, getTableName(TRANSACTIONS_REPORT_AGGREGATION, yearMonth));
}
/** Sets the month we're doing transactions reporting for, and returns the view query map. */
@@ -151,7 +157,7 @@ public final class TransactionsReportingQueryBuilder implements QueryBuilder {
.put("PROJECT_ID", projectId)
.put("DATASTORE_EXPORT_DATA_SET", DATASTORE_EXPORT_DATA_SET)
.put("REGISTRY_TABLE", "Registry")
.put("ICANN_REPORTING_DATA_SET", ICANN_REPORTING_DATA_SET)
.put("ICANN_REPORTING_DATA_SET", icannReportingDataSet)
.put("REGISTRAR_IANA_ID_TABLE", getTableName(REGISTRAR_IANA_ID, yearMonth))
.put("TOTAL_DOMAINS_TABLE", getTableName(TOTAL_DOMAINS, yearMonth))
.put("TOTAL_NAMESERVERS_TABLE", getTableName(TOTAL_NAMESERVERS, yearMonth))
@@ -18,6 +18,7 @@ import com.google.common.collect.ImmutableMap;
import google.registry.tools.javascrap.BackfillRegistryLocksCommand;
import google.registry.tools.javascrap.BackfillSpec11ThreatMatchesCommand;
import google.registry.tools.javascrap.DeleteContactByRoidCommand;
import google.registry.tools.javascrap.HardDeleteHostCommand;
import google.registry.tools.javascrap.PopulateNullRegistrarFieldsCommand;
import google.registry.tools.javascrap.RemoveIpAddressCommand;
import google.registry.tools.javascrap.ResaveAllTldsCommand;
@@ -86,6 +87,7 @@ public final class RegistryTool {
.put("get_sql_credential", GetSqlCredentialCommand.class)
.put("get_tld", GetTldCommand.class)
.put("ghostryde", GhostrydeCommand.class)
.put("hard_delete_host", HardDeleteHostCommand.class)
.put("hash_certificate", HashCertificateCommand.class)
.put("import_datastore", ImportDatastoreCommand.class)
.put("list_cursors", ListCursorsCommand.class)
@@ -123,7 +125,7 @@ public final class RegistryTool {
.put("update_allocation_tokens", UpdateAllocationTokensCommand.class)
.put("update_cursors", UpdateCursorsCommand.class)
.put("update_domain", UpdateDomainCommand.class)
.put("update_kms_keyring", UpdateKmsKeyringCommand.class)
.put("update_keyring_secret", UpdateKeyringSecretCommand.class)
.put("update_premium_list", UpdatePremiumListCommand.class)
.put("update_registrar", UpdateRegistrarCommand.class)
.put("update_reserved_list", UpdateReservedListCommand.class)
@@ -43,6 +43,7 @@ import google.registry.request.Modules.UserServiceModule;
import google.registry.tools.AuthModule.LocalCredentialModule;
import google.registry.tools.javascrap.BackfillRegistryLocksCommand;
import google.registry.tools.javascrap.DeleteContactByRoidCommand;
import google.registry.tools.javascrap.HardDeleteHostCommand;
import google.registry.util.UtilsModule;
import google.registry.whois.NonCachingWhoisModule;
import javax.annotation.Nullable;
@@ -124,6 +125,8 @@ interface RegistryToolComponent {
void inject(GhostrydeCommand command);
void inject(HardDeleteHostCommand command);
void inject(ImportDatastoreCommand command);
void inject(ListCursorsCommand command);
@@ -160,7 +163,7 @@ interface RegistryToolComponent {
void inject(UpdateDomainCommand command);
void inject(UpdateKmsKeyringCommand command);
void inject(UpdateKeyringSecretCommand command);
void inject(UpdateRegistrarCommand command);
@@ -18,6 +18,7 @@ import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Strings.isNullOrEmpty;
import static google.registry.model.EppResourceUtils.loadByForeignKey;
import static google.registry.util.CollectionUtils.findDuplicates;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import static google.registry.util.PreconditionsUtils.checkArgumentPresent;
import com.beust.jcommander.Parameter;
@@ -53,6 +54,17 @@ final class RenewDomainCommand extends MutatingEppToolCommand {
@Parameter(description = "Names of the domains to renew.", required = true)
private List<String> mainParameters;
@Parameter(
names = {"--reason"},
description = "Reason for the change.")
String reason;
@Parameter(
names = {"--registrar_request"},
description = "Whether the change was requested by a registrar.",
arity = 1)
Boolean requestedByRegistrar;
@Inject
Clock clock;
@@ -70,12 +82,23 @@ final class RenewDomainCommand extends MutatingEppToolCommand {
checkArgumentPresent(domainOptional, "Domain '%s' does not exist or is deleted", domainName);
setSoyTemplate(DomainRenewSoyInfo.getInstance(), DomainRenewSoyInfo.RENEWDOMAIN);
DomainBase domain = domainOptional.get();
addSoyRecord(
isNullOrEmpty(clientId) ? domain.getCurrentSponsorRegistrarId() : clientId,
SoyMapData soyMapData =
new SoyMapData(
"domainName", domain.getDomainName(),
"expirationDate", domain.getRegistrationExpirationTime().toString(DATE_FORMATTER),
"period", String.valueOf(period)));
"period", String.valueOf(period));
if (requestedByRegistrar != null) {
soyMapData.put("requestedByRegistrar", requestedByRegistrar.toString());
}
if (reason != null) {
checkArgumentNotNull(
requestedByRegistrar, "--registrar_request is required when --reason is specified");
soyMapData.put("reason", reason);
}
addSoyRecord(
isNullOrEmpty(clientId) ? domain.getCurrentSponsorRegistrarId() : clientId, soyMapData);
}
}
}
@@ -36,6 +36,7 @@ import google.registry.model.domain.DomainBase;
import google.registry.model.domain.secdns.DelegationSignerData;
import google.registry.model.eppcommon.StatusValue;
import google.registry.model.host.HostResource;
import google.registry.tools.soy.DomainRenewSoyInfo;
import google.registry.tools.soy.UniformRapidSuspensionSoyInfo;
import java.util.ArrayList;
import java.util.List;
@@ -43,6 +44,7 @@ import java.util.Optional;
import java.util.Set;
import javax.xml.bind.annotation.adapters.HexBinaryAdapter;
import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormat;
/** A command to suspend a domain for the Uniform Rapid Suspension process. */
@Parameters(separators = " =",
@@ -97,6 +99,13 @@ final class UniformRapidSuspensionCommand extends MutatingEppToolCommand {
description = "Flag indicating that is is an undo command, which removes locks.")
private boolean undo;
@Parameter(
names = {"--renew_one_year"},
required = true,
description = "Flag indicating whether or not the domain will be renewed for a year.",
arity = 1)
private boolean renewOneYear;
/** Set of existing locks that need to be preserved during undo, sorted for nicer output. */
ImmutableSortedSet<String> existingLocks;
@@ -114,19 +123,20 @@ final class UniformRapidSuspensionCommand extends MutatingEppToolCommand {
superuser = true;
DateTime now = DateTime.now(UTC);
ImmutableSet<String> newHostsSet = ImmutableSet.copyOf(newHosts);
Optional<DomainBase> domain = loadByForeignKey(DomainBase.class, domainName, now);
checkArgumentPresent(domain, "Domain '%s' does not exist or is deleted", domainName);
Optional<DomainBase> domainOpt = loadByForeignKey(DomainBase.class, domainName, now);
checkArgumentPresent(domainOpt, "Domain '%s' does not exist or is deleted", domainName);
DomainBase domain = domainOpt.get();
Set<String> missingHosts =
difference(newHostsSet, checkResourcesExist(HostResource.class, newHosts, now));
checkArgument(missingHosts.isEmpty(), "Hosts do not exist: %s", missingHosts);
checkArgument(
locksToPreserve.isEmpty() || undo,
"Locks can only be preserved when running with --undo");
existingNameservers = getExistingNameservers(domain.get());
existingLocks = getExistingLocks(domain.get());
existingDsData = getExistingDsData(domain.get());
existingNameservers = getExistingNameservers(domain);
existingLocks = getExistingLocks(domain);
existingDsData = getExistingDsData(domain);
removeStatuses =
(hasClientHold(domain.get()) && !undo)
(hasClientHold(domain) && !undo)
? ImmutableSet.of(StatusValue.CLIENT_HOLD.getXmlName())
: ImmutableSet.of();
ImmutableSet<String> statusesToApply;
@@ -138,6 +148,30 @@ final class UniformRapidSuspensionCommand extends MutatingEppToolCommand {
} else {
statusesToApply = URS_LOCKS;
}
// trigger renew flow
if (renewOneYear) {
setSoyTemplate(DomainRenewSoyInfo.getInstance(), DomainRenewSoyInfo.RENEWDOMAIN);
addSoyRecord(
CLIENT_ID,
new SoyMapData(
"domainName",
domain.getDomainName(),
"expirationDate",
domain
.getRegistrationExpirationTime()
.toString(DateTimeFormat.forPattern("YYYY-MM-dd")),
// period is the number of years to renew the registration for
"period",
String.valueOf(1),
// use the same values for reason and requestedByRegistrar from update flow
"reason",
(undo ? "Undo " : "") + "Uniform Rapid Suspension",
"requestedByRegistrar",
Boolean.toString(false)));
}
// trigger update flow
setSoyTemplate(
UniformRapidSuspensionSoyInfo.getInstance(),
UniformRapidSuspensionSoyInfo.UNIFORMRAPIDSUSPENSION);
@@ -157,7 +191,12 @@ final class UniformRapidSuspensionCommand extends MutatingEppToolCommand {
"newDsData",
newDsData != null ? DsRecord.convertToSoy(newDsData) : new SoyListData(),
"reason",
(undo ? "Undo " : "") + "Uniform Rapid Suspension"));
(undo ? "Undo " : "") + "Uniform Rapid Suspension",
// Domain auto-renewal is disabled as part of URS, and it's re-enabled if URS is undone.
// Therefore, autorenews is set to false by default and it's set to true only if the
// command is run in --undo mode.
"autorenews",
Boolean.toString(undo)));
}
private ImmutableSortedSet<String> getExistingNameservers(DomainBase domain) {
@@ -27,17 +27,16 @@ import java.nio.file.Files;
import java.nio.file.Path;
import javax.inject.Inject;
/** Command to set and update {@code KmsKeyring} values. */
@Parameters(
separators = " =",
commandDescription = "Update values of secrets in KmsKeyring."
)
final class UpdateKmsKeyringCommand implements CommandWithRemoteApi {
/**
* Command to set and update ASCII-armored secret from the active {@code Keyring} implementation.
*/
@Parameters(separators = " =", commandDescription = "Update values of secret in the keyring.")
final class UpdateKeyringSecretCommand implements CommandWithRemoteApi {
@Inject KmsUpdater kmsUpdater;
@Inject
UpdateKmsKeyringCommand() {}
UpdateKeyringSecretCommand() {}
@Parameter(names = "--keyname", description = "The secret to update", required = true)
private KeyringKeyName keyringKeyName;
@@ -21,6 +21,7 @@ import static google.registry.persistence.transaction.TransactionManagerFactory.
import static google.registry.persistence.transaction.TransactionManagerFactory.ofyTm;
import com.google.appengine.tools.mapreduce.Mapper;
import com.google.common.base.CaseFormat;
import com.google.common.collect.ImmutableList;
import com.googlecode.objectify.Key;
import google.registry.config.RegistryConfig.Config;
@@ -29,16 +30,12 @@ import google.registry.mapreduce.inputs.EppResourceInputs;
import google.registry.model.EppResource;
import google.registry.model.domain.DomainHistory;
import google.registry.model.reporting.HistoryEntry;
import google.registry.persistence.transaction.CriteriaQueryBuilder;
import google.registry.rde.RdeStagingAction;
import google.registry.request.Action;
import google.registry.request.Response;
import google.registry.request.auth.Auth;
import google.registry.tools.server.GenerateZoneFilesAction;
import java.util.Optional;
import javax.inject.Inject;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
/**
* A mapreduce that creates synthetic history objects in SQL for all {@link EppResource} objects.
@@ -86,12 +83,12 @@ public class CreateSyntheticHistoryEntriesAction implements Runnable {
}
/**
* The number of shards to run the map-only mapreduce on.
* The default number of shards to run the map-only mapreduce on.
*
* <p>This is much lower than the default of 100, or even 10, because we can afford it being slow
* and we want to avoid overloading SQL.
* <p>This is much lower than the default of 100 because we can afford it being slow and we want
* to avoid overloading SQL.
*/
private static final int NUM_SHARDS = 3;
private static final int NUM_SHARDS = 10;
@Override
public void run() {
@@ -105,27 +102,44 @@ public class CreateSyntheticHistoryEntriesAction implements Runnable {
.sendLinkToMapreduceConsole(response);
}
// Lifted from HistoryEntryDao
private static Optional<? extends HistoryEntry> mostRecentHistoryFromSql(EppResource resource) {
// Returns true iff any of the *History objects in SQL contain a representation of this resource
// at the point in time that the *History object was created.
private static boolean hasHistoryContainingResource(EppResource resource) {
return jpaTm()
.transact(
() -> {
// Use READ COMMITTED isolation level so that any long-living queries don't cause
// collection of predicate locks to spiral out of control (as would happen with a
// SERIALIZABLE isolation level)
//
// NB: setting the isolation level inside the transaction only works for Postgres and
// will be reverted to the default once the transaction is committed.
jpaTm()
.getEntityManager()
.createNativeQuery("SET TRANSACTION ISOLATION LEVEL READ COMMITTED")
.executeUpdate();
// The class we're searching from is based on which parent type (e.g. Domain) we have
Class<? extends HistoryEntry> historyClass =
getHistoryClassFromParent(resource.getClass());
// The field representing repo ID unfortunately varies by history class
String repoIdFieldName = getRepoIdFieldNameFromHistoryClass(historyClass);
CriteriaBuilder criteriaBuilder = jpaTm().getEntityManager().getCriteriaBuilder();
CriteriaQuery<? extends HistoryEntry> criteriaQuery =
CriteriaQueryBuilder.create(historyClass)
.where(repoIdFieldName, criteriaBuilder::equal, resource.getRepoId())
.orderByDesc("modificationTime")
.build();
return jpaTm()
.criteriaQuery(criteriaQuery)
.setMaxResults(1)
.getResultStream()
.findFirst();
String repoIdFieldName =
CaseFormat.LOWER_CAMEL.to(
CaseFormat.LOWER_UNDERSCORE,
getRepoIdFieldNameFromHistoryClass(historyClass));
// The "history" fields in the *History objects are all prefixed with "history_". If
// any of the non-"history_" fields are non-null, that means that that row contains
// a representation of that EppResource at that point in time. We use creation_time as
// a marker since it's the simplest field and all EppResources will have it.
return (boolean)
jpaTm()
.getEntityManager()
.createNativeQuery(
String.format(
"SELECT EXISTS (SELECT 1 FROM \"%s\" WHERE %s = :repoId AND"
+ " creation_time IS NOT NULL)",
historyClass.getSimpleName(), repoIdFieldName))
.setParameter("repoId", resource.getRepoId())
.getSingleResult();
});
}
@@ -164,10 +178,7 @@ public class CreateSyntheticHistoryEntriesAction implements Runnable {
EppResource eppResource = auditedOfy().load().key(resourceKey).now();
// Only save new history entries if the most recent history for this object in SQL does not
// have the resource at that point in time already
Optional<? extends HistoryEntry> maybeHistory = mostRecentHistoryFromSql(eppResource);
if (maybeHistory
.map(history -> !history.getResourceAtPointInTime().isPresent())
.orElse(true)) {
if (!hasHistoryContainingResource(eppResource)) {
ofyTm()
.transact(
() ->
@@ -0,0 +1,135 @@
// 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.tools.javascrap;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import com.google.common.collect.ImmutableList;
import dagger.Component;
import google.registry.beam.common.RegistryJpaIO;
import google.registry.beam.common.RegistryPipelineOptions;
import google.registry.config.RegistryConfig.Config;
import google.registry.config.RegistryConfig.ConfigModule;
import google.registry.model.EppResource;
import google.registry.model.UpdateAutoTimestamp;
import google.registry.model.UpdateAutoTimestamp.DisableAutoUpdateResource;
import google.registry.model.contact.ContactResource;
import google.registry.model.domain.DomainBase;
import google.registry.model.host.HostResource;
import google.registry.model.reporting.HistoryEntry;
import google.registry.persistence.PersistenceModule.TransactionIsolationLevel;
import google.registry.persistence.VKey;
import java.io.Serializable;
import javax.inject.Singleton;
import javax.persistence.Entity;
import org.apache.beam.sdk.Pipeline;
import org.apache.beam.sdk.options.PipelineOptionsFactory;
import org.apache.beam.sdk.transforms.MapElements;
import org.apache.beam.sdk.values.TypeDescriptor;
/**
* Pipeline that creates a synthetic history entry for every {@link EppResource} in SQL at the
* current time.
*
* <p>The history entries in Datastore does not have the EPP resource embedded in them. Therefore
* after {@link google.registry.beam.initsql.InitSqlPipeline} runs, these fields will all be empty.
* This pipeline loads all EPP resources and for each of them creates a synthetic history entry that
* contains the resource and saves them back to SQL, so that they can be used in the RDE pipeline.
*
* <p>Note that this pipeline should only be run in a test environment right after the init SQL
* pipeline finishes, and no EPP update is being made to the system, otherwise there is no garuantee
* that the latest history entry for a given EPP resource does not already have the resource
* embedded within it.
*
* <p>To run the pipeline:
*
* <p><code>
* $ ./nom_build :core:cSHE --args="--region=us-central1
* --runner=DataflowRunner
* --registryEnvironment=CRASH
* --project={project-id}
* --workerMachineType=n2-standard-4"
* </code>
*
* @see google.registry.tools.javascrap.CreateSyntheticHistoryEntriesAction
*/
public class CreateSyntheticHistoryEntriesPipeline implements Serializable {
private static final ImmutableList<Class<? extends EppResource>> EPP_RESOURCE_CLASSES =
ImmutableList.of(DomainBase.class, ContactResource.class, HostResource.class);
private static final String HISTORY_REASON =
"Backfill EppResource history objects after initial backup to SQL";
static void setup(Pipeline pipeline, String registryAdminRegistrarId) {
for (Class<? extends EppResource> clazz : EPP_RESOURCE_CLASSES) {
pipeline
.apply(
String.format("Read all %s", clazz.getSimpleName()),
RegistryJpaIO.read(
"SELECT id FROM %entity%"
.replace("%entity%", clazz.getAnnotation(Entity.class).name()),
String.class,
repoId -> VKey.createSql(clazz, repoId)))
.apply(
String.format("Save a synthetic HistoryEntry for each %s", clazz),
MapElements.into(TypeDescriptor.of(Void.class))
.via(
(VKey<? extends EppResource> key) -> {
jpaTm()
.transact(
() -> {
EppResource eppResource = jpaTm().loadByKey(key);
try (DisableAutoUpdateResource disable =
UpdateAutoTimestamp.disableAutoUpdate()) {
jpaTm()
.put(
HistoryEntry.createBuilderForResource(eppResource)
.setRegistrarId(registryAdminRegistrarId)
.setBySuperuser(true)
.setRequestedByRegistrar(false)
.setModificationTime(jpaTm().getTransactionTime())
.setReason(HISTORY_REASON)
.setType(HistoryEntry.Type.SYNTHETIC)
.build());
}
});
return null;
}));
}
}
public static void main(String[] args) {
RegistryPipelineOptions options =
PipelineOptionsFactory.fromArgs(args).withValidation().as(RegistryPipelineOptions.class);
RegistryPipelineOptions.validateRegistryPipelineOptions(options);
options.setIsolationOverride(TransactionIsolationLevel.TRANSACTION_READ_COMMITTED);
String registryAdminRegistrarId =
DaggerCreateSyntheticHistoryEntriesPipeline_ConfigComponent.create()
.getRegistryAdminRegistrarId();
Pipeline pipeline = Pipeline.create(options);
setup(pipeline, registryAdminRegistrarId);
pipeline.run();
}
@Singleton
@Component(modules = ConfigModule.class)
interface ConfigComponent {
@Config("registryAdminClientId")
String getRegistryAdminRegistrarId();
}
}
@@ -0,0 +1,99 @@
// 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.tools.javascrap;
import static com.google.common.base.Verify.verify;
import static google.registry.model.ofy.ObjectifyService.auditedOfy;
import static google.registry.persistence.transaction.TransactionManagerFactory.ofyTm;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import com.google.common.collect.ImmutableList;
import com.googlecode.objectify.Key;
import google.registry.model.host.HostResource;
import google.registry.model.index.EppResourceIndex;
import google.registry.model.index.ForeignKeyIndex;
import google.registry.tools.CommandWithRemoteApi;
import google.registry.tools.ConfirmingCommand;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
/**
* Deletes a {@link HostResource} by its ROID.
*
* <p>This deletes the host itself, everything in the same entity group including all {@link
* google.registry.model.reporting.HistoryEntry}s and {@link
* google.registry.model.poll.PollMessage}s, the {@link EppResourceIndex}, and the {@link
* ForeignKeyIndex} (if it exists).
*
* <p>DO NOT use this to hard-delete a host that is still in use on a domain. Bad things will
* happen.
*/
@Parameters(separators = " =", commandDescription = "Delete a host by its ROID.")
public class HardDeleteHostCommand extends ConfirmingCommand implements CommandWithRemoteApi {
@Parameter(names = "--roid", description = "The ROID of the host to be deleted.")
String roid;
@Parameter(names = "--hostname", description = "The hostname, for verification.")
String hostname;
private ImmutableList<Key<Object>> toDelete;
@Override
protected void init() {
ofyTm()
.transact(
() -> {
Key<HostResource> targetKey = Key.create(HostResource.class, roid);
HostResource host = auditedOfy().load().key(targetKey).now();
verify(Objects.equals(host.getHostName(), hostname), "Hostname does not match");
List<Key<Object>> objectsInEntityGroup =
auditedOfy().load().ancestor(host).keys().list();
Optional<ForeignKeyIndex<HostResource>> fki =
Optional.ofNullable(
auditedOfy().load().key(ForeignKeyIndex.createKey(host)).now());
if (!fki.isPresent()) {
System.out.println(
"No ForeignKeyIndex exists, likely because resource is soft-deleted."
+ " Continuing.");
}
EppResourceIndex eppResourceIndex =
auditedOfy().load().entity(EppResourceIndex.create(targetKey)).now();
verify(eppResourceIndex.getKey().equals(targetKey), "Wrong EppResource Index loaded");
ImmutableList.Builder<Key<Object>> toDeleteBuilder =
new ImmutableList.Builder<Key<Object>>()
.addAll(objectsInEntityGroup)
.add(Key.create(eppResourceIndex));
fki.ifPresent(f -> toDeleteBuilder.add(Key.create(f)));
toDelete = toDeleteBuilder.build();
System.out.printf("\n\nAbout to delete %d entities with keys:\n", toDelete.size());
toDelete.forEach(System.out::println);
});
}
@Override
protected String execute() {
tm().transact(() -> auditedOfy().delete().keys(toDelete).now());
return "Done.";
}
}
@@ -2,6 +2,15 @@
"name": "Bulk Delete Cloud Datastore",
"description": "An Apache Beam batch pipeline that deletes Cloud Datastore in bulk. This is easier to use than the GCP-provided template.",
"parameters": [
{
"name": "registryEnvironment",
"label": "The Registry environment.",
"helpText": "The Registry environment, required only because the worker initializer demands it.",
"is_optional": false,
"regexes": [
"^PRODUCTION|SANDBOX|CRASH|QA|ALPHA$"
]
},
{
"name": "kindsToDelete",
"label": "The data KINDs to delete.",
@@ -5,10 +5,10 @@
{
"name": "registryEnvironment",
"label": "The Registry environment.",
"helpText": "The Registry environment, required if environment-specific initialization (such as JPA) is needed on worker VMs.",
"is_optional": true,
"helpText": "The Registry environment.",
"is_optional": false,
"regexes": [
"^[0-9A-Z_]+$"
"^PRODUCTION|SANDBOX|CRASH|QA|ALPHA$"
]
},
{
@@ -5,10 +5,10 @@
{
"name": "registryEnvironment",
"label": "The Registry environment.",
"helpText": "The Registry environment, required if environment-specific initialization (such as JPA) is needed on worker VMs.",
"is_optional": true,
"helpText": "The Registry environment.",
"is_optional": false,
"regexes": [
"^[0-9A-Z_]+$"
"^PRODUCTION|SANDBOX|CRASH|QA|ALPHA$"
]
},
{
@@ -2,12 +2,21 @@
"name": "RDE/BRDA Deposit Generation",
"description": "An Apache Beam pipeline generates RDE or BRDA deposits and deposits them to GCS with GhostRyde encryption.",
"parameters": [
{
"name": "registryEnvironment",
"label": "The Registry environment.",
"helpText": "The Registry environment.",
"is_optional": false,
"regexes": [
"^PRODUCTION|SANDBOX|CRASH|QA|ALPHA$"
]
},
{
"name": "pendings",
"label": "The pendings deposits to generate.",
"helpText": "A TLD to PendingDeposit map that is serialized and Base64 URL-safe encoded.",
"regexes": [
"A-Za-z0-9\\-_"
"[A-Za-z0-9\\-_]+"
]
},
{
@@ -19,12 +28,12 @@
]
},
{
"name": "gcsBucket",
"label": "The GCS bucket that where the resulting files will be stored.",
"name": "rdeStagingBucket",
"label": "The GCS bucket that where the resulting files will be stored.",
"helpText": "Only the bucket name itself, without the leading \"gs://\".",
"is_optional": false,
"regexes": [
"^[a-zA-Z0-9_\\-]+$"
"[a-zA-Z0-9_\\-]+$"
]
},
{
@@ -32,7 +41,7 @@
"label": "The PGP public key used to encrypt the RDE/BRDA deposit files.",
"helpText": "The key is Base64 URL-safe encoded.",
"regexes": [
"A-Za-z0-9\\-_"
"[A-Za-z0-9\\-_]+"
]
}
]
@@ -5,10 +5,10 @@
{
"name": "registryEnvironment",
"label": "The Registry environment.",
"helpText": "The Registry environment, required if environment-specific initialization (such as JPA) is needed on worker VMs.",
"is_optional": true,
"helpText": "The Registry environment.",
"is_optional": false,
"regexes": [
"^[0-9A-Z_]+$"
"^PRODUCTION|SANDBOX|CRASH|QA|ALPHA$"
]
},
{
@@ -21,6 +21,8 @@
{@param domainName: string}
{@param expirationDate: string}
{@param period: string}
{@param? reason: string}
{@param? requestedByRegistrar: string}
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<command>
@@ -32,6 +34,18 @@
<domain:period unit="y">{$period}</domain:period>
</domain:renew>
</renew>
{if $reason or $requestedByRegistrar}
<extension>
<metadata:metadata xmlns:metadata="urn:google:params:xml:ns:metadata-1.0">
{if $reason}
<metadata:reason>{$reason}</metadata:reason>
{/if}
{if $requestedByRegistrar}
<metadata:requestedByRegistrar>{$requestedByRegistrar}</metadata:requestedByRegistrar>
{/if}
</metadata:metadata>
</extension>
{/if}
<clTRID>RegistryTool</clTRID>
</command>
</epp>
@@ -25,6 +25,7 @@
{@param statusesToRemove: list<string>}
{@param newDsData: list<[keyTag:int, alg:int, digestType:int, digest:string]>}
{@param reason: string}
{@param autorenews: string}
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<command>
@@ -75,6 +76,9 @@
</secDNS:add>
{/if}
</secDNS:update>
<superuser:domainUpdate xmlns:superuser="urn:google:params:xml:ns:superuser-1.0">
<superuser:autorenews>{$autorenews}</superuser:autorenews>
</superuser:domainUpdate>
<metadata:metadata xmlns:metadata="urn:google:params:xml:ns:metadata-1.0">
<metadata:reason>{$reason}</metadata:reason>
<metadata:requestedByRegistrar>false</metadata:requestedByRegistrar>
@@ -142,7 +142,7 @@ class SendExpiringCertificateNotificationEmailActionTest {
}
@TestOfyAndSql
void sendNotificationEmail_returnsTrue() throws Exception {
void sendNotificationEmail_techEMailAsRecipient_returnsTrue() throws Exception {
X509Certificate expiringCertificate =
SelfSignedCaCertificate.create(
"www.example.tld",
@@ -157,25 +157,64 @@ class SendExpiringCertificateNotificationEmailActionTest {
.asBuilder()
.setFailoverClientCertificate(cert.get(), clock.nowUtc())
.build());
ImmutableList<RegistrarContact> contacts =
ImmutableList.of(
new RegistrarContact.Builder()
.setParent(registrar)
.setName("Will Doe")
.setEmailAddress("will@example-registrar.tld")
.setPhoneNumber("+1.3105551213")
.setFaxNumber("+1.3105551213")
.setTypes(ImmutableSet.of(RegistrarContact.Type.TECH))
.setVisibleInWhoisAsAdmin(true)
.setVisibleInWhoisAsTech(false)
.build());
persistSimpleResources(contacts);
persistResource(registrar);
persistSampleContacts(registrar, Type.TECH);
assertThat(
action.sendNotificationEmail(registrar, START_OF_TIME, CertificateType.FAILOVER, cert))
.isEqualTo(true);
}
@TestOfyAndSql
void sendNotificationEmail_adminEMailAsRecipient_returnsTrue() throws Exception {
X509Certificate expiringCertificate =
SelfSignedCaCertificate.create(
"www.example.tld",
DateTime.parse("2020-09-02T00:00:00Z"),
DateTime.parse("2021-06-01T00:00:00Z"))
.cert();
Optional<String> cert =
Optional.of(certificateChecker.serializeCertificate(expiringCertificate));
Registrar registrar =
persistResource(
makeRegistrar1()
.asBuilder()
.setFailoverClientCertificate(cert.get(), clock.nowUtc())
.build());
persistSampleContacts(registrar, Type.ADMIN);
assertThat(
action.sendNotificationEmail(registrar, START_OF_TIME, CertificateType.FAILOVER, cert))
.isEqualTo(true);
}
@TestOfyAndSql
void sendNotificationEmail_returnsFalse_unsupportedEmailType() throws Exception {
Registrar registrar =
persistResource(
createRegistrar(
"testId",
"testName",
SelfSignedCaCertificate.create(
"www.example.tld",
DateTime.parse("2020-09-02T00:00:00Z"),
DateTime.parse("2021-06-01T00:00:00Z"))
.cert(),
null)
.build());
persistSampleContacts(registrar, Type.LEGAL);
assertThat(
action.sendNotificationEmail(
registrar,
START_OF_TIME,
CertificateType.FAILOVER,
Optional.of(
certificateChecker.serializeCertificate(
SelfSignedCaCertificate.create(
"www.example.tld",
DateTime.parse("2020-09-02T00:00:00Z"),
DateTime.parse("2021-06-01T00:00:00Z"))
.cert()))))
.isEqualTo(false);
}
@TestOfyAndSql
void sendNotificationEmail_returnsFalse_noEmailRecipients() throws Exception {
X509Certificate expiringCertificate =
@@ -247,93 +286,82 @@ class SendExpiringCertificateNotificationEmailActionTest {
}
@TestOfyAndSql
void sendNotificationEmails_allEmailsBeingAttemptedToSend() throws Exception {
X509Certificate expiringCertificate =
SelfSignedCaCertificate.create(
"www.example.tld",
DateTime.parse("2020-09-02T00:00:00Z"),
DateTime.parse("2021-06-01T00:00:00Z"))
.cert();
X509Certificate certificate =
SelfSignedCaCertificate.create(
"www.example.tld",
DateTime.parse("2020-09-02T00:00:00Z"),
DateTime.parse("2021-10-01T00:00:00Z"))
.cert();
int numOfRegistrars = 10;
int numOfRegistrarsWithExpiringCertificates = 2;
for (int i = 1; i <= numOfRegistrarsWithExpiringCertificates; i++) {
persistResource(
createRegistrar("oldcert" + i, "name" + i, expiringCertificate, null).build());
void sendNotificationEmails_allEmailsBeingSent_onlyMainCertificates() throws Exception {
for (int i = 1; i <= 10; i++) {
Registrar registrar =
persistResource(
createRegistrar(
"oldcert" + i,
"name" + i,
SelfSignedCaCertificate.create(
"www.example.tld",
DateTime.parse("2020-09-02T00:00:00Z"),
DateTime.parse("2021-06-01T00:00:00Z"))
.cert(),
null)
.build());
persistSampleContacts(registrar, Type.TECH);
}
for (int i = numOfRegistrarsWithExpiringCertificates; i <= numOfRegistrars; i++) {
persistResource(createRegistrar("goodcert" + i, "name" + i, certificate, null).build());
}
assertThat(action.sendNotificationEmails()).isEqualTo(numOfRegistrarsWithExpiringCertificates);
assertThat(action.sendNotificationEmails()).isEqualTo(10);
}
@TestOfyAndSql
void sendNotificationEmails_allEmailsBeingAttemptedToSend_onlyMainCertificates()
throws Exception {
X509Certificate expiringCertificate =
SelfSignedCaCertificate.create(
"www.example.tld",
DateTime.parse("2020-09-02T00:00:00Z"),
DateTime.parse("2021-06-01T00:00:00Z"))
.cert();
int numOfRegistrars = 10;
for (int i = 1; i <= numOfRegistrars; i++) {
persistResource(
createRegistrar("oldcert" + i, "name" + i, expiringCertificate, null).build());
void sendNotificationEmails_allEmailsBeingSent_onlyFailOverCertificates() throws Exception {
for (int i = 1; i <= 10; i++) {
Registrar registrar =
persistResource(
createRegistrar(
"oldcert" + i,
"name" + i,
null,
SelfSignedCaCertificate.create(
"www.example.tld",
DateTime.parse("2020-09-02T00:00:00Z"),
DateTime.parse("2021-06-01T00:00:00Z"))
.cert())
.build());
persistSampleContacts(registrar, Type.TECH);
}
assertThat(action.sendNotificationEmails()).isEqualTo(numOfRegistrars);
assertThat(action.sendNotificationEmails()).isEqualTo(10);
}
@TestOfyAndSql
void sendNotificationEmails_allEmailsBeingAttemptedToSend_onlyFailOverCertificates()
throws Exception {
void sendNotificationEmails_allEmailsBeingSent_mixedOfCertificates() throws Exception {
X509Certificate expiringCertificate =
SelfSignedCaCertificate.create(
"www.example.tld",
DateTime.parse("2020-09-02T00:00:00Z"),
DateTime.parse("2021-06-01T00:00:00Z"))
.cert();
int numOfRegistrars = 10;
for (int i = 1; i <= numOfRegistrars; i++) {
persistResource(
createRegistrar("oldcert" + i, "name" + i, null, expiringCertificate).build());
}
assertThat(action.sendNotificationEmails()).isEqualTo(numOfRegistrars);
}
@TestOfyAndSql
void sendNotificationEmails_allEmailsBeingAttemptedToSend_mixedOfCertificates() throws Exception {
X509Certificate expiringCertificate =
SelfSignedCaCertificate.create(
"www.example.tld",
DateTime.parse("2020-09-02T00:00:00Z"),
DateTime.parse("2021-06-01T00:00:00Z"))
.cert();
int numOfRegistrars = 10;
int numOfExpiringFailOverOnly = 2;
int numOfExpiringPrimaryOnly = 3;
for (int i = 1; i <= numOfExpiringFailOverOnly; i++) {
persistResource(
createRegistrar("cl" + i, "expiringFailOverOnly" + i, null, expiringCertificate).build());
for (int i = 1; i <= 3; i++) {
Registrar registrar =
persistResource(
createRegistrar(
"cl" + i, "regWIthexpiringFailOverOnly" + i, null, expiringCertificate)
.build());
persistSampleContacts(registrar, Type.TECH);
}
for (int i = 1; i <= numOfExpiringPrimaryOnly; i++) {
persistResource(
createRegistrar("cli" + i, "expiringPrimaryOnly" + i, expiringCertificate, null).build());
for (int i = 1; i <= 5; i++) {
Registrar registrar =
persistResource(
createRegistrar(
"cli" + i, "regWithexpiringPrimaryOnly" + i, expiringCertificate, null)
.build());
persistSampleContacts(registrar, Type.TECH);
}
for (int i = numOfExpiringFailOverOnly + numOfExpiringPrimaryOnly + 1;
i <= numOfRegistrars;
i++) {
persistResource(
createRegistrar("client" + i, "regularReg" + i, expiringCertificate, expiringCertificate)
.build());
for (int i = 1; i <= 4; i++) {
Registrar registrar =
persistResource(
createRegistrar(
"client" + i,
"regWithTwoExpiring" + i,
expiringCertificate,
expiringCertificate)
.build());
persistSampleContacts(registrar, Type.ADMIN);
}
assertThat(action.sendNotificationEmails())
.isEqualTo(numOfRegistrars + numOfExpiringFailOverOnly + numOfExpiringPrimaryOnly);
assertThat(action.sendNotificationEmails()).isEqualTo(16);
}
@TestOfyAndSql
@@ -639,12 +667,39 @@ class SendExpiringCertificateNotificationEmailActionTest {
}
@TestOfyAndSql
void run_responseStatusIs200() {
void run_sentZeroEmail_responseStatusIs200() {
action.run();
assertThat(response.getStatus()).isEqualTo(SC_OK);
assertThat(response.getPayload())
.isEqualTo("Done. Sent 0 expiring certificate notification emails in total.");
}
/** Returns a sample registrar with a customized registrar name, client id and certificate* */
@TestOfyAndSql
void run_sentEmails_responseStatusIs200() throws Exception {
for (int i = 1; i <= 5; i++) {
Registrar registrar =
persistResource(
createRegistrar(
"id_" + i,
"name" + i,
SelfSignedCaCertificate.create(
"www.example.tld",
DateTime.parse("2020-09-02T00:00:00Z"),
DateTime.parse("2021-06-01T00:00:00Z"))
.cert(),
null)
.build());
persistSampleContacts(registrar, Type.TECH);
}
action.run();
assertThat(response.getStatus()).isEqualTo(SC_OK);
assertThat(response.getPayload())
.isEqualTo("Done. Sent 5 expiring certificate notification emails in total.");
}
/**
* Returns a sample registrar builder with a customized registrar name, client id and certificate.
*/
private Registrar.Builder createRegistrar(
String registrarId,
String registrarName,
@@ -683,4 +738,27 @@ class SendExpiringCertificateNotificationEmailActionTest {
}
return builder;
}
/** Returns persisted sample contacts with a customized contact email type. */
private ImmutableList<RegistrarContact> persistSampleContacts(
Registrar registrar, RegistrarContact.Type emailType) {
return persistSimpleResources(
ImmutableList.of(
new RegistrarContact.Builder()
.setParent(registrar)
.setName("Will Doe")
.setEmailAddress("will@example-registrar.tld")
.setPhoneNumber("+1.0105551213")
.setFaxNumber("+1.0105551213")
.setTypes(ImmutableSet.of(emailType))
.build(),
new RegistrarContact.Builder()
.setParent(registrar)
.setName("Will Smith")
.setEmailAddress("will@test-registrar.tld")
.setPhoneNumber("+1.3105551213")
.setFaxNumber("+1.3105551213")
.setTypes(ImmutableSet.of(emailType))
.build()));
}
}
@@ -0,0 +1,381 @@
// 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 com.google.common.truth.Truth.assertWithMessage;
import static com.google.common.truth.Truth8.assertThat;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.testing.DatabaseHelper.persistResource;
import static org.apache.http.HttpStatus.SC_OK;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import google.registry.model.contact.ContactAddress;
import google.registry.model.contact.ContactAuthInfo;
import google.registry.model.contact.ContactBase;
import google.registry.model.contact.ContactHistory;
import google.registry.model.contact.ContactPhoneNumber;
import google.registry.model.contact.ContactResource;
import google.registry.model.contact.Disclose;
import google.registry.model.contact.PostalInfo;
import google.registry.model.eppcommon.AuthInfo.PasswordAuth;
import google.registry.model.eppcommon.PresenceMarker;
import google.registry.model.eppcommon.StatusValue;
import google.registry.testing.AppEngineExtension;
import google.registry.testing.DatabaseHelper;
import google.registry.testing.DualDatabaseTest;
import google.registry.testing.FakeClock;
import google.registry.testing.FakeResponse;
import google.registry.testing.InjectExtension;
import google.registry.testing.TestSqlOnly;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.RegisterExtension;
/** Unit tests for {@link WipeOutContactHistoryPiiAction}. */
@DualDatabaseTest
class WipeOutContactHistoryPiiActionTest {
private static final int TEST_BATCH_SIZE = 20;
private static final int MIN_MONTHS_BEFORE_WIPE_OUT = 18;
private static final ContactResource defaultContactResource =
new ContactResource.Builder()
.setContactId("sh8013")
.setRepoId("2FF-ROID")
.setStatusValues(ImmutableSet.of(StatusValue.CLIENT_DELETE_PROHIBITED))
.setLocalizedPostalInfo(
new PostalInfo.Builder()
.setType(PostalInfo.Type.LOCALIZED)
.setAddress(
new ContactAddress.Builder()
.setStreet(ImmutableList.of("123 Grand Ave"))
.build())
.build())
.setInternationalizedPostalInfo(
new PostalInfo.Builder()
.setType(PostalInfo.Type.INTERNATIONALIZED)
.setName("John Doe")
.setOrg("Example Inc.")
.setAddress(
new ContactAddress.Builder()
.setStreet(ImmutableList.of("123 Example Dr.", "Suite 100"))
.setCity("Dulles")
.setState("VA")
.setZip("20166-6503")
.setCountryCode("US")
.build())
.build())
.setVoiceNumber(
new ContactPhoneNumber.Builder()
.setPhoneNumber("+1.7035555555")
.setExtension("1234")
.build())
.setFaxNumber(new ContactPhoneNumber.Builder().setPhoneNumber("+1.7035555556").build())
.setEmailAddress("jdoe@example.com")
.setPersistedCurrentSponsorRegistrarId("TheRegistrar")
.setCreationRegistrarId("NewRegistrar")
.setLastEppUpdateRegistrarId("NewRegistrar")
.setCreationTimeForTest(DateTime.parse("1999-04-03T22:00:00.0Z"))
.setLastEppUpdateTime(DateTime.parse("1999-12-03T09:00:00.0Z"))
.setLastTransferTime(DateTime.parse("2000-04-08T09:00:00.0Z"))
.setAuthInfo(ContactAuthInfo.create(PasswordAuth.create("2fooBAR")))
.setDisclose(
new Disclose.Builder()
.setFlag(true)
.setVoice(new PresenceMarker())
.setEmail(new PresenceMarker())
.build())
.build();
@RegisterExtension
public final AppEngineExtension appEngine =
AppEngineExtension.builder().withDatastoreAndCloudSql().withTaskQueue().build();
@RegisterExtension public final InjectExtension inject = new InjectExtension();
private final FakeClock clock = new FakeClock(DateTime.parse("2021-08-26T20:21:22Z"));
private FakeResponse response;
private WipeOutContactHistoryPiiAction action;
@BeforeEach
void beforeEach() {
response = new FakeResponse();
action =
new WipeOutContactHistoryPiiAction(
clock, MIN_MONTHS_BEFORE_WIPE_OUT, TEST_BATCH_SIZE, response);
}
@TestSqlOnly
void getAllHistoryEntitiesOlderThan_returnsAllPersistedEntities() {
ImmutableList<ContactHistory> expectedToBeWipedOut =
persistLotsOfContactHistoryEntities(
20, MIN_MONTHS_BEFORE_WIPE_OUT + 1, 0, defaultContactResource);
jpaTm()
.transact(
() ->
assertThat(
action.getNextContactHistoryEntitiesWithPiiBatch(
clock.nowUtc().minusMonths(MIN_MONTHS_BEFORE_WIPE_OUT)))
.containsExactlyElementsIn(expectedToBeWipedOut));
}
@TestSqlOnly
void getAllHistoryEntitiesOlderThan_returnsOnlyOldEnoughPersistedEntities() {
ImmutableList<ContactHistory> expectedToBeWipedOut =
persistLotsOfContactHistoryEntities(
19, MIN_MONTHS_BEFORE_WIPE_OUT + 2, 0, defaultContactResource);
// persisted entities that should not be part of the actual result
persistLotsOfContactHistoryEntities(
15, 17, MIN_MONTHS_BEFORE_WIPE_OUT - 1, defaultContactResource);
jpaTm()
.transact(
() ->
assertThat(
action.getNextContactHistoryEntitiesWithPiiBatch(
clock.nowUtc().minusMonths(MIN_MONTHS_BEFORE_WIPE_OUT)))
.containsExactlyElementsIn(expectedToBeWipedOut));
}
@TestSqlOnly
void run_withNoEntitiesToWipeOut_success() {
assertThat(
jpaTm()
.transact(
() ->
action
.getNextContactHistoryEntitiesWithPiiBatch(
clock.nowUtc().minusMonths(MIN_MONTHS_BEFORE_WIPE_OUT))
.count()))
.isEqualTo(0);
action.run();
assertThat(
jpaTm()
.transact(
() ->
action
.getNextContactHistoryEntitiesWithPiiBatch(
clock.nowUtc().minusMonths(MIN_MONTHS_BEFORE_WIPE_OUT))
.count()))
.isEqualTo(0);
assertThat(response.getStatus()).isEqualTo(SC_OK);
assertThat(response.getPayload())
.isEqualTo("Done. Wiped out PII of 0 ContactHistory entities in total.");
}
@TestSqlOnly
void run_withOneBatchOfEntities_success() {
int numOfMonthsFromNow = MIN_MONTHS_BEFORE_WIPE_OUT + 2;
ImmutableList<ContactHistory> expectedToBeWipedOut =
persistLotsOfContactHistoryEntities(20, numOfMonthsFromNow, 0, defaultContactResource);
// The query should return a stream of all persisted entities.
assertThat(
jpaTm()
.transact(
() ->
action
.getNextContactHistoryEntitiesWithPiiBatch(
clock.nowUtc().minusMonths(MIN_MONTHS_BEFORE_WIPE_OUT))
.count()))
.isEqualTo(expectedToBeWipedOut.size());
assertAllEntitiesContainPii(DatabaseHelper.loadByEntitiesIfPresent(expectedToBeWipedOut));
action.run();
assertThat(response.getPayload())
.isEqualTo("Done. Wiped out PII of 20 ContactHistory entities in total.");
// The query should return an empty stream after the wipe out action.
assertThat(
jpaTm()
.transact(
() ->
action
.getNextContactHistoryEntitiesWithPiiBatch(
clock.nowUtc().minusMonths(MIN_MONTHS_BEFORE_WIPE_OUT))
.count()))
.isEqualTo(0);
assertAllPiiFieldsAreWipedOut(DatabaseHelper.loadByEntitiesIfPresent(expectedToBeWipedOut));
}
@TestSqlOnly
void run_withMultipleBatches_numOfEntitiesAsNonMultipleOfBatchSize_success() {
int numOfMonthsFromNow = MIN_MONTHS_BEFORE_WIPE_OUT + 2;
ImmutableList<ContactHistory> expectedToBeWipedOut =
persistLotsOfContactHistoryEntities(56, numOfMonthsFromNow, 0, defaultContactResource);
// The query should return a subset of all persisted data.
assertThat(
jpaTm()
.transact(
() ->
action
.getNextContactHistoryEntitiesWithPiiBatch(
clock.nowUtc().minusMonths(MIN_MONTHS_BEFORE_WIPE_OUT))
.count()))
.isEqualTo(TEST_BATCH_SIZE);
assertAllEntitiesContainPii(DatabaseHelper.loadByEntitiesIfPresent(expectedToBeWipedOut));
action.run();
assertThat(response.getPayload())
.isEqualTo("Done. Wiped out PII of 56 ContactHistory entities in total.");
// The query should return an empty stream after the wipe out action.
assertThat(
jpaTm()
.transact(
() ->
action
.getNextContactHistoryEntitiesWithPiiBatch(
clock.nowUtc().minusMonths(MIN_MONTHS_BEFORE_WIPE_OUT))
.count()))
.isEqualTo(0);
assertAllPiiFieldsAreWipedOut(DatabaseHelper.loadByEntitiesIfPresent(expectedToBeWipedOut));
}
@TestSqlOnly
void run_withMultipleBatches_numOfEntitiesAsMultiplesOfBatchSize_success() {
int numOfMonthsFromNow = MIN_MONTHS_BEFORE_WIPE_OUT + 2;
ImmutableList<ContactHistory> expectedToBeWipedOut =
persistLotsOfContactHistoryEntities(
TEST_BATCH_SIZE * 2, numOfMonthsFromNow, 0, defaultContactResource);
// The query should return a subset of all persisted data.
assertThat(
jpaTm()
.transact(
() ->
action
.getNextContactHistoryEntitiesWithPiiBatch(
clock.nowUtc().minusMonths(MIN_MONTHS_BEFORE_WIPE_OUT))
.count()))
.isEqualTo(TEST_BATCH_SIZE);
assertAllEntitiesContainPii(DatabaseHelper.loadByEntitiesIfPresent(expectedToBeWipedOut));
action.run();
assertThat(response.getPayload())
.isEqualTo("Done. Wiped out PII of 40 ContactHistory entities in total.");
// The query should return an empty stream after the wipe out action.
assertThat(
jpaTm()
.transact(
() ->
action
.getNextContactHistoryEntitiesWithPiiBatch(
clock.nowUtc().minusMonths(MIN_MONTHS_BEFORE_WIPE_OUT))
.count()))
.isEqualTo(0);
assertAllPiiFieldsAreWipedOut(DatabaseHelper.loadByEntitiesIfPresent(expectedToBeWipedOut));
}
@TestSqlOnly
void wipeOutContactHistoryData_wipesOutNoEntity() {
jpaTm()
.transact(
() -> {
assertThat(
action.wipeOutContactHistoryData(
action.getNextContactHistoryEntitiesWithPiiBatch(
clock.nowUtc().minusMonths(MIN_MONTHS_BEFORE_WIPE_OUT))))
.isEqualTo(0);
});
}
@TestSqlOnly
void wipeOutContactHistoryData_wipesOutMultipleEntities() {
int numOfMonthsFromNow = MIN_MONTHS_BEFORE_WIPE_OUT + 3;
ImmutableList<ContactHistory> expectedToBeWipedOut =
persistLotsOfContactHistoryEntities(20, numOfMonthsFromNow, 0, defaultContactResource);
assertAllEntitiesContainPii(DatabaseHelper.loadByEntitiesIfPresent(expectedToBeWipedOut));
jpaTm()
.transact(
() -> {
action.wipeOutContactHistoryData(
action.getNextContactHistoryEntitiesWithPiiBatch(
clock.nowUtc().minusMonths(MIN_MONTHS_BEFORE_WIPE_OUT)));
});
assertAllPiiFieldsAreWipedOut(DatabaseHelper.loadByEntitiesIfPresent(expectedToBeWipedOut));
}
/** persists a number of ContactHistory entities for load and query testing. */
ImmutableList<ContactHistory> persistLotsOfContactHistoryEntities(
int numOfEntities, int minusMonths, int minusDays, ContactResource contact) {
ImmutableList.Builder<ContactHistory> expectedEntitesBuilder = new ImmutableList.Builder<>();
for (int i = 0; i < numOfEntities; i++) {
expectedEntitesBuilder.add(
persistResource(
new ContactHistory()
.asBuilder()
.setRegistrarId("NewRegistrar")
.setModificationTime(clock.nowUtc().minusMonths(minusMonths).minusDays(minusDays))
.setType(ContactHistory.Type.CONTACT_DELETE)
.setContact(persistResource(contact))
.build()));
}
return expectedEntitesBuilder.build();
}
boolean areAllPiiFieldsWiped(ContactBase contactBase) {
return contactBase.getEmailAddress() == null
&& contactBase.getFaxNumber() == null
&& contactBase.getInternationalizedPostalInfo() == null
&& contactBase.getLocalizedPostalInfo() == null
&& contactBase.getVoiceNumber() == null;
}
boolean containsPii(ContactBase contactBase) {
return contactBase.getEmailAddress() != null
|| contactBase.getFaxNumber() != null
|| contactBase.getInternationalizedPostalInfo() != null
|| contactBase.getLocalizedPostalInfo() != null
|| contactBase.getVoiceNumber() != null;
}
void assertAllPiiFieldsAreWipedOut(ImmutableList<ContactHistory> entities) {
ImmutableList.Builder<ContactHistory> notWipedEntities = new ImmutableList.Builder<>();
for (ContactHistory entity : entities) {
if (!areAllPiiFieldsWiped(entity.getContactBase().get())) {
notWipedEntities.add(entity);
}
}
assertWithMessage("Not all PII fields of the contact history entities were wiped.")
.that(notWipedEntities.build())
.isEmpty();
}
void assertAllEntitiesContainPii(ImmutableList<ContactHistory> entities) {
ImmutableList.Builder<ContactHistory> entitiesWithNoPii = new ImmutableList.Builder<>();
for (ContactHistory entity : entities) {
if (!containsPii(entity.getContactBase().get())) {
entitiesWithNoPii.add(entity);
}
}
assertWithMessage("Not all contact history entities contain PII.")
.that(entitiesWithNoPii.build())
.isEmpty();
}
}
@@ -40,13 +40,13 @@ public abstract class BeamActionTestBase {
protected Dataflow dataflow = mock(Dataflow.class);
private Projects projects = mock(Projects.class);
private Locations locations = mock(Locations.class);
private FlexTemplates templates = mock(FlexTemplates.class);
protected FlexTemplates templates = mock(FlexTemplates.class);
protected Launch launch = mock(Launch.class);
private LaunchFlexTemplateResponse launchResponse =
new LaunchFlexTemplateResponse().setJob(new Job().setId("jobid"));
@BeforeEach
void beforeEach() throws Exception {
protected void beforeEach() throws Exception {
when(dataflow.projects()).thenReturn(projects);
when(projects.locations()).thenReturn(locations);
when(locations.flexTemplates()).thenReturn(templates);
@@ -62,7 +62,8 @@ public class RegistryJpaReadTest {
@RegisterExtension
@Order(Order.DEFAULT - 1)
final transient DatastoreEntityExtension datastore = new DatastoreEntityExtension();
final transient DatastoreEntityExtension datastore =
new DatastoreEntityExtension().allThreads(true);
@RegisterExtension
final transient JpaIntegrationTestExtension database =
@@ -19,6 +19,7 @@ import static google.registry.beam.common.RegistryPipelineOptions.validateRegist
import static org.junit.jupiter.api.Assertions.assertThrows;
import google.registry.config.RegistryEnvironment;
import google.registry.persistence.PersistenceModule.JpaTransactionManagerType;
import google.registry.persistence.PersistenceModule.TransactionIsolationLevel;
import google.registry.testing.SystemPropertyExtension;
import org.apache.beam.sdk.options.PipelineOptionsFactory;
@@ -123,4 +124,37 @@ class RegistryPipelineOptionsTest {
validateRegistryPipelineOptions(options);
assertThat(options.getProject()).isEqualTo("some-project");
}
@Test
void jpaTransactionManagerType_default() {
RegistryPipelineOptions options =
PipelineOptionsFactory.fromArgs(
"--registryEnvironment=" + RegistryEnvironment.UNITTEST.name())
.withValidation()
.as(RegistryPipelineOptions.class);
assertThat(options.getJpaTransactionManagerType()).isEqualTo(JpaTransactionManagerType.REGULAR);
}
@Test
void jpaTransactionManagerType_regularJpa() {
RegistryPipelineOptions options =
PipelineOptionsFactory.fromArgs(
"--registryEnvironment=" + RegistryEnvironment.UNITTEST.name(),
"--jpaTransactionManagerType=REGULAR")
.withValidation()
.as(RegistryPipelineOptions.class);
assertThat(options.getJpaTransactionManagerType()).isEqualTo(JpaTransactionManagerType.REGULAR);
}
@Test
void jpaTransactionManagerType_bulkQueryJpa() {
RegistryPipelineOptions options =
PipelineOptionsFactory.fromArgs(
"--registryEnvironment=" + RegistryEnvironment.UNITTEST.name(),
"--jpaTransactionManagerType=BULK_QUERY")
.withValidation()
.as(RegistryPipelineOptions.class);
assertThat(options.getJpaTransactionManagerType())
.isEqualTo(JpaTransactionManagerType.BULK_QUERY);
}
}
@@ -16,7 +16,7 @@ package google.registry.beam.initsql;
import static com.google.common.base.Preconditions.checkState;
import static google.registry.model.ofy.ObjectifyService.auditedOfy;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.persistence.transaction.TransactionManagerFactory.ofyTm;
import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.datastore.DatastoreServiceFactory;
@@ -75,7 +75,8 @@ public final class BackupTestStore implements AutoCloseable {
/** Returns the timestamp of the transaction. */
long transact(Iterable<Object> deletes, Iterable<Object> newOrUpdated) {
long timestamp = fakeClock.nowUtc().getMillis();
tm().transact(
ofyTm()
.transact(
() -> {
auditedOfy().delete().entities(deletes);
auditedOfy().save().entities(newOrUpdated);
@@ -91,7 +92,7 @@ public final class BackupTestStore implements AutoCloseable {
@SafeVarargs
public final long insertOrUpdate(Object... entities) {
long timestamp = fakeClock.nowUtc().getMillis();
tm().transact(() -> auditedOfy().save().entities(entities).now());
ofyTm().transact(() -> auditedOfy().save().entities(entities).now());
fakeClock.advanceOneMilli();
return timestamp;
}
@@ -100,7 +101,7 @@ public final class BackupTestStore implements AutoCloseable {
@SafeVarargs
public final long delete(Object... entities) {
long timestamp = fakeClock.nowUtc().getMillis();
tm().transact(() -> auditedOfy().delete().entities(entities).now());
ofyTm().transact(() -> auditedOfy().delete().entities(entities).now());
fakeClock.advanceOneMilli();
return timestamp;
}
@@ -71,7 +71,7 @@ class CommitLogTransformsTest implements Serializable {
@RegisterExtension
@Order(value = 1)
final transient DatastoreEntityExtension datastoreEntityExtension =
new DatastoreEntityExtension();
new DatastoreEntityExtension().allThreads(true);
@RegisterExtension
final transient TestPipelineExtension testPipeline =
@@ -30,7 +30,6 @@ import static google.registry.util.DateTimeUtils.START_OF_TIME;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.googlecode.objectify.Key;
import google.registry.backup.AppEngineEnvironment;
import google.registry.beam.TestPipelineExtension;
import google.registry.flows.domain.DomainFlowUtils;
import google.registry.model.billing.BillingEvent;
@@ -106,7 +105,8 @@ class InitSqlPipelineTest {
@RegisterExtension
@Order(Order.DEFAULT - 1)
final transient DatastoreEntityExtension datastore = new DatastoreEntityExtension();
final transient DatastoreEntityExtension datastore =
new DatastoreEntityExtension("test").allThreads(true);
@RegisterExtension final transient InjectExtension injectExtension = new InjectExtension();
@@ -331,20 +331,18 @@ class InitSqlPipelineTest {
.as(InitSqlPipelineOptions.class);
InitSqlPipeline initSqlPipeline = new InitSqlPipeline(options);
initSqlPipeline.run(testPipeline).waitUntilFinish();
try (AppEngineEnvironment env = new AppEngineEnvironment("test")) {
assertHostResourceEquals(
jpaTm().transact(() -> jpaTm().loadByKey(hostResource.createVKey())), hostResource);
assertThat(jpaTm().transact(() -> jpaTm().loadAllOf(Registrar.class)))
.comparingElementsUsing(immutableObjectCorrespondence("lastUpdateTime"))
.containsExactly(registrar1, registrar2);
assertThat(jpaTm().transact(() -> jpaTm().loadAllOf(ContactResource.class)))
.comparingElementsUsing(immutableObjectCorrespondence("revisions", "updateTimestamp"))
.containsExactly(contact1, contact2);
assertDomainEquals(jpaTm().transact(() -> jpaTm().loadByKey(domain.createVKey())), domain);
assertThat(jpaTm().transact(() -> jpaTm().loadAllOf(Cursor.class)))
.comparingElementsUsing(immutableObjectCorrespondence())
.containsExactly(globalCursor, tldCursor);
}
assertHostResourceEquals(
jpaTm().transact(() -> jpaTm().loadByKey(hostResource.createVKey())), hostResource);
assertThat(jpaTm().transact(() -> jpaTm().loadAllOf(Registrar.class)))
.comparingElementsUsing(immutableObjectCorrespondence("lastUpdateTime"))
.containsExactly(registrar1, registrar2);
assertThat(jpaTm().transact(() -> jpaTm().loadAllOf(ContactResource.class)))
.comparingElementsUsing(immutableObjectCorrespondence("revisions", "updateTimestamp"))
.containsExactly(contact1, contact2);
assertDomainEquals(jpaTm().transact(() -> jpaTm().loadByKey(domain.createVKey())), domain);
assertThat(jpaTm().transact(() -> jpaTm().loadAllOf(Cursor.class)))
.comparingElementsUsing(immutableObjectCorrespondence())
.containsExactly(globalCursor, tldCursor);
}
private static void assertHostResourceEquals(HostResource actual, HostResource expected) {
@@ -26,7 +26,6 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.Streams;
import com.google.common.truth.Truth;
import com.google.storage.onestore.v3.OnestoreEntity.EntityProto;
import google.registry.backup.AppEngineEnvironment;
import google.registry.backup.VersionedEntity;
import java.io.Serializable;
import java.util.Collection;
@@ -124,20 +123,18 @@ public final class InitSqlTestUtils {
public void processElement(
@Element KV<String, Iterable<VersionedEntity>> input,
OutputReceiver<String> out) {
try (AppEngineEnvironment env = new AppEngineEnvironment()) {
ImmutableList<KV<Long, Object>> actual =
Streams.stream(input.getValue())
.map(InitSqlTestUtils::rawEntityToOfyWithTimestamp)
.collect(ImmutableList.toImmutableList());
try {
Truth.assertThat(actual)
.containsExactlyElementsIn(
Stream.of(expected)
.map(InitSqlTestUtils::expectedToOfyWithTimestamp)
.collect(ImmutableList.toImmutableList()));
} catch (AssertionError e) {
out.output(e.toString());
}
ImmutableList<KV<Long, Object>> actual =
Streams.stream(input.getValue())
.map(InitSqlTestUtils::rawEntityToOfyWithTimestamp)
.collect(ImmutableList.toImmutableList());
try {
Truth.assertThat(actual)
.containsExactlyElementsIn(
Stream.of(expected)
.map(InitSqlTestUtils::expectedToOfyWithTimestamp)
.collect(ImmutableList.toImmutableList()));
} catch (AssertionError e) {
out.output(e.toString());
}
}
}));
@@ -95,7 +95,7 @@ class LoadDatastoreSnapshotTest {
@RegisterExtension
@Order(value = 1)
final transient DatastoreEntityExtension datastoreEntityExtension =
new DatastoreEntityExtension();
new DatastoreEntityExtension().allThreads(true);
@RegisterExtension
final transient TestPipelineExtension testPipeline =
@@ -0,0 +1,64 @@
// 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.beam.initsql;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.beam.initsql.Transforms.repairBadData;
import static google.registry.model.ofy.ObjectifyService.auditedOfy;
import static google.registry.persistence.transaction.TransactionManagerFactory.ofyTm;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.newDomainBase;
import static google.registry.testing.DatabaseHelper.newHostResource;
import com.google.appengine.api.datastore.Entity;
import google.registry.model.domain.DomainBase;
import google.registry.model.host.HostResource;
import google.registry.testing.AppEngineExtension;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
/** Unit tests for {@link Transforms}. */
public class TransformsTest {
@RegisterExtension
public final AppEngineExtension appEngine =
AppEngineExtension.builder().withDatastoreAndCloudSql().build();
@BeforeEach
void beforeEach() {
createTld("tld");
}
@Test
void testRepairBadData_canonicalizesDomainName() {
DomainBase domain = newDomainBase("foobar.tld");
Entity entity = ofyTm().transact(() -> auditedOfy().toEntity(domain));
entity.setIndexedProperty("fullyQualifiedDomainName", "FOOBäR.TLD");
assertThat(((DomainBase) auditedOfy().toPojo(repairBadData(entity))).getDomainName())
.isEqualTo("xn--foobr-jra.tld");
}
@Test
void testRepairBadData_canonicalizesHostName() {
HostResource host = newHostResource("baz.foobar.tld");
Entity entity = ofyTm().transact(() -> auditedOfy().toEntity(host));
entity.setIndexedProperty(
"fullyQualifiedHostName", "b̴̹͔͓̣̭̫͇͕̻̬̱͇͗͌́̆̋͒a̶̬̖͚̋̈́̽̇͝͠z̵͠.FOOBäR.TLD");
assertThat(((HostResource) auditedOfy().toPojo(repairBadData(entity))).getHostName())
.isEqualTo(
"xn--baz-kdcb2ajgzb4jtg6doej4e6b9am7c7b6c5nd4k7gpa2a9a7dufyewec.xn--foobr-jra.tld");
}
}
@@ -17,9 +17,6 @@ package google.registry.beam.invoicing;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.tld.Registry.TldState.GENERAL_AVAILABILITY;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.persistence.transaction.TransactionManagerFactory.removeTmOverrideForTest;
import static google.registry.persistence.transaction.TransactionManagerFactory.setTmForTest;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.newRegistry;
import static google.registry.testing.DatabaseHelper.persistActiveDomain;
@@ -48,6 +45,7 @@ import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationT
import google.registry.testing.DatastoreEntityExtension;
import google.registry.testing.FakeClock;
import google.registry.testing.TestDataHelper;
import google.registry.testing.TmOverrideExtension;
import google.registry.util.ResourceUtils;
import java.io.File;
import java.nio.file.Files;
@@ -77,6 +75,25 @@ import org.junit.jupiter.api.io.TempDir;
/** Unit tests for {@link InvoicingPipeline}. */
class InvoicingPipelineTest {
@RegisterExtension
@Order(Order.DEFAULT - 1)
final transient DatastoreEntityExtension datastore =
new DatastoreEntityExtension().allThreads(true);
@RegisterExtension
final TestPipelineExtension pipeline =
TestPipelineExtension.create().enableAbandonedNodeEnforcement(true);
@RegisterExtension
final JpaIntegrationTestExtension database =
new JpaTestExtensions.Builder().withClock(new FakeClock()).buildIntegrationTestExtension();
@RegisterExtension
@Order(Order.DEFAULT + 1)
TmOverrideExtension tmOverrideExtension = TmOverrideExtension.withJpa();
@TempDir Path tmpDir;
private static final String BILLING_BUCKET_URL = "billing_bucket";
private static final String YEAR_MONTH = "2017-10";
private static final String INVOICE_FILE_PREFIX = "REG-INV";
@@ -225,20 +242,6 @@ class InvoicingPipelineTest {
"2017-10-01,2018-09-30,456,20.50,USD,10125,1,PURCHASE,bestdomains - test,1,"
+ "RENEW | TLD: test | TERM: 1-year,20.50,USD,116688");
@RegisterExtension
@Order(Order.DEFAULT - 1)
final transient DatastoreEntityExtension datastore = new DatastoreEntityExtension();
@RegisterExtension
final TestPipelineExtension pipeline =
TestPipelineExtension.create().enableAbandonedNodeEnforcement(true);
@RegisterExtension
final JpaIntegrationTestExtension database =
new JpaTestExtensions.Builder().withClock(new FakeClock()).buildIntegrationTestExtension();
@TempDir Path tmpDir;
private final InvoicingPipelineOptions options =
PipelineOptionsFactory.create().as(InvoicingPipelineOptions.class);
@@ -260,13 +263,12 @@ class InvoicingPipelineTest {
String query = InvoicingPipeline.makeQuery("2017-10", "my-project-id");
assertThat(query)
.isEqualTo(TestDataHelper.loadFile(this.getClass(), "billing_events_test.sql"));
// This is necessary because the TestPipelineExtension verifies that the pipelien is run.
// This is necessary because the TestPipelineExtension verifies that the pipeline is run.
pipeline.run();
}
@Test
void testSuccess_fullSqlPipeline() throws Exception {
setTmForTest(jpaTm());
setupCloudSql();
options.setDatabase("CLOUD_SQL");
InvoicingPipeline invoicingPipeline = new InvoicingPipeline(options);
@@ -281,18 +283,15 @@ class InvoicingPipelineTest {
+ "UnitPriceCurrency,PONumber");
assertThat(overallInvoice.subList(1, overallInvoice.size()))
.containsExactlyElementsIn(EXPECTED_INVOICE_OUTPUT);
removeTmOverrideForTest();
}
@Test
void testSuccess_readFromCloudSql() throws Exception {
setTmForTest(jpaTm());
setupCloudSql();
PCollection<BillingEvent> billingEvents = InvoicingPipeline.readFromCloudSql(options, pipeline);
billingEvents = billingEvents.apply(new ChangeDomainRepo());
PAssert.that(billingEvents).containsInAnyOrder(INPUT_EVENTS);
pipeline.run().waitUntilFinish();
removeTmOverrideForTest();
}
@Test
@@ -22,7 +22,6 @@ import static google.registry.beam.rde.RdePipeline.encodePendings;
import static google.registry.model.common.Cursor.CursorType.RDE_STAGING;
import static google.registry.model.rde.RdeMode.FULL;
import static google.registry.model.rde.RdeMode.THIN;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.rde.RdeResourceType.CONTACT;
import static google.registry.rde.RdeResourceType.DOMAIN;
@@ -64,14 +63,16 @@ import google.registry.model.tld.Registry;
import google.registry.persistence.VKey;
import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension;
import google.registry.persistence.transaction.TransactionManagerFactory;
import google.registry.rde.DepositFragment;
import google.registry.rde.Ghostryde;
import google.registry.rde.PendingDeposit;
import google.registry.rde.RdeResourceType;
import google.registry.testing.CloudTasksHelper;
import google.registry.testing.CloudTasksHelper.TaskMatcher;
import google.registry.testing.DatastoreEntityExtension;
import google.registry.testing.FakeClock;
import google.registry.testing.FakeKeyringModule;
import google.registry.testing.TmOverrideExtension;
import java.io.IOException;
import java.util.function.Function;
import java.util.regex.Matcher;
@@ -86,7 +87,6 @@ import org.bouncycastle.openpgp.PGPPrivateKey;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.joda.time.DateTime;
import org.joda.time.Duration;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
@@ -134,6 +134,8 @@ public class RdePipelineTest {
private final GcsUtils gcsUtils = new GcsUtils(LocalStorageHelper.getOptions());
private final CloudTasksHelper cloudTasksHelper = new CloudTasksHelper();
private final PGPPublicKey encryptionKey =
new FakeKeyringModule().get().getRdeStagingEncryptionKey();
@@ -152,6 +154,10 @@ public class RdePipelineTest {
final JpaIntegrationTestExtension database =
new JpaTestExtensions.Builder().withClock(clock).buildIntegrationTestExtension();
@RegisterExtension
@Order(Order.DEFAULT + 1)
TmOverrideExtension tmOverrideExtension = TmOverrideExtension.withJpa();
@RegisterExtension
final TestPipelineExtension pipeline =
TestPipelineExtension.fromOptions(options).enableAbandonedNodeEnforcement(true);
@@ -160,7 +166,6 @@ public class RdePipelineTest {
@BeforeEach
void beforeEach() throws Exception {
TransactionManagerFactory.setTmForTest(jpaTm());
loadInitialData();
// Two real registrars have been created by loadInitialData(), named "New Registrar" and "The
@@ -212,14 +217,9 @@ public class RdePipelineTest {
options.setValidationMode("LENIENT");
options.setStagingKey(
BaseEncoding.base64Url().encode(PgpHelper.convertPublicKeyToBytes(encryptionKey)));
options.setGcsBucket("gcs-bucket");
options.setRdeStagingBucket("gcs-bucket");
options.setJobName("rde-job");
rdePipeline = new RdePipeline(options, gcsUtils);
}
@AfterEach
void afterEach() {
TransactionManagerFactory.removeTmOverrideForTest();
rdePipeline = new RdePipeline(options, gcsUtils, cloudTasksHelper.getTestCloudTasksUtils());
}
@Test
@@ -314,6 +314,21 @@ public class RdePipelineTest {
assertThat(loadCursorTime(CursorType.RDE_STAGING))
.isEquivalentAccordingToCompareTo(now.plus(Duration.standardDays(1)));
assertThat(loadRevision(now, FULL)).isEqualTo(1);
cloudTasksHelper.assertTasksEnqueued(
"brda",
new TaskMatcher()
.url("/_dr/task/brdaCopy")
.service("backend")
.param("tld", "soy")
.param("watermark", now.toString())
.param("prefix", "rde-job/"));
cloudTasksHelper.assertTasksEnqueued(
"rde-upload",
new TaskMatcher()
.url("/_dr/task/rdeUpload")
.service("backend")
.param("tld", "soy")
.param("prefix", "rde-job/"));
}
// The GCS folder listing can be a bit flaky, so retry if necessary
@@ -337,6 +352,7 @@ public class RdePipelineTest {
assertThat(loadCursorTime(CursorType.RDE_STAGING)).isEquivalentAccordingToCompareTo(now);
assertThat(loadRevision(now, FULL)).isEqualTo(0);
cloudTasksHelper.assertNoTasksEnqueued("brda", "rde-upload");
}
private void verifyFiles(
@@ -18,8 +18,6 @@ import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.ImmutableObjectSubject.immutableObjectCorrespondence;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.persistence.transaction.TransactionManagerFactory.removeTmOverrideForTest;
import static google.registry.persistence.transaction.TransactionManagerFactory.setTmForTest;
import static google.registry.testing.AppEngineExtension.makeRegistrar1;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.persistActiveContact;
@@ -52,6 +50,7 @@ import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationT
import google.registry.testing.DatastoreEntityExtension;
import google.registry.testing.FakeClock;
import google.registry.testing.FakeSleeper;
import google.registry.testing.TmOverrideExtension;
import google.registry.util.ResourceUtils;
import google.registry.util.Retrier;
import java.io.File;
@@ -116,7 +115,8 @@ class Spec11PipelineTest {
@RegisterExtension
@Order(Order.DEFAULT - 1)
final transient DatastoreEntityExtension datastore = new DatastoreEntityExtension();
final transient DatastoreEntityExtension datastore =
new DatastoreEntityExtension().allThreads(true);
@TempDir Path tmpDir;
@@ -128,6 +128,10 @@ class Spec11PipelineTest {
final JpaIntegrationTestExtension database =
new JpaTestExtensions.Builder().withClock(new FakeClock()).buildIntegrationTestExtension();
@RegisterExtension
@Order(Order.DEFAULT + 1)
TmOverrideExtension tmOverrideExtension = TmOverrideExtension.withJpa();
private final Spec11PipelineOptions options =
PipelineOptionsFactory.create().as(Spec11PipelineOptions.class);
@@ -232,7 +236,6 @@ class Spec11PipelineTest {
}
private void setupCloudSql() {
setTmForTest(jpaTm());
persistNewRegistrar("TheRegistrar");
persistNewRegistrar("NewRegistrar");
Registrar registrar1 =
@@ -272,7 +275,6 @@ class Spec11PipelineTest {
persistResource(createDomain("no-email.com", "2A4BA9BBC-COM", registrar2, contact2));
persistResource(
createDomain("anti-anti-anti-virus.dev", "555666888-DEV", registrar3, contact3));
removeTmOverrideForTest();
}
private void verifySaveToGcs() throws Exception {
@@ -42,6 +42,7 @@ import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;
import google.registry.flows.EppException;
import google.registry.flows.EppRequestSource;
import google.registry.flows.FlowUtils.UnknownCurrencyEppException;
import google.registry.flows.ResourceFlowTestCase;
import google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException;
@@ -533,6 +534,55 @@ class DomainRenewFlowTest extends ResourceFlowTestCase<DomainRenewFlow, DomainBa
.build());
}
@TestOfyAndSql
void testSuccess_metaData_withReasonAndRequestedByRegistrar() throws Exception {
eppRequestSource = EppRequestSource.TOOL;
setEppInput(
"domain_renew_metadata_with_reason_and_requestedByRegistrar.xml",
ImmutableMap.of(
"DOMAIN",
"example.tld",
"EXPDATE",
"2000-04-03",
"YEARS",
"1",
"REASON",
"domain-renew-test",
"REQUESTED",
"false"));
persistDomain();
runFlow();
DomainBase domain = reloadResourceByForeignKey();
assertAboutDomains()
.that(domain)
.hasOneHistoryEntryEachOfTypes(
HistoryEntry.Type.DOMAIN_CREATE, HistoryEntry.Type.DOMAIN_RENEW);
assertAboutHistoryEntries()
.that(getOnlyHistoryEntryOfType(domain, HistoryEntry.Type.DOMAIN_RENEW))
.hasMetadataReason("domain-renew-test")
.and()
.hasMetadataRequestedByRegistrar(false);
}
@TestOfyAndSql
void testSuccess_metaData_withRequestedByRegistrarOnly() throws Exception {
eppRequestSource = EppRequestSource.TOOL;
setEppInput("domain_renew_metadata_with_requestedByRegistrar_only.xml");
persistDomain();
runFlow();
DomainBase domain1 = reloadResourceByForeignKey();
assertAboutDomains()
.that(domain1)
.hasOneHistoryEntryEachOfTypes(
HistoryEntry.Type.DOMAIN_CREATE, HistoryEntry.Type.DOMAIN_RENEW);
assertAboutHistoryEntries()
.that(getOnlyHistoryEntryOfType(domain1, HistoryEntry.Type.DOMAIN_RENEW))
.hasMetadataReason(null)
.and()
.hasMetadataRequestedByRegistrar(true);
}
@TestOfyAndSql
void testFailure_neverExisted() throws Exception {
ResourceDoesNotExistException thrown =
@@ -412,6 +412,10 @@ public final class ImmutableObjectSubject extends Subject {
// don't use ImmutableMap or a stream->collect model since we can have nulls
Map<Field, Object> result = new LinkedHashMap<>();
for (Map.Entry<Field, Object> entry : originalFields.entrySet()) {
// TODO(b/203685960): filter by @DoNotCompare instead.
if (entry.getKey().isAnnotationPresent(ImmutableObject.Insignificant.class)) {
continue;
}
if (!ignoredFieldSet.contains(entry.getKey().getName())) {
result.put(entry.getKey(), entry.getValue());
}
@@ -426,7 +430,9 @@ public final class ImmutableObjectSubject extends Subject {
// don't use ImmutableMap or a stream->collect model since we can have nulls
Map<Field, Object> result = new LinkedHashMap<>();
for (Map.Entry<Field, Object> entry : originalFields.entrySet()) {
if (!entry.getKey().isAnnotationPresent(annotation)) {
// TODO(b/203685960): filter by @DoNotCompare instead.
if (!entry.getKey().isAnnotationPresent(annotation)
&& !entry.getKey().isAnnotationPresent(ImmutableObject.Insignificant.class)) {
// Perform any necessary substitutions.
if (entry.getKey().isAnnotationPresent(ImmutableObject.EmptySetToNull.class)
@@ -0,0 +1,87 @@
// 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.model.bulkquery;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.DomainHistory;
import google.registry.model.domain.DomainHistory.DomainHistoryId;
import google.registry.model.domain.GracePeriod;
import google.registry.model.domain.GracePeriod.GracePeriodHistory;
import google.registry.model.domain.secdns.DelegationSignerData;
import google.registry.model.domain.secdns.DomainDsDataHistory;
import google.registry.model.reporting.DomainTransactionRecord;
import google.registry.persistence.VKey;
/**
* Helpers for bulk-loading {@link google.registry.model.domain.DomainBase} and {@link
* google.registry.model.domain.DomainHistory} entities in <em>tests</em>.
*/
public class BulkQueryHelper {
static DomainBase loadAndAssembleDomainBase(String domainRepoId) {
return jpaTm()
.transact(
() ->
BulkQueryEntities.assembleDomainBase(
jpaTm().loadByKey(DomainBaseLite.createVKey(domainRepoId)),
jpaTm()
.loadAllOfStream(GracePeriod.class)
.filter(gracePeriod -> gracePeriod.getDomainRepoId().equals(domainRepoId))
.collect(toImmutableSet()),
jpaTm()
.loadAllOfStream(DelegationSignerData.class)
.filter(dsData -> dsData.getDomainRepoId().equals(domainRepoId))
.collect(toImmutableSet()),
jpaTm()
.loadAllOfStream(DomainHost.class)
.filter(domainHost -> domainHost.getDomainRepoId().equals(domainRepoId))
.map(DomainHost::getHostVKey)
.collect(toImmutableSet())));
}
static DomainHistory loadAndAssembleDomainHistory(DomainHistoryId domainHistoryId) {
return jpaTm()
.transact(
() ->
BulkQueryEntities.assembleDomainHistory(
jpaTm().loadByKey(VKey.createSql(DomainHistoryLite.class, domainHistoryId)),
jpaTm()
.loadAllOfStream(DomainDsDataHistory.class)
.filter(
domainDsDataHistory ->
domainDsDataHistory.getDomainHistoryId().equals(domainHistoryId))
.collect(toImmutableSet()),
jpaTm()
.loadAllOfStream(DomainHistoryHost.class)
.filter(
domainHistoryHost ->
domainHistoryHost.getDomainHistoryId().equals(domainHistoryId))
.map(DomainHistoryHost::getHostVKey)
.collect(toImmutableSet()),
jpaTm()
.loadAllOfStream(GracePeriodHistory.class)
.filter(
gracePeriodHistory ->
gracePeriodHistory.getDomainHistoryId().equals(domainHistoryId))
.collect(toImmutableSet()),
jpaTm()
.loadAllOfStream(DomainTransactionRecord.class)
.filter(x -> true)
.collect(toImmutableSet())));
}
}
@@ -0,0 +1,117 @@
// 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.model.bulkquery;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static org.joda.time.DateTimeZone.UTC;
import com.google.common.collect.Sets;
import com.google.common.collect.Sets.SetView;
import com.google.common.truth.Truth8;
import google.registry.model.domain.DomainBase;
import google.registry.testing.AppEngineExtension;
import google.registry.testing.FakeClock;
import java.util.Set;
import java.util.stream.Collectors;
import javax.persistence.metamodel.Attribute;
import org.joda.time.DateTime;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
/** Unit tests for reading {@link DomainBaseLite}. */
class DomainBaseLiteTest {
protected FakeClock fakeClock = new FakeClock(DateTime.now(UTC));
@RegisterExtension
public final AppEngineExtension appEngine =
AppEngineExtension.builder().withDatastoreAndCloudSql().withClock(fakeClock).build();
private final TestSetupHelper setupHelper = new TestSetupHelper(fakeClock);
@BeforeEach
void setUp() {
setupHelper.initializeAllEntities();
}
@AfterEach
void afterEach() {
setupHelper.tearDownBulkQueryJpaTm();
}
@Test
void readDomainHost() {
setupHelper.applyChangeToDomainAndHistory();
setupHelper.setupBulkQueryJpaTm(appEngine);
Truth8.assertThat(
jpaTm().transact(() -> jpaTm().loadAllOf(DomainHost.class)).stream()
.map(DomainHost::getHostVKey))
.containsExactly(setupHelper.host.createVKey());
}
@Test
void domainBaseLiteAttributes_versusDomainBase() {
Set<String> domainBaseAttributes =
jpaTm()
.transact(
() ->
jpaTm()
.getEntityManager()
.getMetamodel()
.entity(DomainBase.class)
.getAttributes())
.stream()
.map(Attribute::getName)
.collect(Collectors.toSet());
setupHelper.setupBulkQueryJpaTm(appEngine);
Set<String> domainBaseLiteAttributes =
jpaTm()
.transact(
() ->
jpaTm()
.getEntityManager()
.getMetamodel()
.entity(DomainBaseLite.class)
.getAttributes())
.stream()
.map(Attribute::getName)
.collect(Collectors.toSet());
assertThat(domainBaseAttributes).containsAtLeastElementsIn(domainBaseLiteAttributes);
SetView<?> excludedFromDomainBase =
Sets.difference(domainBaseAttributes, domainBaseLiteAttributes);
assertThat(excludedFromDomainBase)
.containsExactly("internalDelegationSignerData", "internalGracePeriods", "nsHosts");
}
@Test
void readDomainBaseLite_simple() {
setupHelper.setupBulkQueryJpaTm(appEngine);
assertThat(BulkQueryHelper.loadAndAssembleDomainBase(TestSetupHelper.DOMAIN_REPO_ID))
.isEqualTo(setupHelper.domain);
}
@Test
void readDomainBaseLite_full() {
setupHelper.applyChangeToDomainAndHistory();
setupHelper.setupBulkQueryJpaTm(appEngine);
assertThat(BulkQueryHelper.loadAndAssembleDomainBase(TestSetupHelper.DOMAIN_REPO_ID))
.isEqualTo(setupHelper.domain);
}
}
@@ -0,0 +1,125 @@
// 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.model.bulkquery;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static org.joda.time.DateTimeZone.UTC;
import com.google.common.collect.Sets;
import com.google.common.collect.Sets.SetView;
import com.google.common.truth.Truth8;
import google.registry.model.domain.DomainHistory;
import google.registry.testing.AppEngineExtension;
import google.registry.testing.FakeClock;
import java.util.Set;
import java.util.stream.Collectors;
import javax.persistence.metamodel.Attribute;
import org.joda.time.DateTime;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
/** Unit tests for {@link DomainHistoryLite}. */
public class DomainHistoryLiteTest {
protected FakeClock fakeClock = new FakeClock(DateTime.now(UTC));
@RegisterExtension
public final AppEngineExtension appEngine =
AppEngineExtension.builder().withDatastoreAndCloudSql().withClock(fakeClock).build();
private final TestSetupHelper setupHelper = new TestSetupHelper(fakeClock);
@BeforeEach
void setUp() {
setupHelper.initializeAllEntities();
}
@AfterEach
void afterEach() {
setupHelper.tearDownBulkQueryJpaTm();
}
@Test
void readDomainHistoryHost() {
setupHelper.applyChangeToDomainAndHistory();
setupHelper.setupBulkQueryJpaTm(appEngine);
Truth8.assertThat(
jpaTm().transact(() -> jpaTm().loadAllOf(DomainHistoryHost.class)).stream()
.map(DomainHistoryHost::getHostVKey))
.containsExactly(setupHelper.host.createVKey());
}
@Test
void domainHistoryLiteAttributes_versusDomainHistory() {
Set<String> domainHistoryAttributes =
jpaTm()
.transact(
() ->
jpaTm()
.getEntityManager()
.getMetamodel()
.entity(DomainHistory.class)
.getAttributes())
.stream()
.map(Attribute::getName)
.collect(Collectors.toSet());
setupHelper.setupBulkQueryJpaTm(appEngine);
Set<String> domainHistoryLiteAttributes =
jpaTm()
.transact(
() ->
jpaTm()
.getEntityManager()
.getMetamodel()
.entity(DomainHistoryLite.class)
.getAttributes())
.stream()
.map(Attribute::getName)
.collect(Collectors.toSet());
assertThat(domainHistoryAttributes).containsAtLeastElementsIn(domainHistoryLiteAttributes);
SetView<?> excludedFromDomainHistory =
Sets.difference(domainHistoryAttributes, domainHistoryLiteAttributes);
assertThat(excludedFromDomainHistory)
.containsExactly(
"dsDataHistories",
"gracePeriodHistories",
"internalDomainTransactionRecords",
"nsHosts");
}
@Test
void readDomainHistory_noContent() {
setupHelper.setupBulkQueryJpaTm(appEngine);
assertThat(
BulkQueryHelper.loadAndAssembleDomainHistory(
setupHelper.domainHistory.getDomainHistoryId()))
.isEqualTo(setupHelper.domainHistory);
}
@Test
void readDomainHistory_full() {
setupHelper.applyChangeToDomainAndHistory();
setupHelper.setupBulkQueryJpaTm(appEngine);
assertThat(
BulkQueryHelper.loadAndAssembleDomainHistory(
setupHelper.domainHistory.getDomainHistoryId()))
.isEqualTo(setupHelper.domainHistory);
}
}
@@ -0,0 +1,210 @@
// 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.model.bulkquery;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.testing.SqlHelper.saveRegistrar;
import static google.registry.util.DateTimeUtils.END_OF_TIME;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.base.Ascii;
import com.google.common.collect.ImmutableSet;
import google.registry.model.contact.ContactResource;
import google.registry.model.domain.DesignatedContact;
import google.registry.model.domain.DomainAuthInfo;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.DomainHistory;
import google.registry.model.domain.GracePeriod;
import google.registry.model.domain.Period;
import google.registry.model.domain.launch.LaunchNotice;
import google.registry.model.domain.rgp.GracePeriodStatus;
import google.registry.model.domain.secdns.DelegationSignerData;
import google.registry.model.eppcommon.AuthInfo.PasswordAuth;
import google.registry.model.eppcommon.StatusValue;
import google.registry.model.eppcommon.Trid;
import google.registry.model.host.HostResource;
import google.registry.model.registrar.Registrar;
import google.registry.model.reporting.DomainTransactionRecord;
import google.registry.model.reporting.DomainTransactionRecord.TransactionReportField;
import google.registry.model.reporting.HistoryEntry;
import google.registry.model.tld.Registry;
import google.registry.model.transfer.ContactTransferData;
import google.registry.persistence.BulkQueryJpaFactory;
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension;
import google.registry.persistence.transaction.JpaTransactionManager;
import google.registry.persistence.transaction.TransactionManagerFactory;
import google.registry.testing.AppEngineExtension;
import google.registry.testing.DatabaseHelper;
import google.registry.testing.FakeClock;
/** Entity creation utilities for domain-related tests. */
class TestSetupHelper {
public static final String TLD = "tld";
public static final String DOMAIN_REPO_ID = "4-TLD";
public static final String DOMAIN_NAME = "example.tld";
public static final String REGISTRAR_ID = "AnRegistrar";
private final FakeClock fakeClock;
Registry registry;
Registrar registrar;
ContactResource contact;
DomainBase domain;
DomainHistory domainHistory;
HostResource host;
private JpaTransactionManager originalJpaTm;
private JpaTransactionManager bulkQueryJpaTm;
TestSetupHelper(FakeClock fakeClock) {
this.fakeClock = fakeClock;
}
void initializeAllEntities() {
registry = putInDb(DatabaseHelper.newRegistry(TLD, Ascii.toUpperCase(TLD)));
registrar = saveRegistrar(REGISTRAR_ID);
contact = putInDb(createContact(DOMAIN_REPO_ID, REGISTRAR_ID));
domain = putInDb(createSimpleDomain(contact));
domainHistory = putInDb(createHistoryWithoutContent(domain, fakeClock));
host = putInDb(createHost());
}
void applyChangeToDomainAndHistory() {
domain = putInDb(createFullDomain(contact, host, fakeClock));
domainHistory = putInDb(createFullHistory(domain, fakeClock));
}
void setupBulkQueryJpaTm(AppEngineExtension appEngineExtension) {
bulkQueryJpaTm =
BulkQueryJpaFactory.createBulkQueryJpaTransactionManager(
appEngineExtension
.getJpaIntegrationTestExtension()
.map(JpaIntegrationTestExtension::getJpaProperties)
.orElseThrow(
() -> new IllegalStateException("Expecting JpaIntegrationTestExtension.")),
fakeClock);
originalJpaTm = TransactionManagerFactory.jpaTm();
TransactionManagerFactory.setJpaTm(() -> bulkQueryJpaTm);
}
void tearDownBulkQueryJpaTm() {
if (bulkQueryJpaTm != null) {
bulkQueryJpaTm.teardown();
TransactionManagerFactory.setJpaTm(() -> originalJpaTm);
}
}
static ContactResource createContact(String repoId, String registrarId) {
return new ContactResource.Builder()
.setRepoId(repoId)
.setCreationRegistrarId(registrarId)
.setTransferData(new ContactTransferData.Builder().build())
.setPersistedCurrentSponsorRegistrarId(registrarId)
.build();
}
static DomainBase createSimpleDomain(ContactResource contact) {
return DatabaseHelper.newDomainBase(DOMAIN_NAME, DOMAIN_REPO_ID, contact)
.asBuilder()
.setCreationRegistrarId(REGISTRAR_ID)
.setPersistedCurrentSponsorRegistrarId(REGISTRAR_ID)
.build();
}
static DomainBase createFullDomain(
ContactResource contact, HostResource host, FakeClock fakeClock) {
return createSimpleDomain(contact)
.asBuilder()
.setDomainName(DOMAIN_NAME)
.setRepoId(DOMAIN_REPO_ID)
.setCreationRegistrarId(REGISTRAR_ID)
.setLastEppUpdateTime(fakeClock.nowUtc())
.setLastEppUpdateRegistrarId(REGISTRAR_ID)
.setLastTransferTime(fakeClock.nowUtc())
.setNameservers(host.createVKey())
.setStatusValues(
ImmutableSet.of(
StatusValue.CLIENT_DELETE_PROHIBITED,
StatusValue.SERVER_DELETE_PROHIBITED,
StatusValue.SERVER_TRANSFER_PROHIBITED,
StatusValue.SERVER_UPDATE_PROHIBITED,
StatusValue.SERVER_RENEW_PROHIBITED,
StatusValue.SERVER_HOLD))
.setContacts(
ImmutableSet.of(
DesignatedContact.create(DesignatedContact.Type.ADMIN, contact.createVKey())))
.setSubordinateHosts(ImmutableSet.of("ns1.example.com"))
.setPersistedCurrentSponsorRegistrarId(REGISTRAR_ID)
.setRegistrationExpirationTime(fakeClock.nowUtc().plusYears(1))
.setAuthInfo(DomainAuthInfo.create(PasswordAuth.create("password")))
.setDsData(ImmutableSet.of(DelegationSignerData.create(1, 2, 3, new byte[] {0, 1, 2})))
.setLaunchNotice(LaunchNotice.create("tcnid", "validatorId", START_OF_TIME, START_OF_TIME))
.setSmdId("smdid")
.addGracePeriod(
GracePeriod.create(
GracePeriodStatus.ADD, DOMAIN_REPO_ID, END_OF_TIME, REGISTRAR_ID, null, 100L))
.build();
}
static HostResource createHost() {
return new HostResource.Builder()
.setRepoId("host1")
.setHostName("ns1.example.com")
.setCreationRegistrarId(REGISTRAR_ID)
.setPersistedCurrentSponsorRegistrarId(REGISTRAR_ID)
.build();
}
static DomainTransactionRecord createDomainTransactionRecord(FakeClock fakeClock) {
return new DomainTransactionRecord.Builder()
.setTld(TLD)
.setReportingTime(fakeClock.nowUtc())
.setReportField(TransactionReportField.NET_ADDS_1_YR)
.setReportAmount(1)
.build();
}
static DomainHistory createHistoryWithoutContent(DomainBase domain, FakeClock fakeClock) {
return new DomainHistory.Builder()
.setType(HistoryEntry.Type.DOMAIN_CREATE)
.setXmlBytes("<xml></xml>".getBytes(UTF_8))
.setModificationTime(fakeClock.nowUtc())
.setRegistrarId(REGISTRAR_ID)
.setTrid(Trid.create("ABC-123", "server-trid"))
.setBySuperuser(false)
.setReason("reason")
.setRequestedByRegistrar(true)
.setDomainRepoId(domain.getRepoId())
.setOtherRegistrarId("otherClient")
.setPeriod(Period.create(1, Period.Unit.YEARS))
.build();
}
static DomainHistory createFullHistory(DomainBase domain, FakeClock fakeClock) {
return createHistoryWithoutContent(domain, fakeClock)
.asBuilder()
.setType(HistoryEntry.Type.DOMAIN_TRANSFER_APPROVE)
.setDomain(domain)
.setDomainTransactionRecords(ImmutableSet.of(createDomainTransactionRecord(fakeClock)))
.build();
}
static <T> T putInDb(T entity) {
jpaTm().transact(() -> jpaTm().put(entity));
return jpaTm().transact(() -> jpaTm().loadByEntity(entity));
}
}
@@ -696,7 +696,7 @@ public class DomainBaseTest extends EntityTestCase {
IllegalArgumentException.class, () -> domain.asBuilder().setDomainName("AAA.BBB"));
assertThat(thrown)
.hasMessageThat()
.contains("Domain name must be in puny-coded, lower-case form");
.isEqualTo("Domain name AAA.BBB not in puny-coded, lower-case form");
}
@Test
@@ -706,7 +706,7 @@ public class DomainBaseTest extends EntityTestCase {
IllegalArgumentException.class, () -> domain.asBuilder().setDomainName("みんな.みんな"));
assertThat(thrown)
.hasMessageThat()
.contains("Domain name must be in puny-coded, lower-case form");
.isEqualTo("Domain name みんな.みんな not in puny-coded, lower-case form");
}
@Test
@@ -24,11 +24,15 @@ import static google.registry.testing.DatabaseHelper.newContactResource;
import static google.registry.testing.DatabaseHelper.newContactResourceWithRoid;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.collect.ImmutableList;
import com.googlecode.objectify.Key;
import google.registry.model.EntityTestCase;
import google.registry.model.contact.ContactAddress;
import google.registry.model.contact.ContactBase;
import google.registry.model.contact.ContactHistory;
import google.registry.model.contact.ContactPhoneNumber;
import google.registry.model.contact.ContactResource;
import google.registry.model.contact.PostalInfo;
import google.registry.model.eppcommon.Trid;
import google.registry.model.reporting.HistoryEntry;
import google.registry.persistence.VKey;
@@ -130,6 +134,60 @@ public class ContactHistoryTest extends EntityTestCase {
.hasFieldsEqualTo(jpaTm().loadByEntity(contactHistory).getContactBase().get()));
}
@TestSqlOnly
void testWipeOutPii_assertsAllPiiFieldsAreNull() {
ContactHistory originalEntity =
createContactHistory(
new ContactResource.Builder()
.setRepoId("1-FOOBAR")
.setLocalizedPostalInfo(
new PostalInfo.Builder()
.setType(PostalInfo.Type.LOCALIZED)
.setAddress(
new ContactAddress.Builder()
.setStreet(ImmutableList.of("111 8th Ave", "4th Floor"))
.setCity("New York")
.setState("NY")
.setZip("10011")
.setCountryCode("US")
.build())
.build())
.setInternationalizedPostalInfo(
new PostalInfo.Builder()
.setType(PostalInfo.Type.INTERNATIONALIZED)
.setAddress(
new ContactAddress.Builder()
.setStreet(ImmutableList.of("111 8th Ave", "4th Floor"))
.setCity("New York")
.setState("NY")
.setZip("10011")
.setCountryCode("US")
.build())
.build())
.setVoiceNumber(new ContactPhoneNumber.Builder().setPhoneNumber("867-5309").build())
.setFaxNumber(
new ContactPhoneNumber.Builder()
.setPhoneNumber("867-5309")
.setExtension("1000")
.build())
.setEmailAddress("test@example.com")
.build());
assertThat(originalEntity.getContactBase().get().getEmailAddress()).isNotNull();
assertThat(originalEntity.getContactBase().get().getLocalizedPostalInfo()).isNotNull();
assertThat(originalEntity.getContactBase().get().getInternationalizedPostalInfo()).isNotNull();
assertThat(originalEntity.getContactBase().get().getVoiceNumber()).isNotNull();
assertThat(originalEntity.getContactBase().get().getFaxNumber()).isNotNull();
ContactHistory wipedEntity = originalEntity.asBuilder().wipeOutPii().build();
assertThat(wipedEntity.getContactBase().get().getEmailAddress()).isNull();
assertThat(wipedEntity.getContactBase().get().getLocalizedPostalInfo()).isNull();
assertThat(wipedEntity.getContactBase().get().getInternationalizedPostalInfo()).isNull();
assertThat(wipedEntity.getContactBase().get().getVoiceNumber()).isNull();
assertThat(wipedEntity.getContactBase().get().getFaxNumber()).isNull();
}
private ContactHistory createContactHistory(ContactBase contact) {
return new ContactHistory.Builder()
.setType(HistoryEntry.Type.HOST_CREATE)
@@ -200,7 +200,7 @@ class HostResourceTest extends EntityTestCase {
IllegalArgumentException.class, () -> host.asBuilder().setHostName("AAA.BBB.CCC"));
assertThat(thrown)
.hasMessageThat()
.contains("Host name must be in puny-coded, lower-case form");
.isEqualTo("Host name AAA.BBB.CCC not in puny-coded, lower-case form");
}
@TestOfyAndSql
@@ -210,7 +210,7 @@ class HostResourceTest extends EntityTestCase {
IllegalArgumentException.class, () -> host.asBuilder().setHostName("みんな.みんな.みんな"));
assertThat(thrown)
.hasMessageThat()
.contains("Host name must be in puny-coded, lower-case form");
.isEqualTo("Host name みんな.みんな.みんな not in puny-coded, lower-case form");
}
@TestOfyAndSql
@@ -16,7 +16,6 @@ package google.registry.model.ofy;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.ofy.ObjectifyService.auditedOfy;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.testing.DatabaseHelper.createTld;
import com.google.appengine.api.datastore.Entity;
@@ -66,7 +65,7 @@ public class CommitLogMutationTest {
Entity rawEntity = convertToEntityInTxn(someObject);
// Needs to be in a transaction so that registry-saving-to-entity will work.
CommitLogMutation mutation =
tm().transact(() -> CommitLogMutation.create(manifestKey, someObject));
auditedOfy().transact(() -> CommitLogMutation.create(manifestKey, someObject));
assertThat(Key.create(mutation))
.isEqualTo(CommitLogMutation.createKey(manifestKey, Key.create(someObject)));
assertThat(mutation.getEntity()).isEqualTo(rawEntity);
@@ -86,6 +85,6 @@ public class CommitLogMutationTest {
}
private static Entity convertToEntityInTxn(final ImmutableObject object) {
return tm().transact(() -> auditedOfy().save().toEntity(object));
return auditedOfy().transact(() -> auditedOfy().save().toEntity(object));
}
}

Some files were not shown because too many files have changed in this diff Show More