1
0
mirror of https://github.com/google/nomulus synced 2026-05-17 05:11:50 +00:00

Compare commits

...

5 Commits

Author SHA1 Message Date
Lai Jiang
c844c8e9b1 Add the ability to parse PKCS#8 private key in PEM file (#682) 2020-07-14 11:20:00 -04:00
gbrodman
f747610533 Include the relock action in the web.xml routing file (#680) 2020-07-13 21:57:35 -04:00
Shicong Huang
e1db357fc3 Merge two reserved list entities (#616)
* Merge reserved list

* Replace INSTANCE with getInstance()

* Fix broken test

* Rebase on master

* Simplify class
2020-07-13 13:40:34 -04:00
Weimin Yu
ba1915e271 Write one PCollection to SQL (#664)
* Write one PCollection to SQL

Defined a transform that writes a PCollection of entities to SQL using
JPA. Allows configuring parallelism level and batch size.
2020-07-13 13:34:01 -04:00
Shicong Huang
58618a274e Add two folders of auto-generated Java classes to .gitignore (#679) 2020-07-13 10:09:56 -04:00
37 changed files with 790 additions and 844 deletions

4
.gitignore vendored
View File

@@ -79,6 +79,10 @@ nomulus.iml
nomulus.ipr
nomulus.iws
# Auto-generated java classes by Intellij
*/src/main/generated/
*/src/test/generated_tests/
# VScode
.vscode

View File

@@ -19,7 +19,6 @@ import static com.google.common.base.Preconditions.checkState;
import static com.google.common.base.Strings.isNullOrEmpty;
import com.google.common.base.Splitter;
import dagger.Binds;
import dagger.Component;
import dagger.Lazy;
import dagger.Module;
@@ -32,10 +31,6 @@ import google.registry.persistence.PersistenceModule;
import google.registry.persistence.PersistenceModule.JdbcJpaTm;
import google.registry.persistence.PersistenceModule.SocketFactoryJpaTm;
import google.registry.persistence.transaction.JpaTransactionManager;
import google.registry.util.Clock;
import google.registry.util.Sleeper;
import google.registry.util.SystemClock;
import google.registry.util.SystemSleeper;
import google.registry.util.UtilsModule;
import java.io.BufferedReader;
import java.io.IOException;
@@ -159,19 +154,10 @@ public class BeamJpaModule {
@Provides
@Config("beamHibernateHikariMaximumPoolSize")
static int getBeamHibernateHikariMaximumPoolSize() {
// TODO(weiminyu): make this configurable. Should be equal to number of cores.
return 4;
}
@Module
interface BindModule {
@Binds
Sleeper sleeper(SystemSleeper sleeper);
@Binds
Clock clock(SystemClock clock);
}
@Singleton
@Component(
modules = {

View File

@@ -19,24 +19,37 @@ import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static google.registry.beam.initsql.BackupPaths.getCommitLogTimestamp;
import static google.registry.beam.initsql.BackupPaths.getExportFilePatterns;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.persistence.transaction.TransactionManagerFactory.setJpaTm;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static google.registry.util.DateTimeUtils.isBeforeOrAt;
import static java.util.Comparator.comparing;
import static org.apache.beam.sdk.values.TypeDescriptors.integers;
import static org.apache.beam.sdk.values.TypeDescriptors.kvs;
import static org.apache.beam.sdk.values.TypeDescriptors.strings;
import avro.shaded.com.google.common.collect.Iterators;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Streams;
import google.registry.backup.AppEngineEnvironment;
import google.registry.backup.CommitLogImports;
import google.registry.backup.VersionedEntity;
import google.registry.model.ofy.ObjectifyService;
import google.registry.model.ofy.Ofy;
import google.registry.persistence.transaction.JpaTransactionManager;
import google.registry.tools.LevelDbLogReader;
import google.registry.util.SystemSleeper;
import java.io.Serializable;
import java.util.Collection;
import java.util.Iterator;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ThreadLocalRandom;
import java.util.function.Supplier;
import javax.persistence.OptimisticLockException;
import org.apache.beam.sdk.coders.StringUtf8Coder;
import org.apache.beam.sdk.io.Compression;
import org.apache.beam.sdk.io.FileIO;
@@ -47,6 +60,7 @@ import org.apache.beam.sdk.transforms.Create;
import org.apache.beam.sdk.transforms.DoFn;
import org.apache.beam.sdk.transforms.Flatten;
import org.apache.beam.sdk.transforms.GroupByKey;
import org.apache.beam.sdk.transforms.GroupIntoBatches;
import org.apache.beam.sdk.transforms.MapElements;
import org.apache.beam.sdk.transforms.PTransform;
import org.apache.beam.sdk.transforms.ParDo;
@@ -56,10 +70,12 @@ import org.apache.beam.sdk.values.PBegin;
import org.apache.beam.sdk.values.PCollection;
import org.apache.beam.sdk.values.PCollectionList;
import org.apache.beam.sdk.values.PCollectionTuple;
import org.apache.beam.sdk.values.PDone;
import org.apache.beam.sdk.values.TupleTag;
import org.apache.beam.sdk.values.TupleTagList;
import org.apache.beam.sdk.values.TypeDescriptor;
import org.joda.time.DateTime;
import org.joda.time.Duration;
/**
* {@link PTransform Pipeline transforms} used in pipelines that load from both Datastore export
@@ -245,6 +261,38 @@ public final class Transforms {
.iterator()));
}
/**
* Returns a {@link PTransform} that writes a {@link PCollection} of entities to a SQL database.
*
* @param transformId a unique ID for an instance of the returned transform
* @param maxWriters the max number of concurrent writes to SQL, which also determines the max
* number of connection pools created
* @param batchSize the number of entities to write in each operation
* @param jpaSupplier supplier of a {@link JpaTransactionManager}
*/
public static PTransform<PCollection<VersionedEntity>, PDone> writeToSql(
String transformId,
int maxWriters,
int batchSize,
SerializableSupplier<JpaTransactionManager> jpaSupplier) {
return new PTransform<PCollection<VersionedEntity>, PDone>() {
@Override
public PDone expand(PCollection<VersionedEntity> input) {
input
.apply(
"Shard data for " + transformId,
MapElements.into(kvs(integers(), TypeDescriptor.of(VersionedEntity.class)))
.via(ve -> KV.of(ThreadLocalRandom.current().nextInt(maxWriters), ve)))
.apply("Batch output by shard " + transformId, GroupIntoBatches.ofSize(batchSize))
.apply("Write in batch for " + transformId, ParDo.of(new SqlBatchWriter(jpaSupplier)));
return PDone.in(input.getPipeline());
}
};
}
/** Interface for serializable {@link Supplier suppliers}. */
public interface SerializableSupplier<T> extends Supplier<T>, Serializable {}
/**
* Returns a {@link PTransform} that produces a {@link PCollection} containing all elements in the
* given {@link Iterable}.
@@ -322,4 +370,104 @@ public final class Transforms {
}
}
}
/**
* Writes a batch of entities to a SQL database.
*
* <p>Note that an arbitrary number of instances of this class may be created and freed in
* arbitrary order in a single JVM. Due to the tech debt that forced us to use a static variable
* to hold the {@code JpaTransactionManager} instance, we must ensure that JpaTransactionManager
* is not changed or torn down while being used by some instance.
*/
private static class SqlBatchWriter extends DoFn<KV<Integer, Iterable<VersionedEntity>>, Void> {
private static int instanceCount = 0;
private static JpaTransactionManager originalJpa;
private final SerializableSupplier<JpaTransactionManager> jpaSupplier;
private transient Ofy ofy;
private transient SystemSleeper sleeper;
SqlBatchWriter(SerializableSupplier<JpaTransactionManager> jpaSupplier) {
this.jpaSupplier = jpaSupplier;
}
@Setup
public void setup() {
sleeper = new SystemSleeper();
ObjectifyService.initOfy();
ofy = ObjectifyService.ofy();
synchronized (SqlBatchWriter.class) {
if (instanceCount == 0) {
originalJpa = jpaTm();
setJpaTm(jpaSupplier);
}
instanceCount++;
}
}
@Teardown
public void teardown() {
synchronized (SqlBatchWriter.class) {
instanceCount--;
if (instanceCount == 0) {
jpaTm().teardown();
setJpaTm(() -> originalJpa);
}
}
}
@ProcessElement
public void processElement(@Element KV<Integer, Iterable<VersionedEntity>> kv) {
try (AppEngineEnvironment env = new AppEngineEnvironment()) {
ImmutableList<Object> ofyEntities =
Streams.stream(kv.getValue())
.map(VersionedEntity::getEntity)
.map(Optional::get)
.map(ofy::toPojo)
.collect(ImmutableList.toImmutableList());
retry(() -> jpaTm().transact(() -> jpaTm().saveNewOrUpdateAll(ofyEntities)));
}
}
// TODO(b/160632289): Enhance Retrier and use it here.
private void retry(Runnable runnable) {
int maxAttempts = 5;
int initialDelayMillis = 100;
double jitterRatio = 0.2;
for (int attempt = 0; attempt < maxAttempts; attempt++) {
try {
runnable.run();
return;
} catch (Throwable throwable) {
throwIfNotCausedBy(throwable, OptimisticLockException.class);
int sleepMillis = (1 << attempt) * initialDelayMillis;
int jitter =
ThreadLocalRandom.current().nextInt((int) (sleepMillis * jitterRatio))
- (int) (sleepMillis * jitterRatio / 2);
sleeper.sleepUninterruptibly(Duration.millis(sleepMillis + jitter));
}
}
}
/**
* Rethrows {@code throwable} if it is not (and does not have a cause of) {@code causeType};
* otherwise returns with no side effects.
*/
private void throwIfNotCausedBy(Throwable throwable, Class<? extends Throwable> causeType) {
Throwable t = throwable;
while (t != null) {
if (causeType.isInstance(t)) {
return;
}
t = t.getCause();
}
Throwables.throwIfUnchecked(t);
throw new RuntimeException(t);
}
}
}

View File

@@ -367,6 +367,12 @@
<url-pattern>/_dr/task/linkRdeHosts</url-pattern>
</servlet-mapping>
<!-- Action to automatically re-lock a domain after unlocking it -->
<servlet-mapping>
<servlet-name>backend-servlet</servlet-name>
<url-pattern>/_dr/task/relockDomain</url-pattern>
</servlet-mapping>
<!-- Security config -->
<security-constraint>
<web-resource-collection>

View File

@@ -15,6 +15,7 @@
package google.registry.model.registry.label;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.base.Strings.isNullOrEmpty;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static google.registry.model.common.EntityGroupRoot.getCrossTldKey;
@@ -30,6 +31,7 @@ import com.google.common.collect.Multiset;
import com.google.common.util.concurrent.UncheckedExecutionException;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.annotation.Id;
import com.googlecode.objectify.annotation.Ignore;
import com.googlecode.objectify.annotation.Parent;
import google.registry.model.Buildable;
import google.registry.model.ImmutableObject;
@@ -42,6 +44,11 @@ import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import javax.annotation.Nullable;
import javax.persistence.Column;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.MappedSuperclass;
import javax.persistence.Transient;
import org.joda.time.DateTime;
/**
@@ -49,25 +56,46 @@ import org.joda.time.DateTime;
*
* @param <T> The type of the root value being listed, e.g. {@link ReservationType}.
* @param <R> The type of domain label entry being listed, e.g. {@link ReservedListEntry} (note,
* must subclass {@link DomainLabelEntry}.
* must subclass {@link DomainLabelEntry}.
*/
@MappedSuperclass
public abstract class BaseDomainLabelList<T extends Comparable<?>, R extends DomainLabelEntry<T, ?>>
extends ImmutableObject implements Buildable {
@Ignore
@javax.persistence.Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Long revisionId;
@Id
@Column(nullable = false)
String name;
@Parent
Key<EntityGroupRoot> parent = getCrossTldKey();
@Parent @Transient Key<EntityGroupRoot> parent = getCrossTldKey();
DateTime creationTime;
@Transient DateTime creationTime;
// The list in Cloud SQL is immutable, we only have a creation_timestamp field and it should be
// set to the timestamp when the list is created. In Datastore, we have two fields and the
// lastUpdateTime is set to the current timestamp when creating and updating a list. So, we use
// lastUpdateTime as the creation_timestamp column during the dual-write phase for compatibility.
@Column(name = "creation_timestamp", nullable = false)
DateTime lastUpdateTime;
/** Returns the ID of this revision, or throws if null. */
public long getRevisionId() {
checkState(
revisionId != null,
"revisionId is null because this object has not been persisted to the database yet");
return revisionId;
}
/** Returns the name of the reserved list. */
public String getName() {
return name;
}
/** Returns the creation time of this revision of the reserved list. */
public DateTime getCreationTime() {
return creationTime;
}
@@ -183,6 +211,9 @@ public abstract class BaseDomainLabelList<T extends Comparable<?>, R extends Dom
@Override
public T build() {
checkArgument(!isNullOrEmpty(getInstance().name), "List must have a name");
// The list is immutable in Cloud SQL, so make sure the revision id is not set when the
// builder object is created from a list object
getInstance().revisionId = null;
return super.build();
}
}

View File

@@ -23,16 +23,20 @@ import com.google.common.net.InternetDomainName;
import com.googlecode.objectify.annotation.Id;
import google.registry.model.Buildable.GenericBuilder;
import google.registry.model.ImmutableObject;
import javax.persistence.Column;
import javax.persistence.MappedSuperclass;
/**
* Represents a label entry parsed from a line in a reserved/premium list txt file.
*
* @param <T> The type of the value stored for the domain label, e.g. {@link ReservationType}.
*/
@MappedSuperclass
public abstract class DomainLabelEntry<T extends Comparable<?>, D extends DomainLabelEntry<?, ?>>
extends ImmutableObject implements Comparable<D> {
@Id
@Column(name = "domain_label", insertable = false, updatable = false)
String label;
String comment;

View File

@@ -16,11 +16,8 @@ package google.registry.model.registry.label;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static google.registry.config.RegistryConfig.getDomainLabelListCacheDuration;
import static google.registry.model.common.EntityGroupRoot.getCrossTldKey;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.model.registry.label.ReservationType.FULLY_BLOCKED;
import static google.registry.util.CollectionUtils.nullToEmpty;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
@@ -30,12 +27,8 @@ import com.google.common.base.Splitter;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.MapDifference;
import com.google.common.collect.Maps;
import com.google.common.flogger.FluentLogger;
import com.google.common.util.concurrent.UncheckedExecutionException;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.annotation.Embed;
@@ -45,45 +38,58 @@ import com.googlecode.objectify.mapper.Mapper;
import google.registry.model.Buildable;
import google.registry.model.registry.Registry;
import google.registry.model.registry.label.DomainLabelMetrics.MetricsReservedListMatch;
import google.registry.schema.replay.DatastoreEntity;
import google.registry.schema.replay.SqlEntity;
import google.registry.schema.tld.ReservedList.ReservedEntry;
import google.registry.schema.tld.ReservedListDao;
import google.registry.schema.replay.DatastoreAndSqlEntity;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import javax.annotation.Nullable;
import javax.persistence.CollectionTable;
import javax.persistence.Column;
import javax.persistence.ElementCollection;
import javax.persistence.Embeddable;
import javax.persistence.Index;
import javax.persistence.JoinColumn;
import javax.persistence.MapKeyColumn;
import javax.persistence.Table;
import org.joda.time.DateTime;
/**
* A reserved list entity, persisted to Datastore, that is used to check domain label reservations.
* A list of reserved domain labels that are blocked from being registered for various reasons.
*
* <p>Note that the primary key of this entity is {@link #revisionId}, which is auto-generated by
* the database. So, if a retry of insertion happens after the previous attempt unexpectedly
* succeeds, we will end up with having two exact same reserved lists that differ only by
* revisionId. This is fine though, because we only use the list with the highest revisionId.
*/
@Entity
@javax.persistence.Entity
@Table(indexes = {@Index(columnList = "name", name = "reservedlist_name_idx")})
public final class ReservedList
extends BaseDomainLabelList<ReservationType, ReservedList.ReservedListEntry>
implements DatastoreEntity {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
implements DatastoreAndSqlEntity {
@Mapify(ReservedListEntry.LabelMapper.class)
@ElementCollection
@CollectionTable(
name = "ReservedEntry",
joinColumns = @JoinColumn(name = "revisionId", referencedColumnName = "revisionId"))
@MapKeyColumn(name = "domain_label")
Map<String, ReservedListEntry> reservedListMap;
@Column(nullable = false)
boolean shouldPublish = true;
@Override
public ImmutableList<SqlEntity> toSqlEntities() {
return ImmutableList.of(); // ReservedList is dual-written
}
/**
* A reserved list entry entity, persisted to Datastore, that represents a single label and its
* reservation type.
*/
@Embed
public static class ReservedListEntry
extends DomainLabelEntry<ReservationType, ReservedListEntry> implements Buildable {
@Embeddable
public static class ReservedListEntry extends DomainLabelEntry<ReservationType, ReservedListEntry>
implements Buildable {
@Column(nullable = false)
ReservationType reservationType;
/** Mapper for use with @Mapify */
@@ -150,6 +156,7 @@ public final class ReservedList
return shouldPublish;
}
/** Returns a {@link Map} of domain labels to {@link ReservedListEntry}. */
public ImmutableMap<String, ReservedListEntry> getReservedListEntries() {
return ImmutableMap.copyOf(nullToEmpty(reservedListMap));
}
@@ -239,65 +246,10 @@ public final class ReservedList
new CacheLoader<String, ReservedList>() {
@Override
public ReservedList load(String listName) {
ReservedList datastoreList =
ofy()
.load()
.type(ReservedList.class)
.parent(getCrossTldKey())
.id(listName)
.now();
// Also load the list from Cloud SQL, compare the two lists, and log if different.
try {
loadAndCompareCloudSqlList(datastoreList);
} catch (Throwable t) {
logger.atSevere().withCause(t).log("Error comparing reserved lists.");
}
return datastoreList;
return ReservedListDualWriteDao.getLatestRevision(listName).orElse(null);
}
});
private static final void loadAndCompareCloudSqlList(ReservedList datastoreList) {
Optional<google.registry.schema.tld.ReservedList> maybeCloudSqlList =
ReservedListDao.getLatestRevision(datastoreList.getName());
if (maybeCloudSqlList.isPresent()) {
Map<String, ReservedEntry> datastoreLabelsToReservations =
datastoreList.reservedListMap.entrySet().parallelStream()
.collect(
toImmutableMap(
Map.Entry::getKey,
entry ->
ReservedEntry.create(
entry.getValue().reservationType, entry.getValue().comment)));
google.registry.schema.tld.ReservedList cloudSqlList = maybeCloudSqlList.get();
MapDifference<String, ReservedEntry> diff =
Maps.difference(datastoreLabelsToReservations, cloudSqlList.getLabelsToReservations());
if (!diff.areEqual()) {
if (diff.entriesDiffering().size() > 10) {
logger.atWarning().log(
String.format(
"Unequal reserved lists detected, Cloud SQL list with revision"
+ " id %d has %d different records than the current"
+ " Datastore list.",
cloudSqlList.getRevisionId(), diff.entriesDiffering().size()));
} else {
StringBuilder diffMessage = new StringBuilder("Unequal reserved lists detected:\n");
diff.entriesDiffering()
.forEach(
(label, valueDiff) ->
diffMessage.append(
String.format(
"Domain label %s has entry %s in Datastore and entry"
+ " %s in Cloud SQL.\n",
label, valueDiff.leftValue(), valueDiff.rightValue())));
logger.atWarning().log(diffMessage.toString());
}
}
} else {
logger.atWarning().log("Reserved list in Cloud SQL is empty.");
}
}
/**
* Gets the {@link ReservationType} of a label in a single ReservedList, or returns an absent
* Optional if none exists in the list.

View File

@@ -0,0 +1,124 @@
// Copyright 2020 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.model.registry.label;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static google.registry.model.common.EntityGroupRoot.getCrossTldKey;
import static google.registry.persistence.transaction.TransactionManagerFactory.ofyTm;
import com.google.common.collect.MapDifference;
import com.google.common.collect.MapDifference.ValueDifference;
import com.google.common.collect.Maps;
import com.google.common.flogger.FluentLogger;
import com.googlecode.objectify.Key;
import google.registry.model.registry.label.ReservedList.ReservedListEntry;
import google.registry.persistence.VKey;
import java.util.Map;
import java.util.Optional;
/**
* A {@link ReservedList} DAO that does dual-write and dual-read against Datastore and Cloud SQL. It
* still uses Datastore as the primary storage and suppresses any exception thrown by Cloud SQL.
*
* <p>TODO(b/160993806): Delete this DAO and switch to use the SQL only DAO after migrating to Cloud
* SQL.
*/
public class ReservedListDualWriteDao {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private ReservedListDualWriteDao() {}
/** Persist a new reserved list to Cloud SQL. */
public static void save(ReservedList reservedList) {
ofyTm().transact(() -> ofyTm().saveNewOrUpdate(reservedList));
try {
logger.atInfo().log("Saving reserved list %s to Cloud SQL", reservedList.getName());
ReservedListSqlDao.save(reservedList);
logger.atInfo().log(
"Saved reserved list %s with %d entries to Cloud SQL",
reservedList.getName(), reservedList.getReservedListEntries().size());
} catch (Throwable t) {
logger.atSevere().withCause(t).log("Error saving the reserved list to Cloud SQL.");
}
}
/**
* Returns the most recent revision of the {@link ReservedList} with the specified name, if it
* exists.
*/
public static Optional<ReservedList> getLatestRevision(String reservedListName) {
Optional<ReservedList> maybeDatastoreList =
ofyTm()
.maybeLoad(
VKey.createOfy(
ReservedList.class,
Key.create(getCrossTldKey(), ReservedList.class, reservedListName)));
try {
// Also load the list from Cloud SQL, compare the two lists, and log if different.
maybeDatastoreList.ifPresent(ReservedListDualWriteDao::loadAndCompareCloudSqlList);
} catch (Throwable t) {
logger.atSevere().withCause(t).log("Error comparing reserved lists.");
}
return maybeDatastoreList;
}
private static void loadAndCompareCloudSqlList(ReservedList datastoreList) {
Optional<ReservedList> maybeCloudSqlList =
ReservedListSqlDao.getLatestRevision(datastoreList.getName());
if (maybeCloudSqlList.isPresent()) {
Map<String, ReservedListEntry> datastoreLabelsToReservations =
datastoreList.reservedListMap.entrySet().parallelStream()
.collect(
toImmutableMap(
Map.Entry::getKey,
entry ->
ReservedListEntry.create(
entry.getKey(),
entry.getValue().reservationType,
entry.getValue().comment)));
ReservedList cloudSqlList = maybeCloudSqlList.get();
MapDifference<String, ReservedListEntry> diff =
Maps.difference(datastoreLabelsToReservations, cloudSqlList.reservedListMap);
if (!diff.areEqual()) {
if (diff.entriesDiffering().size() > 10) {
logger.atWarning().log(
String.format(
"Unequal reserved lists detected, Cloud SQL list with revision"
+ " id %d has %d different records than the current"
+ " Datastore list.",
cloudSqlList.getRevisionId(), diff.entriesDiffering().size()));
} else {
StringBuilder diffMessage = new StringBuilder("Unequal reserved lists detected:\n");
diff.entriesDiffering().entrySet().stream()
.forEach(
entry -> {
String label = entry.getKey();
ValueDifference<ReservedListEntry> valueDiff = entry.getValue();
diffMessage.append(
String.format(
"Domain label %s has entry %s in Datastore and entry"
+ " %s in Cloud SQL.\n",
label, valueDiff.leftValue(), valueDiff.rightValue()));
});
logger.atWarning().log(diffMessage.toString());
}
}
} else {
logger.atWarning().log("Reserved list in Cloud SQL is empty.");
}
}
}

View File

@@ -12,20 +12,46 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.schema.tld;
package google.registry.model.registry.label;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import com.google.common.util.concurrent.UncheckedExecutionException;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
/** Data access object class for {@link ReservedList} */
public class ReservedListDao {
/**
* A {@link ReservedList} DAO for Cloud SQL.
*
* <p>TODO(b/160993806): Rename this class to ReservedListDao after migrating to Cloud SQL.
*/
public class ReservedListSqlDao {
private ReservedListSqlDao() {}
/** Persist a new reserved list to Cloud SQL. */
public static void save(ReservedList reservedList) {
jpaTm().transact(() -> jpaTm().getEntityManager().persist(reservedList));
checkArgumentNotNull(reservedList, "Must specify reservedList");
jpaTm().transact(() -> jpaTm().saveNew(reservedList));
}
/**
* Returns the most recent revision of the {@link ReservedList} with the specified name, if it
* exists.
*/
public static Optional<ReservedList> getLatestRevision(String reservedListName) {
return jpaTm()
.transact(
() ->
jpaTm()
.getEntityManager()
.createQuery(
"FROM ReservedList rl LEFT JOIN FETCH rl.reservedListMap WHERE"
+ " rl.revisionId IN (SELECT MAX(revisionId) FROM ReservedList subrl"
+ " WHERE subrl.name = :name)",
ReservedList.class)
.setParameter("name", reservedListName)
.getResultStream()
.findFirst());
}
/**
@@ -46,39 +72,4 @@ public class ReservedListDao {
.size()
> 0);
}
/**
* Returns the most recent revision of the {@link ReservedList} with the specified name, if it
* exists. TODO(shicong): Change this method to package level access after dual-read phase.
*/
public static Optional<ReservedList> getLatestRevision(String reservedListName) {
return jpaTm()
.transact(
() ->
jpaTm()
.getEntityManager()
.createQuery(
"FROM ReservedList rl LEFT JOIN FETCH rl.labelsToReservations WHERE"
+ " rl.revisionId IN (SELECT MAX(revisionId) FROM ReservedList subrl"
+ " WHERE subrl.name = :name)",
ReservedList.class)
.setParameter("name", reservedListName)
.getResultStream()
.findFirst());
}
/**
* Returns the most recent revision of the {@link ReservedList} with the specified name, from
* cache.
*/
public static Optional<ReservedList> getLatestRevisionCached(String reservedListName) {
try {
return ReservedListCache.cacheReservedLists.get(reservedListName);
} catch (ExecutionException e) {
throw new UncheckedExecutionException(
"Could not retrieve reserved list named " + reservedListName, e);
}
}
private ReservedListDao() {}
}

View File

@@ -58,6 +58,31 @@ public class VKey<T> extends ImmutableObject implements Serializable {
return new VKey(kind, null, sqlKey);
}
/** Creates a {@link VKey} which only contains the ofy primary key. */
public static <T> VKey<T> createOfy(
Class<? extends T> kind, com.googlecode.objectify.Key<? extends T> ofyKey) {
checkArgumentNotNull(kind, "kind must not be null");
checkArgumentNotNull(ofyKey, "ofyKey must not be null");
return new VKey(kind, ofyKey, null);
}
/**
* Creates a {@link VKey} which only contains the ofy primary key by specifying the id of the
* {@link Key}.
*/
public static <T> VKey<T> createOfy(Class<? extends T> kind, long id) {
return createOfy(kind, Key.create(kind, id));
}
/**
* Creates a {@link VKey} which only contains the ofy primary key by specifying the name of the
* {@link Key}.
*/
public static <T> VKey<T> createOfy(Class<? extends T> kind, String name) {
checkArgumentNotNull(kind, "name must not be null");
return createOfy(kind, Key.create(kind, name));
}
/** Creates a {@link VKey} which only contains both sql and ofy primary key. */
public static <T> VKey<T> create(
Class<? extends T> kind, Object sqlKey, com.googlecode.objectify.Key ofyKey) {

View File

@@ -25,4 +25,11 @@ public interface JpaTransactionManager extends TransactionManager {
/** Deletes the entity by its id, throws exception if the entity is not deleted. */
public abstract <T> void assertDelete(VKey<T> key);
/**
* Releases all resources and shuts down.
*
* <p>The errorprone check forbids injection of {@link java.io.Closeable} resources.
*/
void teardown();
}

View File

@@ -62,6 +62,11 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
this.clock = clock;
}
@Override
public void teardown() {
emf.close();
}
@Override
public EntityManager getEntityManager() {
if (transactionInfo.get().entityManager == null) {

View File

@@ -75,7 +75,12 @@ public class TransactionManagerFactory {
return tm;
}
/** Returns {@link JpaTransactionManager} instance. */
/**
* Returns {@link JpaTransactionManager} instance.
*
* <p>Between invocations of {@link TransactionManagerFactory#setJpaTm} every call to this method
* returns the same instance.
*/
public static JpaTransactionManager jpaTm() {
return jpaTm.get();
}
@@ -93,7 +98,7 @@ public class TransactionManagerFactory {
RegistryEnvironment.get().equals(RegistryEnvironment.UNITTEST)
|| RegistryToolEnvironment.get() != null,
"setJpamTm() should only be called by tools and tests.");
jpaTm = jpaTmSupplier;
jpaTm = Suppliers.memoize(jpaTmSupplier::get);
}
/** Sets the return of {@link #tm()} to the given instance of {@link TransactionManager}. */

View File

@@ -1,177 +0,0 @@
// Copyright 2019 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.schema.tld;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.ImmutableList.sortedCopyOf;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static google.registry.util.DomainNameUtils.canonicalizeDomainName;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import google.registry.model.CreateAutoTimestamp;
import google.registry.model.ImmutableObject;
import google.registry.model.registry.label.ReservationType;
import google.registry.schema.replay.DatastoreEntity;
import google.registry.schema.replay.SqlEntity;
import java.util.Map;
import java.util.stream.Stream;
import javax.annotation.Nullable;
import javax.persistence.CollectionTable;
import javax.persistence.Column;
import javax.persistence.ElementCollection;
import javax.persistence.Embeddable;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Index;
import javax.persistence.JoinColumn;
import javax.persistence.MapKeyColumn;
import javax.persistence.Table;
import org.joda.time.DateTime;
/**
* A list of reserved domain labels that are blocked from being registered for various reasons.
*
* <p>Note that the primary key of this entity is {@link #revisionId}, which is auto-generated by
* the database. So, if a retry of insertion happens after the previous attempt unexpectedly
* succeeds, we will end up with having two exact same reserved lists that differ only by
* revisionId. This is fine though, because we only use the list with the highest revisionId.
*/
@Entity
@Table(indexes = {@Index(columnList = "name", name = "reservedlist_name_idx")})
public class ReservedList extends ImmutableObject implements SqlEntity {
@Column(nullable = false)
private String name;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(nullable = false)
private Long revisionId;
@Column(nullable = false)
private CreateAutoTimestamp creationTimestamp = CreateAutoTimestamp.create(null);
@Column(nullable = false)
private Boolean shouldPublish;
@ElementCollection
@CollectionTable(
name = "ReservedEntry",
joinColumns = @JoinColumn(name = "revisionId", referencedColumnName = "revisionId"))
@MapKeyColumn(name = "domainLabel")
private Map<String, ReservedEntry> labelsToReservations;
@Override
public ImmutableList<DatastoreEntity> toDatastoreEntities() {
return ImmutableList.of(); // ReservedList is dual-written\
}
@Embeddable
public static class ReservedEntry extends ImmutableObject {
@Column(nullable = false)
private ReservationType reservationType;
@Column(nullable = true)
private String comment;
private ReservedEntry(ReservationType reservationType, @Nullable String comment) {
this.reservationType = reservationType;
this.comment = comment;
}
// Hibernate requires this default constructor.
private ReservedEntry() {}
/** Constructs a {@link ReservedEntry} object. */
public static ReservedEntry create(ReservationType reservationType, @Nullable String comment) {
return new ReservedEntry(reservationType, comment);
}
/** Returns the reservation type for this entry. */
public ReservationType getReservationType() {
return reservationType;
}
/** Returns the comment for this entry. Retruns null if there is no comment. */
public String getComment() {
return comment;
}
}
private ReservedList(
String name, Boolean shouldPublish, Map<String, ReservedEntry> labelsToReservations) {
this.name = name;
this.shouldPublish = shouldPublish;
this.labelsToReservations = labelsToReservations;
}
// Hibernate requires this default constructor.
private ReservedList() {}
/** Constructs a {@link ReservedList} object. */
public static ReservedList create(
String name, Boolean shouldPublish, Map<String, ReservedEntry> labelsToReservations) {
ImmutableList<String> invalidLabels =
labelsToReservations.entrySet().parallelStream()
.flatMap(
entry -> {
String label = entry.getKey();
if (label.equals(canonicalizeDomainName(label))) {
return Stream.empty();
} else {
return Stream.of(label);
}
})
.collect(toImmutableList());
checkArgument(
invalidLabels.isEmpty(),
"Label(s) [%s] must be in puny-coded, lower-case form",
Joiner.on(",").join(sortedCopyOf(invalidLabels)));
return new ReservedList(name, shouldPublish, labelsToReservations);
}
/** Returns the name of the reserved list. */
public String getName() {
return name;
}
/** Returns the ID of this revision, or throws if null. */
public Long getRevisionId() {
checkState(
revisionId != null,
"revisionId is null because this object has not been persisted to the database yet");
return revisionId;
}
/** Returns the creation time of this revision of the reserved list. */
public DateTime getCreationTimestamp() {
return creationTimestamp.getTimestamp();
}
/** Returns a {@link Map} of domain labels to {@link ReservedEntry}. */
public ImmutableMap<String, ReservedEntry> getLabelsToReservations() {
return ImmutableMap.copyOf(labelsToReservations);
}
/** Returns true if the reserved list should be published. */
public Boolean getShouldPublish() {
return shouldPublish;
}
}

View File

@@ -1,54 +0,0 @@
// Copyright 2019 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.schema.tld;
import static google.registry.config.RegistryConfig.getDomainLabelListCacheDuration;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import google.registry.util.NonFinalForTesting;
import java.util.Optional;
import org.joda.time.Duration;
/** Caching utils for {@link ReservedList} */
public class ReservedListCache {
/**
* In-memory cache for reserved lists.
*
* <p>This is cached for a shorter duration because we need to periodically reload from the DB to
* check if a new revision has been published, and if so, then use that.
*/
@NonFinalForTesting
static LoadingCache<String, Optional<ReservedList>> cacheReservedLists =
createCacheReservedLists(getDomainLabelListCacheDuration());
@VisibleForTesting
static LoadingCache<String, Optional<ReservedList>> createCacheReservedLists(
Duration cachePersistDuration) {
return CacheBuilder.newBuilder()
.expireAfterWrite(cachePersistDuration.getMillis(), MILLISECONDS)
.build(
new CacheLoader<String, Optional<ReservedList>>() {
@Override
public Optional<ReservedList> load(String reservedListName) {
return ReservedListDao.getLatestRevision(reservedListName);
}
});
}
}

View File

@@ -14,23 +14,12 @@
package google.registry.tools;
import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.model.registry.label.BaseDomainLabelList.splitOnComment;
import static google.registry.util.DomainNameUtils.canonicalizeDomainName;
import com.beust.jcommander.Parameter;
import com.google.common.base.Splitter;
import com.google.common.collect.HashMultiset;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.google.common.collect.Multiset;
import com.google.common.flogger.FluentLogger;
import google.registry.model.registry.label.ReservationType;
import google.registry.schema.tld.ReservedList.ReservedEntry;
import google.registry.model.registry.label.ReservedList;
import google.registry.model.registry.label.ReservedListDualWriteDao;
import google.registry.tools.params.PathParameter;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
/**
@@ -62,69 +51,22 @@ public abstract class CreateOrUpdateReservedListCommand extends MutatingCommand
arity = 1)
Boolean shouldPublish;
google.registry.schema.tld.ReservedList cloudSqlReservedList;
abstract void saveToCloudSql();
ReservedList reservedList;
@Override
protected String execute() throws Exception {
// Save the list to Datastore and output its response.
String output = super.execute();
logger.atInfo().log(output);
String cloudSqlMessage =
protected String execute() {
String message =
String.format(
"Saved reserved list %s with %d entries",
name, cloudSqlReservedList.getLabelsToReservations().size());
name, reservedList.getReservedListEntries().size());
try {
logger.atInfo().log("Saving reserved list to Cloud SQL for TLD %s", name);
saveToCloudSql();
logger.atInfo().log(cloudSqlMessage);
logger.atInfo().log("Saving reserved list for TLD %s", name);
ReservedListDualWriteDao.save(reservedList);
logger.atInfo().log(message);
} catch (Throwable e) {
cloudSqlMessage =
"Unexpected error saving reserved list to Cloud SQL from nomulus tool command";
logger.atSevere().withCause(e).log(cloudSqlMessage);
message = "Unexpected error saving reserved list from nomulus tool command";
logger.atSevere().withCause(e).log(message);
}
return cloudSqlMessage;
}
/** Turns the list CSV data into a map of labels to {@link ReservedEntry}. */
static ImmutableMap<String, ReservedEntry> parseToReservationsByLabels(Iterable<String> lines) {
Map<String, ReservedEntry> labelsToEntries = Maps.newHashMap();
Multiset<String> duplicateLabels = HashMultiset.create();
for (String originalLine : lines) {
List<String> lineAndComment = splitOnComment(originalLine);
if (lineAndComment.isEmpty()) {
continue;
}
String line = lineAndComment.get(0);
String comment = lineAndComment.get(1);
List<String> parts = Splitter.on(',').trimResults().splitToList(line);
checkArgument(
parts.size() == 2 || parts.size() == 3,
"Could not parse line in reserved list: %s",
originalLine);
String label = parts.get(0);
checkArgument(
label.equals(canonicalizeDomainName(label)),
"Label '%s' must be in puny-coded, lower-case form",
label);
ReservationType reservationType = ReservationType.valueOf(parts.get(1));
ReservedEntry reservedEntry = ReservedEntry.create(reservationType, comment);
// Check if the label was already processed for this list (which is an error), and if so,
// accumulate it so that a list of all duplicates can be thrown.
if (labelsToEntries.containsKey(label)) {
duplicateLabels.add(label, duplicateLabels.contains(label) ? 1 : 2);
} else {
labelsToEntries.put(label, reservedEntry);
}
}
if (!duplicateLabels.isEmpty()) {
throw new IllegalStateException(
String.format(
"Reserved list cannot contain duplicate labels. Dupes (with counts) were: %s",
duplicateLabels));
}
return ImmutableMap.copyOf(labelsToEntries);
return message;
}
}

View File

@@ -16,7 +16,6 @@ package google.registry.tools;
import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.model.registry.Registries.assertTldExists;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.util.ListNamingUtils.convertFilePathToName;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.joda.time.DateTimeZone.UTC;
@@ -27,7 +26,6 @@ import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import google.registry.model.registry.label.ReservedList;
import google.registry.schema.tld.ReservedListDao;
import java.nio.file.Files;
import java.util.List;
import org.joda.time.DateTime;
@@ -50,15 +48,14 @@ final class CreateReservedListCommand extends CreateOrUpdateReservedListCommand
protected void init() throws Exception {
name = Strings.isNullOrEmpty(name) ? convertFilePathToName(input) : name;
checkArgument(
!ReservedList.get(name).isPresent(),
"A reserved list already exists by this name");
!ReservedList.get(name).isPresent(), "A reserved list already exists by this name");
if (!override) {
validateListName(name);
}
DateTime now = DateTime.now(UTC);
List<String> allLines = Files.readAllLines(input, UTF_8);
boolean shouldPublish = this.shouldPublish == null || this.shouldPublish;
ReservedList reservedList =
reservedList =
new ReservedList.Builder()
.setName(name)
.setReservedListMapFromLines(allLines)
@@ -66,23 +63,6 @@ final class CreateReservedListCommand extends CreateOrUpdateReservedListCommand
.setCreationTime(now)
.setLastUpdateTime(now)
.build();
stageEntityChange(null, reservedList);
cloudSqlReservedList =
google.registry.schema.tld.ReservedList.create(
name, shouldPublish, parseToReservationsByLabels(allLines));
}
@Override
void saveToCloudSql() {
jpaTm()
.transact(
() -> {
checkArgument(
!ReservedListDao.checkExists(cloudSqlReservedList.getName()),
"A reserved list of this name already exists: %s.",
cloudSqlReservedList.getName());
ReservedListDao.save(cloudSqlReservedList);
});
}
private static void validateListName(String name) {

View File

@@ -15,18 +15,17 @@
package google.registry.tools;
import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.util.ListNamingUtils.convertFilePathToName;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.beust.jcommander.Parameters;
import com.google.common.base.Strings;
import google.registry.model.registry.label.ReservedList;
import google.registry.schema.tld.ReservedListDao;
import google.registry.util.SystemClock;
import java.nio.file.Files;
import java.util.List;
import java.util.Optional;
import org.joda.time.DateTime;
/** Command to safely update {@link ReservedList} on Datastore. */
@Parameters(separators = " =", commandDescription = "Update a ReservedList in Datastore.")
@@ -35,42 +34,20 @@ final class UpdateReservedListCommand extends CreateOrUpdateReservedListCommand
@Override
protected void init() throws Exception {
name = Strings.isNullOrEmpty(name) ? convertFilePathToName(input) : name;
// TODO(shicong): Read existing entry from Cloud SQL
Optional<ReservedList> existing = ReservedList.get(name);
checkArgument(
existing.isPresent(), "Could not update reserved list %s because it doesn't exist.", name);
boolean shouldPublish =
this.shouldPublish == null ? existing.get().getShouldPublish() : this.shouldPublish;
List<String> allLines = Files.readAllLines(input, UTF_8);
DateTime now = new SystemClock().nowUtc();
ReservedList.Builder updated =
existing
.get()
.asBuilder()
.setReservedListMapFromLines(allLines)
.setLastUpdateTime(new SystemClock().nowUtc())
.setLastUpdateTime(now)
.setShouldPublish(shouldPublish);
stageEntityChange(existing.get(), updated.build());
cloudSqlReservedList =
google.registry.schema.tld.ReservedList.create(
name, shouldPublish, parseToReservationsByLabels(allLines));
}
@Override
void saveToCloudSql() {
jpaTm()
.transact(
() -> {
// This check is currently disabled because, during the Cloud SQL migration, we need
// to be able to update reserved lists in Datastore while simultaneously creating
// their first revision in Cloud SQL (i.e. if they haven't been migrated over yet).
// TODO(shicong): Re-instate this once all reserved lists are migrated to Cloud SQL,
// and add a unit test to verity that an exception will be thrown if
// the reserved list doesn't exist.
// checkArgument(
// ReservedListDao.checkExists(cloudSqlReservedList.getName()),
// "A reserved list of this name doesn't exist: %s.",
// cloudSqlReservedList.getName());
ReservedListDao.save(cloudSqlReservedList);
});
reservedList = updated.build();
}
}

View File

@@ -36,12 +36,12 @@
<class>google.registry.schema.server.Lock</class>
<class>google.registry.schema.tld.PremiumList</class>
<class>google.registry.schema.tld.PremiumEntry</class>
<class>google.registry.schema.tld.ReservedList</class>
<class>google.registry.model.domain.secdns.DelegationSignerData</class>
<class>google.registry.model.domain.GracePeriod</class>
<class>google.registry.model.poll.PollMessage</class>
<class>google.registry.model.poll.PollMessage$OneTime</class>
<class>google.registry.model.poll.PollMessage$Autorenew</class>
<class>google.registry.model.registry.label.ReservedList</class>
<!-- Customized type converters -->
<class>google.registry.persistence.converter.BillingCostTransitionConverter</class>

View File

@@ -18,6 +18,7 @@ import static com.google.common.truth.Truth.assertThat;
import google.registry.persistence.NomulusPostgreSql;
import google.registry.persistence.transaction.JpaTransactionManager;
import google.registry.testing.DatastoreEntityExtension;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
@@ -26,6 +27,7 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.junit.jupiter.api.io.TempDir;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
@@ -38,6 +40,9 @@ public class BeamJpaModuleTest {
@Container
public PostgreSQLContainer database = new PostgreSQLContainer(NomulusPostgreSql.getDockerTag());
@RegisterExtension
public DatastoreEntityExtension datastoreEntityExtension = new DatastoreEntityExtension();
@TempDir File tempFolder;
private File credentialFile;
@@ -51,7 +56,7 @@ public class BeamJpaModuleTest {
}
@Test
public void getJpaTransactionManager_local() {
void getJpaTransactionManager_local() {
JpaTransactionManager jpa =
DaggerBeamJpaModule_JpaTransactionManagerComponent.builder()
.beamJpaModule(new BeamJpaModule(credentialFile.getAbsolutePath()))

View File

@@ -173,6 +173,7 @@ public class ExportloadingTransformsTest implements Serializable {
}
@Test
@Category(NeedsRunner.class)
public void loadDataFromFiles() {
PCollection<VersionedEntity> entities =
pipeline

View File

@@ -31,6 +31,7 @@ import google.registry.model.registry.Registry;
import google.registry.testing.FakeClock;
import google.registry.testing.InjectRule;
import java.io.File;
import org.apache.beam.sdk.testing.NeedsRunner;
import org.apache.beam.sdk.testing.TestPipeline;
import org.apache.beam.sdk.values.KV;
import org.apache.beam.sdk.values.PCollectionTuple;
@@ -38,6 +39,7 @@ import org.joda.time.DateTime;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.experimental.categories.Category;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@@ -152,6 +154,7 @@ public class LoadDatastoreSnapshotTest {
}
@Test
@Category(NeedsRunner.class)
public void loadDatastoreSnapshot() {
PCollectionTuple snapshot =
pipeline.apply(

View File

@@ -0,0 +1,125 @@
// Copyright 2020 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.beam.initsql;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import com.google.appengine.api.datastore.Entity;
import com.google.common.collect.ImmutableList;
import google.registry.backup.VersionedEntity;
import google.registry.model.contact.ContactResource;
import google.registry.model.ofy.Ofy;
import google.registry.model.registrar.Registrar;
import google.registry.persistence.transaction.JpaTestRules;
import google.registry.persistence.transaction.JpaTestRules.JpaIntegrationTestRule;
import google.registry.testing.AppEngineRule;
import google.registry.testing.DatastoreHelper;
import google.registry.testing.FakeClock;
import google.registry.testing.InjectRule;
import java.io.File;
import java.io.PrintStream;
import java.io.Serializable;
import java.util.stream.Collectors;
import org.apache.beam.sdk.testing.NeedsRunner;
import org.apache.beam.sdk.testing.TestPipeline;
import org.apache.beam.sdk.transforms.Create;
import org.joda.time.DateTime;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.experimental.categories.Category;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Unit test for {@link Transforms#writeToSql}. */
@RunWith(JUnit4.class)
public class WriteToSqlTest implements Serializable {
private static final DateTime START_TIME = DateTime.parse("2000-01-01T00:00:00.0Z");
private final FakeClock fakeClock = new FakeClock(START_TIME);
@Rule public final transient InjectRule injectRule = new InjectRule();
@Rule
public transient JpaIntegrationTestRule jpaRule =
new JpaTestRules.Builder().withClock(fakeClock).buildIntegrationTestRule();
@Rule public transient TemporaryFolder temporaryFolder = new TemporaryFolder();
@Rule
public final transient TestPipeline pipeline =
TestPipeline.create().enableAbandonedNodeEnforcement(true);
private ImmutableList<Entity> contacts;
private File credentialFile;
@Before
public void beforeEach() throws Exception {
try (BackupTestStore store = new BackupTestStore(fakeClock)) {
injectRule.setStaticField(Ofy.class, "clock", fakeClock);
// Required for contacts created below.
Registrar ofyRegistrar = AppEngineRule.makeRegistrar2();
store.insertOrUpdate(ofyRegistrar);
jpaTm().transact(() -> jpaTm().saveNewOrUpdate(store.loadAsOfyEntity(ofyRegistrar)));
ImmutableList.Builder<Entity> builder = new ImmutableList.Builder<>();
for (int i = 0; i < 3; i++) {
ContactResource contact = DatastoreHelper.newContactResource("contact_" + i);
store.insertOrUpdate(contact);
builder.add(store.loadAsDatastoreEntity(contact));
}
contacts = builder.build();
}
credentialFile = temporaryFolder.newFile();
new PrintStream(credentialFile)
.printf(
"%s %s %s",
jpaRule.getDatabaseUrl(), jpaRule.getDatabaseUsername(), jpaRule.getDatabasePassword())
.close();
}
@Test
@Category(NeedsRunner.class)
public void writeToSql_twoWriters() {
pipeline
.apply(
Create.of(
contacts.stream()
.map(InitSqlTestUtils::entityToBytes)
.map(bytes -> VersionedEntity.from(0L, bytes))
.collect(Collectors.toList())))
.apply(
Transforms.writeToSql(
"ContactResource",
2,
4,
() ->
DaggerBeamJpaModule_JpaTransactionManagerComponent.builder()
.beamJpaModule(new BeamJpaModule(credentialFile.getAbsolutePath()))
.build()
.localDbJpaTransactionManager()));
pipeline.run().waitUntilFinish();
ImmutableList<?> sqlContacts = jpaTm().transact(() -> jpaTm().loadAll(ContactResource.class));
// TODO(weiminyu): compare load entities with originals. Note: lastUpdateTimes won't match by
// design. Need an elegant way to deal with this.bbq
assertThat(sqlContacts).hasSize(3);
}
}

View File

@@ -0,0 +1,124 @@
// Copyright 2019 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.registry.label;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import com.google.common.collect.ImmutableMap;
import google.registry.model.registry.label.ReservedList.ReservedListEntry;
import google.registry.persistence.transaction.JpaTestRules;
import google.registry.persistence.transaction.JpaTestRules.JpaIntegrationWithCoverageExtension;
import google.registry.testing.DatastoreEntityExtension;
import google.registry.testing.FakeClock;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
/** Unit tests for {@link ReservedListSqlDao}. */
public class ReservedListSqlDaoTest {
private final FakeClock fakeClock = new FakeClock();
@RegisterExtension
@Order(value = 1)
DatastoreEntityExtension datastoreEntityExtension = new DatastoreEntityExtension();
@RegisterExtension
JpaIntegrationWithCoverageExtension jpa =
new JpaTestRules.Builder().withClock(fakeClock).buildIntegrationWithCoverageExtension();
private ImmutableMap<String, ReservedListEntry> test_reservations;
private ReservedList test_reserved_list;
@BeforeEach
void setUp() {
test_reservations =
ImmutableMap.of(
"food",
ReservedListEntry.create("food", ReservationType.RESERVED_FOR_SPECIFIC_USE, null),
"music",
ReservedListEntry.create("music", ReservationType.FULLY_BLOCKED, "fully blocked"));
test_reserved_list =
new ReservedList.Builder()
.setName("testlist")
.setLastUpdateTime(fakeClock.nowUtc())
.setShouldPublish(false)
.setReservedListMap(test_reservations)
.build();
}
@Test
public void save_worksSuccessfully() {
ReservedListSqlDao.save(test_reserved_list);
jpaTm()
.transact(
() -> {
ReservedList persistedList =
jpaTm()
.getEntityManager()
.createQuery("FROM ReservedList WHERE name = :name", ReservedList.class)
.setParameter("name", "testlist")
.getSingleResult();
assertThat(persistedList.getReservedListEntries())
.containsExactlyEntriesIn(test_reservations);
assertThat(persistedList.getLastUpdateTime()).isEqualTo(fakeClock.nowUtc());
});
}
@Test
public void checkExists_worksSuccessfully() {
assertThat(ReservedListSqlDao.checkExists("testlist")).isFalse();
ReservedListSqlDao.save(test_reserved_list);
assertThat(ReservedListSqlDao.checkExists("testlist")).isTrue();
}
@Test
public void getLatestRevision_worksSuccessfully() {
assertThat(ReservedListSqlDao.getLatestRevision("testlist").isPresent()).isFalse();
ReservedListSqlDao.save(test_reserved_list);
ReservedList persistedList = ReservedListSqlDao.getLatestRevision("testlist").get();
assertThat(persistedList.getRevisionId()).isNotNull();
assertThat(persistedList.getLastUpdateTime()).isEqualTo(fakeClock.nowUtc());
assertThat(persistedList.getName()).isEqualTo("testlist");
assertThat(persistedList.getShouldPublish()).isFalse();
assertThat(persistedList.getReservedListEntries()).containsExactlyEntriesIn(test_reservations);
}
@Test
public void getLatestRevision_returnsLatestRevision() {
ReservedListSqlDao.save(
new ReservedList.Builder()
.setName("testlist")
.setLastUpdateTime(fakeClock.nowUtc())
.setShouldPublish(false)
.setReservedListMap(
ImmutableMap.of(
"old",
ReservedListEntry.create(
"old", ReservationType.RESERVED_FOR_SPECIFIC_USE, null)))
.build());
ReservedListSqlDao.save(test_reserved_list);
ReservedList persistedList = ReservedListSqlDao.getLatestRevision("testlist").get();
assertThat(persistedList.getRevisionId()).isNotNull();
assertThat(persistedList.getLastUpdateTime()).isEqualTo(fakeClock.nowUtc());
assertThat(persistedList.getName()).isEqualTo("testlist");
assertThat(persistedList.getShouldPublish()).isFalse();
assertThat(persistedList.getReservedListEntries()).containsExactlyEntriesIn(test_reservations);
}
}

View File

@@ -16,11 +16,13 @@ package google.registry.persistence;
import static com.google.common.truth.Truth.assertThat;
import google.registry.testing.DatastoreEntityExtension;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
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;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
@@ -33,6 +35,9 @@ public class PersistenceModuleTest {
private final PostgreSQLContainer database =
new PostgreSQLContainer(NomulusPostgreSql.getDockerTag());
@RegisterExtension
public DatastoreEntityExtension datastoreEntityExtension = new DatastoreEntityExtension();
private EntityManagerFactory emf;
@BeforeEach

View File

@@ -209,6 +209,18 @@ abstract class JpaTransactionManagerRule extends ExternalResource {
cachedTm = null;
}
public String getDatabaseUrl() {
return database.getJdbcUrl();
}
public String getDatabaseUsername() {
return database.getUsername();
}
public String getDatabasePassword() {
return database.getPassword();
}
private void resetTablesAndSequences() {
try (Connection conn = createConnection();
Statement statement = conn.createStatement()) {

View File

@@ -23,6 +23,7 @@ import google.registry.model.history.ContactHistoryTest;
import google.registry.model.history.HostHistoryTest;
import google.registry.model.poll.PollMessageTest;
import google.registry.model.registry.RegistryLockDaoTest;
import google.registry.model.registry.label.ReservedListSqlDaoTest;
import google.registry.model.reporting.Spec11ThreatMatchTest;
import google.registry.persistence.transaction.JpaEntityCoverage;
import google.registry.schema.cursor.CursorDaoTest;
@@ -31,7 +32,6 @@ import google.registry.schema.integration.SqlIntegrationTestSuite.BeforeSuiteTes
import google.registry.schema.registrar.RegistrarDaoTest;
import google.registry.schema.server.LockDaoTest;
import google.registry.schema.tld.PremiumListDaoTest;
import google.registry.schema.tld.ReservedListDaoTest;
import google.registry.schema.tmch.ClaimsListDaoTest;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
@@ -84,8 +84,8 @@ import org.junit.runner.RunWith;
PollMessageTest.class,
PremiumListDaoTest.class,
RegistrarDaoTest.class,
ReservedListSqlDaoTest.class,
RegistryLockDaoTest.class,
ReservedListDaoTest.class,
Spec11ThreatMatchTest.class,
// AfterSuiteTest must be the last entry. See class javadoc for details.
AfterSuiteTest.class

View File

@@ -20,11 +20,13 @@ import static google.registry.testing.LogsSubject.assertAboutLogs;
import com.google.common.testing.TestLogHandler;
import google.registry.persistence.transaction.JpaTestRules;
import google.registry.persistence.transaction.JpaTestRules.JpaIntegrationWithCoverageExtension;
import google.registry.testing.DatastoreEntityExtension;
import google.registry.testing.FakeClock;
import java.util.Optional;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.joda.time.Duration;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
@@ -35,6 +37,10 @@ public class LockDaoTest {
private final TestLogHandler logHandler = new TestLogHandler();
private final Logger loggerToIntercept = Logger.getLogger(LockDao.class.getCanonicalName());
@RegisterExtension
@Order(value = 1)
public DatastoreEntityExtension datastoreEntityExtension = new DatastoreEntityExtension();
@RegisterExtension
public final JpaIntegrationWithCoverageExtension jpa =
new JpaTestRules.Builder().withClock(fakeClock).buildIntegrationWithCoverageExtension();

View File

@@ -1,110 +0,0 @@
// Copyright 2019 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.schema.tld;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import com.google.common.collect.ImmutableMap;
import google.registry.model.registry.label.ReservationType;
import google.registry.persistence.transaction.JpaTestRules;
import google.registry.persistence.transaction.JpaTestRules.JpaIntegrationWithCoverageExtension;
import google.registry.schema.tld.ReservedList.ReservedEntry;
import google.registry.testing.FakeClock;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
/** Unit tests for {@link ReservedListDao}. */
public class ReservedListDaoTest {
private final FakeClock fakeClock = new FakeClock();
@RegisterExtension
public final JpaIntegrationWithCoverageExtension jpa =
new JpaTestRules.Builder().withClock(fakeClock).buildIntegrationWithCoverageExtension();
private static final ImmutableMap<String, ReservedEntry> TEST_RESERVATIONS =
ImmutableMap.of(
"food",
ReservedEntry.create(ReservationType.RESERVED_FOR_SPECIFIC_USE, null),
"music",
ReservedEntry.create(ReservationType.FULLY_BLOCKED, "fully blocked"));
@Test
public void save_worksSuccessfully() {
ReservedList reservedList = ReservedList.create("testname", false, TEST_RESERVATIONS);
ReservedListDao.save(reservedList);
jpaTm()
.transact(
() -> {
ReservedList persistedList =
jpaTm()
.getEntityManager()
.createQuery("FROM ReservedList WHERE name = :name", ReservedList.class)
.setParameter("name", "testname")
.getSingleResult();
assertThat(persistedList.getLabelsToReservations())
.containsExactlyEntriesIn(TEST_RESERVATIONS);
assertThat(persistedList.getCreationTimestamp()).isEqualTo(fakeClock.nowUtc());
});
}
@Test
public void checkExists_worksSuccessfully() {
assertThat(ReservedListDao.checkExists("testlist")).isFalse();
ReservedListDao.save(ReservedList.create("testlist", false, TEST_RESERVATIONS));
assertThat(ReservedListDao.checkExists("testlist")).isTrue();
}
@Test
public void getLatestRevision_worksSuccessfully() {
assertThat(ReservedListDao.getLatestRevision("testlist").isPresent()).isFalse();
ReservedListDao.save(ReservedList.create("testlist", false, TEST_RESERVATIONS));
ReservedList persistedList = ReservedListDao.getLatestRevision("testlist").get();
assertThat(persistedList.getRevisionId()).isNotNull();
assertThat(persistedList.getCreationTimestamp()).isEqualTo(fakeClock.nowUtc());
assertThat(persistedList.getName()).isEqualTo("testlist");
assertThat(persistedList.getShouldPublish()).isFalse();
assertThat(persistedList.getLabelsToReservations()).containsExactlyEntriesIn(TEST_RESERVATIONS);
}
@Test
public void getLatestRevision_returnsLatestRevision() {
ReservedListDao.save(
ReservedList.create(
"testlist",
false,
ImmutableMap.of(
"old", ReservedEntry.create(ReservationType.RESERVED_FOR_SPECIFIC_USE, null))));
ReservedListDao.save(ReservedList.create("testlist", false, TEST_RESERVATIONS));
ReservedList persistedList = ReservedListDao.getLatestRevision("testlist").get();
assertThat(persistedList.getRevisionId()).isNotNull();
assertThat(persistedList.getCreationTimestamp()).isEqualTo(fakeClock.nowUtc());
assertThat(persistedList.getName()).isEqualTo("testlist");
assertThat(persistedList.getShouldPublish()).isFalse();
assertThat(persistedList.getLabelsToReservations()).containsExactlyEntriesIn(TEST_RESERVATIONS);
}
@Test
public void getLatestRevisionCached_worksSuccessfully() {
ReservedListDao.save(ReservedList.create("testlist", false, TEST_RESERVATIONS));
ReservedList persistedList = ReservedListDao.getLatestRevisionCached("testlist").get();
assertThat(persistedList.getRevisionId()).isNotNull();
assertThat(persistedList.getCreationTimestamp()).isEqualTo(fakeClock.nowUtc());
assertThat(persistedList.getName()).isEqualTo("testlist");
assertThat(persistedList.getShouldPublish()).isFalse();
assertThat(persistedList.getLabelsToReservations()).containsExactlyEntriesIn(TEST_RESERVATIONS);
}
}

View File

@@ -1,85 +0,0 @@
// Copyright 2019 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.schema.tld;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.registry.label.ReservationType.FULLY_BLOCKED;
import static org.junit.Assert.assertThrows;
import com.google.common.collect.ImmutableMap;
import google.registry.model.registry.label.ReservationType;
import google.registry.schema.tld.ReservedList.ReservedEntry;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Unit tests for {@link ReservedList} */
@RunWith(JUnit4.class)
public class ReservedListTest {
@Test
public void verifyConstructorAndGetters_workCorrectly() {
ReservedList reservedList =
ReservedList.create(
"app",
false,
ImmutableMap.of(
"book",
ReservedEntry.create(ReservationType.ALLOWED_IN_SUNRISE, null),
"music",
ReservedEntry.create(
ReservationType.RESERVED_FOR_ANCHOR_TENANT, "reserved for anchor tenant")));
assertThat(reservedList.getName()).isEqualTo("app");
assertThat(reservedList.getShouldPublish()).isFalse();
assertThat(reservedList.getLabelsToReservations())
.containsExactly(
"book",
ReservedEntry.create(ReservationType.ALLOWED_IN_SUNRISE, null),
"music",
ReservedEntry.create(
ReservationType.RESERVED_FOR_ANCHOR_TENANT, "reserved for anchor tenant"));
}
@Test
public void create_throwsExceptionWhenLabelIsNotLowercase() {
Exception e =
assertThrows(
IllegalArgumentException.class,
() ->
ReservedList.create(
"UPPER.tld",
true,
ImmutableMap.of("UPPER", ReservedEntry.create(FULLY_BLOCKED, ""))));
assertThat(e)
.hasMessageThat()
.contains("Label(s) [UPPER] must be in puny-coded, lower-case form");
}
@Test
public void create_labelMustBePunyCoded() {
Exception e =
assertThrows(
IllegalArgumentException.class,
() ->
ReservedList.create(
"lower.みんな",
true,
ImmutableMap.of("みんな", ReservedEntry.create(FULLY_BLOCKED, ""))));
assertThat(e)
.hasMessageThat()
.contains("Label(s) [みんな] must be in puny-coded, lower-case form");
}
}

View File

@@ -19,7 +19,9 @@ import static com.google.common.truth.Truth.assertThat;
import com.google.common.collect.ImmutableMap;
import google.registry.persistence.transaction.JpaTestRules;
import google.registry.persistence.transaction.JpaTestRules.JpaIntegrationWithCoverageExtension;
import google.registry.testing.DatastoreEntityExtension;
import google.registry.testing.FakeClock;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
@@ -32,6 +34,10 @@ public class ClaimsListDaoTest {
public final JpaIntegrationWithCoverageExtension jpa =
new JpaTestRules.Builder().withClock(fakeClock).buildIntegrationWithCoverageExtension();
@RegisterExtension
@Order(value = 1)
public final DatastoreEntityExtension datastoreEntityExtension = new DatastoreEntityExtension();
@Test
public void trySave_insertsClaimsListSuccessfully() {
ClaimsList claimsList =

View File

@@ -1,126 +0,0 @@
// Copyright 2019 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;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.registry.label.ReservationType.ALLOWED_IN_SUNRISE;
import static google.registry.model.registry.label.ReservationType.FULLY_BLOCKED;
import static google.registry.tools.CreateOrUpdateReservedListCommand.parseToReservationsByLabels;
import static org.junit.Assert.assertThrows;
import com.google.common.collect.ImmutableList;
import google.registry.model.registry.label.ReservationType;
import google.registry.schema.tld.ReservedList.ReservedEntry;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Unit tests for {@link CreateOrUpdateReservedListCommand}. */
@RunWith(JUnit4.class)
public class CreateOrUpdateReservedListCommandTest {
@Test
public void parseToReservationsByLabels_worksCorrectly() {
assertThat(
parseToReservationsByLabels(
ImmutableList.of(
"reserveddomain,FULLY_BLOCKED",
"availableinga,ALLOWED_IN_SUNRISE#allowed_in_sunrise",
"fourletterword,FULLY_BLOCKED")))
.containsExactly(
"reserveddomain",
ReservedEntry.create(ReservationType.FULLY_BLOCKED, ""),
"availableinga",
ReservedEntry.create(ALLOWED_IN_SUNRISE, "allowed_in_sunrise"),
"fourletterword",
ReservedEntry.create(FULLY_BLOCKED, ""));
}
@Test
public void parseToReservationsByLabels_throwsExceptionForInvalidLabel() {
Throwable thrown =
assertThrows(
IllegalArgumentException.class,
() ->
parseToReservationsByLabels(
ImmutableList.of("reserveddomain,FULLY_BLOCKED", "UPPER,FULLY_BLOCKED")));
assertThat(thrown)
.hasMessageThat()
.isEqualTo("Label 'UPPER' must be in puny-coded, lower-case form");
}
@Test
public void parseToReservationsByLabels_throwsExceptionForNonPunyCodedLabel() {
Throwable thrown =
assertThrows(
IllegalArgumentException.class,
() ->
parseToReservationsByLabels(
ImmutableList.of("reserveddomain,FULLY_BLOCKED", "みんな,FULLY_BLOCKED")));
assertThat(thrown)
.hasMessageThat()
.isEqualTo("Label 'みんな' must be in puny-coded, lower-case form");
}
@Test
public void parseToReservationsByLabels_throwsExceptionForInvalidReservationType() {
Throwable thrown =
assertThrows(
IllegalArgumentException.class,
() ->
parseToReservationsByLabels(
ImmutableList.of(
"reserveddomain,FULLY_BLOCKED", "invalidtype,INVALID_RESERVATION_TYPE")));
assertThat(thrown)
.hasMessageThat()
.isEqualTo(
"No enum constant"
+ " google.registry.model.registry.label.ReservationType.INVALID_RESERVATION_TYPE");
}
@Test
public void parseToReservationsByLabels_throwsExceptionForInvalidLines() {
Throwable thrown =
assertThrows(
IllegalArgumentException.class,
() ->
parseToReservationsByLabels(
ImmutableList.of(
"reserveddomain,FULLY_BLOCKED,too,many,parts",
"fourletterword,FULLY_BLOCKED")));
assertThat(thrown)
.hasMessageThat()
.isEqualTo(
"Could not parse line in reserved list: reserveddomain,FULLY_BLOCKED,too,many,parts");
}
@Test
public void parseToReservationsByLabels_throwsExceptionForDuplicateEntries() {
Throwable thrown =
assertThrows(
IllegalStateException.class,
() ->
parseToReservationsByLabels(
ImmutableList.of(
"reserveddomain,FULLY_BLOCKED",
"fourletterword,FULLY_BLOCKED",
"fourletterword,FULLY_BLOCKED")));
assertThat(thrown)
.hasMessageThat()
.isEqualTo(
"Reserved list cannot contain duplicate labels. Dupes (with counts) were:"
+ " [fourletterword x 2]");
}
}

View File

@@ -22,15 +22,16 @@ import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.Assert.assertThrows;
import com.beust.jcommander.ParameterException;
import com.google.common.collect.ImmutableMap;
import com.google.common.io.Files;
import com.google.common.truth.Truth8;
import google.registry.model.registry.label.ReservedList;
import google.registry.schema.tld.ReservedList.ReservedEntry;
import google.registry.schema.tld.ReservedListDao;
import google.registry.model.registry.label.ReservedList.ReservedListEntry;
import google.registry.model.registry.label.ReservedListSqlDao;
import java.io.File;
import java.io.IOException;
import java.util.Map;
import javax.persistence.EntityManager;
import org.joda.time.DateTime;
import org.junit.Before;
import org.junit.Test;
@@ -100,12 +101,20 @@ public abstract class CreateOrUpdateReservedListCommandTestCase<
.isEqualTo("Label example.tld must not be a multi-level domain name");
}
google.registry.schema.tld.ReservedList createCloudSqlReservedList(
String name, boolean shouldPublish, Map<String, ReservedEntry> labelsToEntries) {
return google.registry.schema.tld.ReservedList.create(name, shouldPublish, labelsToEntries);
ReservedList createCloudSqlReservedList(
String name,
DateTime creationTime,
boolean shouldPublish,
ImmutableMap<String, ReservedListEntry> labelsToEntries) {
return new ReservedList.Builder()
.setName(name)
.setLastUpdateTime(creationTime)
.setShouldPublish(shouldPublish)
.setReservedListMap(labelsToEntries)
.build();
}
google.registry.schema.tld.ReservedList getCloudSqlReservedList(String name) {
ReservedList getCloudSqlReservedList(String name) {
return jpaTm()
.transact(
() -> {
@@ -117,27 +126,25 @@ public abstract class CreateOrUpdateReservedListCommandTestCase<
.setParameter("name", name)
.getSingleResult();
return em.createQuery(
"FROM ReservedList rl LEFT JOIN FETCH rl.labelsToReservations WHERE"
"FROM ReservedList rl LEFT JOIN FETCH rl.reservedListMap WHERE"
+ " rl.revisionId = :revisionId",
google.registry.schema.tld.ReservedList.class)
ReservedList.class)
.setParameter("revisionId", revisionId)
.getSingleResult();
});
}
void verifyXnq9jyb4cInCloudSql() {
assertThat(ReservedListDao.checkExists("xn--q9jyb4c_common-reserved")).isTrue();
google.registry.schema.tld.ReservedList persistedList =
getCloudSqlReservedList("xn--q9jyb4c_common-reserved");
assertThat(ReservedListSqlDao.checkExists("xn--q9jyb4c_common-reserved")).isTrue();
ReservedList persistedList = getCloudSqlReservedList("xn--q9jyb4c_common-reserved");
assertThat(persistedList.getName()).isEqualTo("xn--q9jyb4c_common-reserved");
assertThat(persistedList.getShouldPublish()).isTrue();
assertThat(persistedList.getCreationTimestamp()).isEqualTo(fakeClock.nowUtc());
assertThat(persistedList.getLabelsToReservations())
assertThat(persistedList.getReservedListEntries())
.containsExactly(
"baddies",
ReservedEntry.create(FULLY_BLOCKED, ""),
ReservedListEntry.create("baddies", FULLY_BLOCKED, ""),
"ford",
ReservedEntry.create(FULLY_BLOCKED, "random comment"));
ReservedListEntry.create("ford", FULLY_BLOCKED, "random comment"));
}
void verifyXnq9jyb4cInDatastore() {

View File

@@ -27,8 +27,8 @@ import static org.junit.Assert.assertThrows;
import com.google.common.collect.ImmutableMap;
import google.registry.model.registry.Registry;
import google.registry.model.registry.label.ReservedList;
import google.registry.schema.tld.ReservedList.ReservedEntry;
import google.registry.schema.tld.ReservedListDao;
import google.registry.model.registry.label.ReservedList.ReservedListEntry;
import google.registry.model.registry.label.ReservedListSqlDao;
import org.joda.time.DateTime;
import org.junit.Before;
import org.junit.Test;
@@ -187,11 +187,13 @@ public class CreateReservedListCommandTest extends
public void testSaveToCloudSql_noExceptionThrownWhenSaveFail() throws Exception {
// Note that, during the dual-write phase, we want to make sure that no exception will be
// thrown if saving reserved list to Cloud SQL fails.
ReservedListDao.save(
ReservedListSqlDao.save(
createCloudSqlReservedList(
"xn--q9jyb4c_common-reserved",
fakeClock.nowUtc(),
true,
ImmutableMap.of("testdomain", ReservedEntry.create(FULLY_BLOCKED, ""))));
ImmutableMap.of(
"testdomain", ReservedListEntry.create("testdomain", FULLY_BLOCKED, ""))));
runCommandForced("--name=xn--q9jyb4c_common-reserved", "--input=" + reservedTermsPath);
verifyXnq9jyb4cInDatastore();
}

View File

@@ -24,8 +24,8 @@ import static org.junit.Assert.assertThrows;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import google.registry.model.registry.label.ReservedList;
import google.registry.schema.tld.ReservedList.ReservedEntry;
import google.registry.schema.tld.ReservedListDao;
import google.registry.model.registry.label.ReservedList.ReservedListEntry;
import google.registry.model.registry.label.ReservedListSqlDao;
import org.junit.Before;
import org.junit.Test;
@@ -50,11 +50,13 @@ public class UpdateReservedListCommandTest extends
}
private void populateInitialReservedListInCloudSql(boolean shouldPublish) {
ReservedListDao.save(
ReservedListSqlDao.save(
createCloudSqlReservedList(
"xn--q9jyb4c_common-reserved",
fakeClock.nowUtc(),
shouldPublish,
ImmutableMap.of("helicopter", ReservedEntry.create(FULLY_BLOCKED, ""))));
ImmutableMap.of(
"helicopter", ReservedListEntry.create("helicopter", FULLY_BLOCKED, ""))));
}
@Test
@@ -131,6 +133,6 @@ public class UpdateReservedListCommandTest extends
// Datastore when we update it.
runCommandForced("--name=xn--q9jyb4c_common-reserved", "--input=" + reservedTermsPath);
verifyXnq9jyb4cInDatastore();
assertThat(ReservedListDao.checkExists("xn--q9jyb4c_common-reserved")).isTrue();
assertThat(ReservedListSqlDao.checkExists("xn--q9jyb4c_common-reserved")).isTrue();
}
}

View File

@@ -462,8 +462,8 @@ create sequence history_id_sequence start 1 increment 1;
create table "ReservedEntry" (
revision_id int8 not null,
comment text,
reservation_type int4 not null,
comment text,
domain_label text not null,
primary key (revision_id, domain_label)
);

View File

@@ -40,6 +40,7 @@ import javax.inject.Named;
import javax.inject.Provider;
import javax.inject.Qualifier;
import javax.inject.Singleton;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
@@ -215,17 +216,29 @@ public final class CertificateSupplierModule {
@PemFile
static PrivateKey providePemPrivateKey(@PemFile ImmutableList<Object> pemObjects) {
JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC");
Function<PEMKeyPair, PrivateKey> privateKeyConverter =
Function<PEMKeyPair, PrivateKey> pkcs1PrivateKeyConverter =
pemKeyPair -> {
try {
return converter.getKeyPair(pemKeyPair).getPrivate();
} catch (PEMException e) {
throw new RuntimeException(
String.format("Error converting private key: %s", pemKeyPair), e);
String.format("Error converting PKCS#1 private key: %s", pemKeyPair), e);
}
};
Function<PrivateKeyInfo, PrivateKey> pkcs8PrivateKeyConverter =
privateKeyInfo -> {
try {
return converter.getPrivateKey(privateKeyInfo);
} catch (PEMException e) {
throw new RuntimeException(
String.format("Error converting PKCS#8 private key: %s", privateKeyInfo), e);
}
};
ImmutableList<PrivateKey> privateKeys =
filterAndConvert(pemObjects, PEMKeyPair.class, privateKeyConverter);
ImmutableList.<PrivateKey>builder()
.addAll(filterAndConvert(pemObjects, PEMKeyPair.class, pkcs1PrivateKeyConverter))
.addAll(filterAndConvert(pemObjects, PrivateKeyInfo.class, pkcs8PrivateKeyConverter))
.build();
checkState(
privateKeys.size() == 1,
"The pem file must contain exactly one private key, but %s keys are found",