Compare commits

...
Author SHA1 Message Date
gbrodmanandGitHub d685f7e2df Add a SQL schema and DAO for KmsSecretRevision (#840)
* Add a SQL schema and DAO for KmsSecretRevision

The dual-object nature of KmsSecret and KmsSecretRevision will not be
necessary once we have moved to SQL. In that world, the only object will
be the one now called KmsSecretRevision. KmsSecretRevision already
stores its parent so all we need to do is convert that key to the String
secretName (or from the secretName to the key, if loading from SQL) and
select the max revision ID for a given secret name.

In a future PR, we will add a dual-writing DAO to these objects and
perform the dual writes, similar to how ReservedList functions.

* Regenerate diagram

* Rename revisionId and cryptoKeyVersionName

* Fix SQL files and diagram
2020-10-30 18:45:43 -04:00
gbrodmanandGitHub 40eef2a06c Add SQL schema and DAO for SignedMarkRevocationList (#850)
* Add SQL schema and DAO for SignedMarkRevocationList

This gets saved every day so we're not concerned about history, meaning
we can dual-write and/or dual-read without concern. The structure here
is somewhat similar to the ClaimsListDao and related classes.

* Update the DB files
2020-10-30 17:52:09 -04:00
sarahcaseybotandGitHub 8bd5eb4eca Move CertificateChecker to core/ (#852)
* Move CertificateChecker to core/

* rename certificates/ to certs/
2020-10-30 15:57:12 -04:00
Weimin YuandGitHub 83918e92b5 Sync the live folder after Nomulus rollback (#854)
* Sync the live folder after Nomulus rollback

To update the nomulus tool on corp desktop, the artifacts from the
rollback target release should be copied to the 'live' folder.

* Fix a test
2020-10-29 16:21:56 -04:00
Shicong HuangandGitHub 5bba65835a Change primary key of DelegationSignerData and add its history table (#841)
* Change primary key of DelegationSignerData and add its history table

* Change primary key and resolve comments

* Rebase on HEAD
2020-10-29 16:19:15 -04:00
Shicong HuangandGitHub 1e51f51979 Use TransactionManager APIs in DatastoreHelper (#849)
* Make DatastoreHelper support Postgresql

* Rebase on HEAD

* Resolve comments

* Use put* inside insert* and update*

* Resolve comments
2020-10-29 11:41:04 -04:00
Weimin YuandGitHub db2e896d42 An automated rollback tool for Nomulus (#847)
* An automated rollback tool for Nomulus

A tool that directs traffic between deployed versions. It handles the
conversion between Nomulus tags and AppEngine versions, executes schema
compatibility tests, ensures that steps are executed in the correct order,
and updates deployment records appropriately.
2020-10-29 10:37:20 -04:00
Michael MullerandGitHub 478064f32b Add a test for ImmutableObject significant fields (#853)
This should have been introduced in #846.  Better late than never.
2020-10-28 11:11:42 -04:00
Shicong HuangandGitHub 0db535b838 Add ER diagram links to db/README.md (#851) 2020-10-28 10:13:39 -04:00
Shicong HuangandGitHub 3705f37fab Add a build task to upload ER diagrams to GCS (#844)
* Add a build task to upload ER diagrams to GCS

* Merge ER diagram task into cloudbuild-javadoc
2020-10-27 10:41:12 -04:00
Michael MullerandGitHub 86bdd154bc Restore ofy keys in GracePeriod objects (#846)
* Restore ofy keys in GracePeriod objects

Restore the ofy keys when loading GracePeriod object from SQL.  There's no
clear way to do this using the normal approach (fix-up during a PostLoad
method) because fixups to these violate immutability after hibernate has
already obtained their hash values.  Instead, we force reconstitution of the
ofy keys in all public methods that access them (including equals() and
hashCode()) so that they can be generated before an invalid hash is generated.

As part of this change, convert the GracePeriod id from an autogenerated
sequence to a UUID allocated from ObjectifyService and enhance ImmutableObject
to allow it to exclude certain fields from hash/equals and print.

The ImmutableObject enhancements are necessary because we compare grace
periods against locally created test objects in a number of unit tests and
there's no way this can work with GracePeriods loaded from SQL currently, as
they will have an identifier field generated from the database and the test
objects will have an identifier field of null (or a new unique value, after
this change).

Removing autogeneration from GracePeriod ids ended up being likely not
strictly necessary for this change (it was a consequence of an earlier
iteration).  However, it does alleviate the problem of mutation of an
immutable object after creation and is more in line with how we've decided to
allocate other identifiers.

* Changed needed after rebase.
2020-10-26 13:38:14 -04:00
88 changed files with 9006 additions and 4385 deletions
@@ -1,44 +0,0 @@
// Copyright 2020 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.config;
import com.google.common.collect.ImmutableSortedMap;
import dagger.Module;
import dagger.Provides;
import google.registry.config.RegistryConfig.Config;
import google.registry.util.CertificateChecker;
import google.registry.util.Clock;
import javax.inject.Singleton;
import org.joda.time.DateTime;
/** Dagger module that provides the {@link CertificateChecker} used in the application. */
// TODO(sarahbot@): Move this module to a better location. Possibly flows/. If we decide to move
// CertificateChecker.java to core/ delete this file and inject the CertificateChecker constructor
// instead.
@Module
public abstract class CertificateCheckerModule {
@Provides
@Singleton
static CertificateChecker provideCertificateChecker(
@Config("maxValidityDaysSchedule") ImmutableSortedMap<DateTime, Integer> validityDaysMap,
@Config("expirationWarningDays") int daysToExpiration,
@Config("minimumRsaKeyLength") int minimumRsaKeyLength,
Clock clock) {
return new CertificateChecker(validityDaysMap, daysToExpiration, minimumRsaKeyLength, clock);
}
private CertificateCheckerModule() {}
}
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.util;
package google.registry.flows.certs;
import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
@@ -20,6 +20,9 @@ import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;
import google.registry.config.RegistryConfig.Config;
import google.registry.util.Clock;
import google.registry.util.DateTimeUtils;
import java.io.ByteArrayInputStream;
import java.security.PublicKey;
import java.security.cert.CertificateException;
@@ -28,6 +31,7 @@ import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPublicKey;
import java.util.Date;
import java.util.stream.Collectors;
import javax.inject.Inject;
import org.joda.time.DateTime;
import org.joda.time.Days;
@@ -58,10 +62,12 @@ public class CertificateChecker {
* );
* </pre>
*/
@Inject
public CertificateChecker(
ImmutableSortedMap<DateTime, Integer> maxValidityLengthSchedule,
int daysToExpiration,
int minimumRsaKeyLength,
@Config("maxValidityDaysSchedule")
ImmutableSortedMap<DateTime, Integer> maxValidityLengthSchedule,
@Config("expirationWarningDays") int daysToExpiration,
@Config("minimumRsaKeyLength") int minimumRsaKeyLength,
Clock clock) {
checkArgument(
maxValidityLengthSchedule.containsKey(START_OF_TIME),
@@ -61,7 +61,17 @@ public abstract class ImmutableObject implements Cloneable {
private boolean equalsImmutableObject(ImmutableObject other) {
return getClass().equals(other.getClass())
&& hashCode() == other.hashCode()
&& ModelUtils.getFieldValues(this).equals(ModelUtils.getFieldValues(other));
&& getSignificantFields().equals(other.getSignificantFields());
}
/**
* Returns the map of significant fields (fields that we care about for purposes of comparison and
* display).
*
* <p>Isolated into a method so that derived classes can override it.
*/
protected Map<Field, Object> getSignificantFields() {
return ModelUtils.getFieldValues(this);
}
@Override
@@ -72,7 +82,7 @@ public abstract class ImmutableObject implements Cloneable {
@Override
public int hashCode() {
if (hashCode == null) {
hashCode = Arrays.hashCode(ModelUtils.getFieldValues(this).values().toArray());
hashCode = Arrays.hashCode(getSignificantFields().values().toArray());
}
return hashCode;
}
@@ -111,7 +121,7 @@ public abstract class ImmutableObject implements Cloneable {
@Override
public String toString() {
NavigableMap<String, Object> sortedFields = new TreeMap<>();
for (Entry<Field, Object> entry : ModelUtils.getFieldValues(this).entrySet()) {
for (Entry<Field, Object> entry : getSignificantFields().entrySet()) {
sortedFields.put(entry.getKey().getName(), entry.getValue());
}
return toStringHelper(sortedFields);
@@ -121,7 +131,7 @@ public abstract class ImmutableObject implements Cloneable {
public String toHydratedString() {
// We can't use ImmutableSortedMap because we need to allow null values.
NavigableMap<String, Object> sortedFields = new TreeMap<>();
for (Entry<Field, Object> entry : ModelUtils.getFieldValues(this).entrySet()) {
for (Entry<Field, Object> entry : getSignificantFields().entrySet()) {
Field field = entry.getKey();
Object value = entry.getValue();
sortedFields.put(
@@ -161,7 +171,7 @@ public abstract class ImmutableObject implements Cloneable {
// LinkedHashMap to preserve field ordering and because ImmutableMap forbids null
// values.
Map<String, Object> result = new LinkedHashMap<>();
for (Entry<Field, Object> entry : ModelUtils.getFieldValues(o).entrySet()) {
for (Entry<Field, Object> entry : ((ImmutableObject) o).getSignificantFields().entrySet()) {
Field field = entry.getKey();
if (!field.isAnnotationPresent(IgnoredInDiffableMap.class)) {
result.put(field.getName(), toMapRecursive(entry.getValue()));
@@ -194,7 +194,7 @@ public class ModelUtils {
* returned map in its implementation of {@link ImmutableObject#toString} and {@link
* ImmutableObject#equals}, which work by comparing and printing these maps.
*/
static Map<Field, Object> getFieldValues(Object instance) {
public static Map<Field, Object> getFieldValues(Object instance) {
// Don't make this ImmutableMap because field values can be null.
Map<Field, Object> values = new LinkedHashMap<>();
for (Field field : getAllFields(instance.getClass()).values()) {
@@ -60,7 +60,9 @@ public class ContactHistory extends HistoryEntry implements SqlEntity {
@Id
@Access(AccessType.PROPERTY)
public String getContactRepoId() {
return parent.getName();
// 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. */
@@ -303,14 +303,13 @@ public class DomainContent extends EppResource
allContacts.stream().map(DesignatedContact::reconstitute).collect(toImmutableSet());
setContactFields(allContacts, true);
// We have to return the cloned object here because the original object's
// hashcode is not correct due to the change to its domainRepoId. The cloned
// object will have a null hashcode so that it can get a recalculated hashcode
// when its hashCode() is invoked.
// We have to return the cloned object here because the original object's hashcode is not
// correct due to the change to its domainRepoId and history ids. The cloned object will have a
// null hashcode so that it can get a recalculated hashcode when its hashCode() is invoked.
// TODO(b/162739503): Remove this after fully migrating to Cloud SQL.
gracePeriods =
nullToEmptyImmutableCopy(gracePeriods).stream()
.map(gracePeriod -> gracePeriod.cloneWithDomainRepoId(getRepoId()))
.map(gracePeriod -> gracePeriod.cloneAfterOfyLoad(getRepoId()))
.collect(toImmutableSet());
// Restore history record ids.
@@ -14,14 +14,17 @@
package google.registry.model.domain;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static google.registry.util.CollectionUtils.nullToEmptyImmutableCopy;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.annotation.EntitySubclass;
import com.googlecode.objectify.annotation.Ignore;
import google.registry.model.ImmutableObject;
import google.registry.model.domain.DomainHistory.DomainHistoryId;
import google.registry.model.domain.secdns.DomainDsDataHistory;
import google.registry.model.host.HostResource;
import google.registry.model.reporting.DomainTransactionRecord;
import google.registry.model.reporting.HistoryEntry;
@@ -40,10 +43,12 @@ import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.ElementCollection;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.IdClass;
import javax.persistence.Index;
import javax.persistence.JoinColumn;
import javax.persistence.JoinColumns;
import javax.persistence.JoinTable;
import javax.persistence.OneToMany;
import javax.persistence.PostLoad;
@@ -75,7 +80,9 @@ public class DomainHistory extends HistoryEntry implements SqlEntity {
@Id
@Access(AccessType.PROPERTY)
public String getDomainRepoId() {
return parent.getName();
// 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. */
@@ -93,6 +100,24 @@ public class DomainHistory extends HistoryEntry implements SqlEntity {
@Column(name = "host_repo_id")
Set<VKey<HostResource>> nsHosts;
@OneToMany(
cascade = {CascadeType.ALL},
fetch = FetchType.EAGER,
orphanRemoval = true)
@JoinColumns({
@JoinColumn(
name = "domainHistoryRevisionId",
referencedColumnName = "historyRevisionId",
insertable = false,
updatable = false),
@JoinColumn(
name = "domainRepoId",
referencedColumnName = "domainRepoId",
insertable = false,
updatable = false)
})
Set<DomainDsDataHistory> dsDataHistories;
@Override
@Nullable
@Access(AccessType.PROPERTY)
@@ -127,14 +152,24 @@ public class DomainHistory extends HistoryEntry implements SqlEntity {
*
* <p>This will be empty for any DomainHistory/HistoryEntry generated before this field was added,
* mid-2017, as well as any action that does not generate billable events (e.g. updates).
*
* <p>This method is dedicated for Hibernate, external caller should use {@link
* #getDomainTransactionRecords()}.
*/
@Access(AccessType.PROPERTY)
@OneToMany(cascade = {CascadeType.ALL})
@JoinColumn(name = "historyRevisionId", referencedColumnName = "historyRevisionId")
@JoinColumn(name = "domainRepoId", referencedColumnName = "domainRepoId")
@Override
public Set<DomainTransactionRecord> getDomainTransactionRecords() {
return super.getDomainTransactionRecords();
@SuppressWarnings("unused")
private Set<DomainTransactionRecord> getInternalDomainTransactionRecords() {
return domainTransactionRecords;
}
/** Sets the domain transaction records. This method is dedicated for Hibernate. */
@SuppressWarnings("unused")
private void setInternalDomainTransactionRecords(
Set<DomainTransactionRecord> domainTransactionRecords) {
this.domainTransactionRecords = domainTransactionRecords;
}
@Id
@@ -150,6 +185,11 @@ public class DomainHistory extends HistoryEntry implements SqlEntity {
return nsHosts;
}
/** Returns the collection of {@link DomainDsDataHistory} instances. */
public ImmutableSet<DomainDsDataHistory> getDsDataHistories() {
return nullToEmptyImmutableCopy(dsDataHistories);
}
/**
* The values of all the fields on the {@link DomainContent} object after the action represented
* by this history object was executed.
@@ -266,9 +306,6 @@ public class DomainHistory extends HistoryEntry implements SqlEntity {
public Builder setDomainContent(DomainContent domainContent) {
getInstance().domainContent = domainContent;
if (domainContent != null) {
getInstance().nsHosts = nullToEmptyImmutableCopy(domainContent.nsHosts);
}
return this;
}
@@ -276,5 +313,22 @@ public class DomainHistory extends HistoryEntry implements SqlEntity {
getInstance().parent = Key.create(DomainBase.class, domainRepoId);
return this;
}
@Override
public DomainHistory build() {
DomainHistory instance = super.build();
// TODO(b/171990736): Assert instance.domainContent is not null after database migration.
// Note that we cannot assert that instance.domainContent is not null here because this
// builder is also used to convert legacy HistoryEntry objects to DomainHistory, when
// domainContent is not available.
if (instance.domainContent != null) {
instance.nsHosts = nullToEmptyImmutableCopy(instance.domainContent.nsHosts);
instance.dsDataHistories =
nullToEmptyImmutableCopy(instance.domainContent.getDsData()).stream()
.map(dsData -> DomainDsDataHistory.createFrom(instance.id, dsData))
.collect(toImmutableSet());
}
return instance;
}
}
}
@@ -21,6 +21,7 @@ import com.googlecode.objectify.annotation.Embed;
import google.registry.model.billing.BillingEvent;
import google.registry.model.billing.BillingEvent.Recurring;
import google.registry.model.domain.rgp.GracePeriodStatus;
import google.registry.model.ofy.ObjectifyService;
import google.registry.persistence.VKey;
import google.registry.schema.replay.DatastoreAndSqlEntity;
import javax.annotation.Nullable;
@@ -53,12 +54,15 @@ public class GracePeriod extends GracePeriodBase implements DatastoreAndSqlEntit
(billingEventRecurring != null) == GracePeriodStatus.AUTO_RENEW.equals(type),
"Recurring billing events must be present on (and only on) autorenew grace periods");
GracePeriod instance = new GracePeriod();
instance.id = ObjectifyService.allocateId();
instance.type = checkArgumentNotNull(type);
instance.domainRepoId = checkArgumentNotNull(domainRepoId);
instance.expirationTime = checkArgumentNotNull(expirationTime);
instance.clientId = checkArgumentNotNull(clientId);
instance.billingEventOneTime = billingEventOneTime;
instance.billingEventOneTimeHistoryId = DomainBase.getHistoryId(billingEventOneTime);
instance.billingEventRecurring = billingEventRecurring;
instance.billingEventRecurringHistoryId = DomainBase.getHistoryId(billingEventRecurring);
return instance;
}
@@ -108,14 +112,16 @@ public class GracePeriod extends GracePeriodBase implements DatastoreAndSqlEntit
}
/**
* Returns a clone of this {@link GracePeriod} with {@link #domainRepoId} set to the given value.
* Returns a clone of this {@link GracePeriod} with {@link #domainRepoId} set to the given value
* and reconstructed history ids.
*
* <p>TODO(b/162739503): Remove this function after fully migrating to Cloud SQL.
*/
public GracePeriod cloneWithDomainRepoId(String domainRepoId) {
public GracePeriod cloneAfterOfyLoad(String domainRepoId) {
GracePeriod clone = clone(this);
clone.id = ObjectifyService.allocateId();
clone.domainRepoId = checkArgumentNotNull(domainRepoId);
clone.restoreHistoryIds();
return clone;
}
}
@@ -14,18 +14,21 @@
package google.registry.model.domain;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.annotation.Embed;
import com.googlecode.objectify.annotation.Ignore;
import google.registry.model.ImmutableObject;
import google.registry.model.ModelUtils;
import google.registry.model.billing.BillingEvent;
import google.registry.model.billing.BillingEvent.OneTime;
import google.registry.model.domain.rgp.GracePeriodStatus;
import google.registry.persistence.VKey;
import java.lang.reflect.Field;
import java.util.LinkedHashMap;
import java.util.Map;
import javax.persistence.Column;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.MappedSuperclass;
import org.joda.time.DateTime;
@@ -36,7 +39,6 @@ public class GracePeriodBase extends ImmutableObject {
/** Unique id required for hibernate representation. */
@javax.persistence.Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Ignore
Long id;
@@ -67,6 +69,10 @@ public class GracePeriodBase extends ImmutableObject {
@Column(name = "billing_event_id")
VKey<OneTime> billingEventOneTime = null;
@Ignore
@Column(name = "billing_event_history_id")
Long billingEventOneTimeHistoryId;
/**
* The recurring billing event corresponding to the action that triggered this grace period, if
* applicable - i.e. if the action was an autorenew - or null in all other cases.
@@ -75,6 +81,14 @@ public class GracePeriodBase extends ImmutableObject {
@Column(name = "billing_recurrence_id")
VKey<BillingEvent.Recurring> billingEventRecurring = null;
@Ignore
@Column(name = "billing_recurrence_history_id")
Long billingEventRecurringHistoryId;
public long getId() {
return id;
}
public GracePeriodStatus getType() {
return type;
}
@@ -101,6 +115,7 @@ public class GracePeriodBase extends ImmutableObject {
* period is not AUTO_RENEW.
*/
public VKey<BillingEvent.OneTime> getOneTimeBillingEvent() {
restoreOfyKeys();
return billingEventOneTime;
}
@@ -109,6 +124,63 @@ public class GracePeriodBase extends ImmutableObject {
* period is AUTO_RENEW.
*/
public VKey<BillingEvent.Recurring> getRecurringBillingEvent() {
restoreOfyKeys();
return billingEventRecurring;
}
/**
* Restores history ids for composite VKeys after a load from datastore.
*
* <p>For use by DomainContent.load() ONLY.
*/
protected void restoreHistoryIds() {
billingEventOneTimeHistoryId = DomainBase.getHistoryId(billingEventOneTime);
billingEventRecurringHistoryId = DomainBase.getHistoryId(billingEventRecurring);
}
/**
* Override {@link ImmutableObject#getSignificantFields()} to exclude "id", which breaks equality
* testing in the unit tests.
*/
@Override
protected Map<Field, Object> getSignificantFields() {
restoreOfyKeys();
// Can't use streams or ImmutableMap because we can have null values.
Map<Field, Object> result = new LinkedHashMap();
for (Map.Entry<Field, Object> entry : ModelUtils.getFieldValues(this).entrySet()) {
if (!entry.getKey().getName().equals("id")) {
result.put(entry.getKey(), entry.getValue());
}
}
return result;
}
/**
* Restores Ofy keys in the billing events.
*
* <p>This must be called by all methods that access the one time or recurring billing event keys.
* When the billing event keys are loaded from SQL, they are loaded as asymmetric keys because the
* database columns that we load them from do not contain all of the information necessary to
* reconsitute the Ofy side of the key. In other cases, we restore the Ofy key during the
* hibernate {@link javax.persistence.PostLoad} method from the other fields of the object, but we
* have been unable to make this work with hibernate's internal persistence model in this case
* because the {@link GracePeriod}'s hash code is evaluated prior to these calls, and would be
* invalidated by changing the fields.
*/
private final synchronized void restoreOfyKeys() {
if (billingEventOneTime != null && !billingEventOneTime.maybeGetOfyKey().isPresent()) {
billingEventOneTime =
DomainBase.restoreOfyFrom(
Key.create(DomainBase.class, domainRepoId),
billingEventOneTime,
billingEventOneTimeHistoryId);
}
if (billingEventRecurring != null && !billingEventRecurring.maybeGetOfyKey().isPresent()) {
billingEventRecurring =
DomainBase.restoreOfyFrom(
Key.create(DomainBase.class, domainRepoId),
billingEventRecurring,
billingEventRecurringHistoryId);
}
}
}
@@ -17,89 +17,68 @@ package google.registry.model.domain.secdns;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import com.googlecode.objectify.annotation.Embed;
import com.googlecode.objectify.annotation.Ignore;
import google.registry.model.ImmutableObject;
import google.registry.model.domain.secdns.DelegationSignerData.DelegationSignerDataId;
import google.registry.schema.replay.DatastoreAndSqlEntity;
import google.registry.model.domain.secdns.DelegationSignerData.DomainDsDataId;
import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Access;
import javax.persistence.AccessType;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.IdClass;
import javax.persistence.Index;
import javax.persistence.Table;
import javax.xml.bind.DatatypeConverter;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlTransient;
import javax.xml.bind.annotation.XmlType;
import javax.xml.bind.annotation.adapters.HexBinaryAdapter;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
/**
* Holds the data necessary to construct a single Delegation Signer (DS) record for a domain.
*
* @see <a href="http://tools.ietf.org/html/rfc5910">RFC 5910</a>
* @see <a href="http://tools.ietf.org/html/rfc4034">RFC 4034</a>
* <p>TODO(shicong): Rename this class to DomainDsData.
*/
@Embed
@XmlType(name = "dsData")
@Entity
@IdClass(DomainDsDataId.class)
@Table(indexes = @Index(columnList = "domainRepoId"))
@IdClass(DelegationSignerDataId.class)
public class DelegationSignerData extends ImmutableObject implements DatastoreAndSqlEntity {
public class DelegationSignerData extends DomainDsDataBase {
private DelegationSignerData() {}
@Ignore @XmlTransient @javax.persistence.Id String domainRepoId;
/** The identifier for this particular key in the domain. */
@javax.persistence.Id
@Column(nullable = false)
int keyTag;
/**
* The algorithm used by this key.
*
* @see <a href="http://tools.ietf.org/html/rfc4034#appendix-A.1">RFC 4034 Appendix A.1</a>
*/
@Column(nullable = false)
@XmlElement(name = "alg")
int algorithm;
/**
* The algorithm used to generate the digest.
*
* @see <a href="http://tools.ietf.org/html/rfc4034#appendix-A.2">RFC 4034 Appendix A.2</a>
*/
@Column(nullable = false)
int digestType;
/**
* The hexBinary digest of the public key.
*
* @see <a href="http://tools.ietf.org/html/rfc4034#section-5.1.4">RFC 4034 Section 5.1.4</a>
*/
@Column(nullable = false)
@XmlJavaTypeAdapter(HexBinaryAdapter.class)
byte[] digest;
@Override
@Id
@Access(AccessType.PROPERTY)
public String getDomainRepoId() {
return super.getDomainRepoId();
}
@Override
@Id
@Access(AccessType.PROPERTY)
public int getKeyTag() {
return keyTag;
return super.getKeyTag();
}
@Override
@Id
@Access(AccessType.PROPERTY)
public int getAlgorithm() {
return algorithm;
return super.getAlgorithm();
}
@Override
@Id
@Access(AccessType.PROPERTY)
public int getDigestType() {
return digestType;
return super.getDigestType();
}
@Override
@Id
@Access(AccessType.PROPERTY)
public byte[] getDigest() {
return digest;
}
public String getDigestAsString() {
return digest == null ? "" : DatatypeConverter.printHexBinary(digest);
return super.getDigest();
}
public DelegationSignerData cloneWithDomainRepoId(String domainRepoId) {
@@ -135,30 +114,135 @@ public class DelegationSignerData extends ImmutableObject implements DatastoreAn
return create(keyTag, algorithm, digestType, DatatypeConverter.parseHexBinary(digestAsHex));
}
/**
* Returns the presentation format of this DS record.
*
* @see <a href="https://tools.ietf.org/html/rfc4034#section-5.3">RFC 4034 Section 5.3</a>
*/
public String toRrData() {
return String.format(
"%d %d %d %s",
this.keyTag, this.algorithm, this.digestType, DatatypeConverter.printHexBinary(digest));
}
/** Class to represent the composite primary key of {@link DelegationSignerData} entity. */
static class DomainDsDataId extends ImmutableObject implements Serializable {
static class DelegationSignerDataId extends ImmutableObject implements Serializable {
String domainRepoId;
int keyTag;
private DelegationSignerDataId() {}
int algorithm;
private DelegationSignerDataId(String domainRepoId, int keyTag) {
int digestType;
byte[] digest;
/** Hibernate requires this default constructor. */
private DomainDsDataId() {}
/** Constructs a {link DomainDsDataId} instance. */
DomainDsDataId(String domainRepoId, int keyTag, int algorithm, int digestType, byte[] digest) {
this.domainRepoId = domainRepoId;
this.keyTag = keyTag;
this.algorithm = algorithm;
this.digestType = digestType;
this.digest = digest;
}
/**
* Returns the domain repository ID.
*
* <p>This method is private because it is only used by Hibernate.
*/
@SuppressWarnings("unused")
private String getDomainRepoId() {
return domainRepoId;
}
/**
* Returns the key tag.
*
* <p>This method is private because it is only used by Hibernate.
*/
@SuppressWarnings("unused")
private int getKeyTag() {
return keyTag;
}
/**
* Returns the algorithm.
*
* <p>This method is private because it is only used by Hibernate.
*/
@SuppressWarnings("unused")
private int getAlgorithm() {
return algorithm;
}
/**
* Returns the digest type.
*
* <p>This method is private because it is only used by Hibernate.
*/
@SuppressWarnings("unused")
private int getDigestType() {
return digestType;
}
/**
* Returns the digest.
*
* <p>This method is private because it is only used by Hibernate.
*/
@SuppressWarnings("unused")
private byte[] getDigest() {
return digest;
}
/**
* Sets the domain repository ID.
*
* <p>This method is private because it is only used by Hibernate.
*/
@SuppressWarnings("unused")
private void setDomainRepoId(String domainRepoId) {
this.domainRepoId = domainRepoId;
}
/**
* Sets the key tag.
*
* <p>This method is private because it is only used by Hibernate.
*/
@SuppressWarnings("unused")
private void setKeyTag(int keyTag) {
this.keyTag = keyTag;
}
public static DelegationSignerDataId create(String domainRepoId, int keyTag) {
return new DelegationSignerDataId(checkArgumentNotNull(domainRepoId), keyTag);
/**
* Sets the algorithm.
*
* <p>This method is private because it is only used by Hibernate.
*/
@SuppressWarnings("unused")
private void setAlgorithm(int algorithm) {
this.algorithm = algorithm;
}
/**
* Sets the digest type.
*
* <p>This method is private because it is only used by Hibernate.
*/
@SuppressWarnings("unused")
private void setDigestType(int digestType) {
this.digestType = digestType;
}
/**
* Sets the digest.
*
* <p>This method is private because it is only used by Hibernate.
*/
@SuppressWarnings("unused")
private void setDigest(byte[] digest) {
this.digest = digest;
}
public static DomainDsDataId create(
String domainRepoId, int keyTag, int algorithm, int digestType, byte[] digest) {
return new DomainDsDataId(
domainRepoId, keyTag, algorithm, digestType, checkArgumentNotNull(digest));
}
}
}
@@ -0,0 +1,151 @@
// 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.domain.secdns;
import com.googlecode.objectify.annotation.Embed;
import com.googlecode.objectify.annotation.Ignore;
import google.registry.model.ImmutableObject;
import google.registry.schema.replay.DatastoreAndSqlEntity;
import javax.persistence.Access;
import javax.persistence.AccessType;
import javax.persistence.MappedSuperclass;
import javax.persistence.Transient;
import javax.xml.bind.DatatypeConverter;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlTransient;
import javax.xml.bind.annotation.adapters.HexBinaryAdapter;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
/** Base class for {@link DelegationSignerData} and {@link DomainDsDataHistory}. */
@Embed
@MappedSuperclass
@Access(AccessType.FIELD)
public abstract class DomainDsDataBase extends ImmutableObject implements DatastoreAndSqlEntity {
@Ignore @XmlTransient @Transient String domainRepoId;
/** The identifier for this particular key in the domain. */
@Transient int keyTag;
/**
* The algorithm used by this key.
*
* @see <a href="http://tools.ietf.org/html/rfc4034#appendix-A.1">RFC 4034 Appendix A.1</a>
*/
@Transient
@XmlElement(name = "alg")
int algorithm;
/**
* The algorithm used to generate the digest.
*
* @see <a href="http://tools.ietf.org/html/rfc4034#appendix-A.2">RFC 4034 Appendix A.2</a>
*/
@Transient int digestType;
/**
* The hexBinary digest of the public key.
*
* @see <a href="http://tools.ietf.org/html/rfc4034#section-5.1.4">RFC 4034 Section 5.1.4</a>
*/
@Transient
@XmlJavaTypeAdapter(HexBinaryAdapter.class)
byte[] digest;
public String getDomainRepoId() {
return domainRepoId;
}
public int getKeyTag() {
return keyTag;
}
public int getAlgorithm() {
return algorithm;
}
public int getDigestType() {
return digestType;
}
public byte[] getDigest() {
return digest;
}
/**
* Sets the domain repository ID.
*
* <p>This method is private because it is only used by Hibernate.
*/
@SuppressWarnings("unused")
private void setDomainRepoId(String domainRepoId) {
this.domainRepoId = domainRepoId;
}
/**
* Sets the key tag.
*
* <p>This method is private because it is only used by Hibernate.
*/
@SuppressWarnings("unused")
private void setKeyTag(int keyTag) {
this.keyTag = keyTag;
}
/**
* Sets the algorithm.
*
* <p>This method is private because it is only used by Hibernate.
*/
@SuppressWarnings("unused")
private void setAlgorithm(int algorithm) {
this.algorithm = algorithm;
}
/**
* Sets the digest type.
*
* <p>This method is private because it is only used by Hibernate.
*/
@SuppressWarnings("unused")
private void setDigestType(int digestType) {
this.digestType = digestType;
}
/**
* Sets the digest.
*
* <p>This method is private because it is only used by Hibernate.
*/
@SuppressWarnings("unused")
private void setDigest(byte[] digest) {
this.digest = digest;
}
public String getDigestAsString() {
return digest == null ? "" : DatatypeConverter.printHexBinary(digest);
}
/**
* Returns the presentation format of this DS record.
*
* @see <a href="https://tools.ietf.org/html/rfc4034#section-5.3">RFC 4034 Section 5.3</a>
*/
public String toRrData() {
return String.format(
"%d %d %d %s",
this.keyTag, this.algorithm, this.digestType, DatatypeConverter.printHexBinary(digest));
}
}
@@ -0,0 +1,84 @@
// 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.domain.secdns;
import google.registry.model.domain.DomainHistory;
import google.registry.model.ofy.ObjectifyService;
import javax.persistence.Access;
import javax.persistence.AccessType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
/** Entity class to represent a historic {@link DelegationSignerData}. */
@Entity
public class DomainDsDataHistory extends DomainDsDataBase {
@Id Long dsDataHistoryRevisionId;
/** ID of the {@link DomainHistory} entity that this entity is associated with. */
@Column(nullable = false)
Long domainHistoryRevisionId;
private DomainDsDataHistory() {}
/**
* Creates a {@link DomainDsDataHistory} instance from given {@link #domainHistoryRevisionId} and
* {@link DelegationSignerData} instance.
*/
public static DomainDsDataHistory createFrom(
long domainHistoryRevisionId, DelegationSignerData dsData) {
DomainDsDataHistory instance = new DomainDsDataHistory();
instance.domainHistoryRevisionId = domainHistoryRevisionId;
instance.domainRepoId = dsData.domainRepoId;
instance.keyTag = dsData.getKeyTag();
instance.algorithm = dsData.getAlgorithm();
instance.digestType = dsData.getDigestType();
instance.digest = dsData.getDigest();
instance.dsDataHistoryRevisionId = ObjectifyService.allocateId();
return instance;
}
@Override
@Access(AccessType.PROPERTY)
public String getDomainRepoId() {
return super.getDomainRepoId();
}
@Override
@Access(AccessType.PROPERTY)
public int getKeyTag() {
return super.getKeyTag();
}
@Override
@Access(AccessType.PROPERTY)
public int getAlgorithm() {
return super.getAlgorithm();
}
@Override
@Access(AccessType.PROPERTY)
public int getDigestType() {
return super.getDigestType();
}
@Override
@Access(AccessType.PROPERTY)
@Column(nullable = false)
public byte[] getDigest() {
return super.getDigest();
}
}
@@ -61,7 +61,9 @@ public class HostHistory extends HistoryEntry implements SqlEntity {
@Id
@Access(AccessType.PROPERTY)
public String getHostRepoId() {
return parent.getName();
// 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. */
@@ -23,7 +23,9 @@ import com.google.common.base.Functions;
import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Streams;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.Result;
import google.registry.model.contact.ContactHistory;
import google.registry.model.host.HostHistory;
import google.registry.model.reporting.HistoryEntry;
@@ -102,12 +104,22 @@ public class DatastoreTransactionManager implements TransactionManager {
@Override
public void insert(Object entity) {
saveEntity(entity);
put(entity);
}
@Override
public void insertAll(ImmutableCollection<?> entities) {
getOfy().save().entities(entities);
putAll(entities);
}
@Override
public void insertWithoutBackup(Object entity) {
putWithoutBackup(entity);
}
@Override
public void insertAllWithoutBackup(ImmutableCollection<?> entities) {
putAllWithoutBackup(entities);
}
@Override
@@ -117,17 +129,37 @@ public class DatastoreTransactionManager implements TransactionManager {
@Override
public void putAll(ImmutableCollection<?> entities) {
getOfy().save().entities(entities);
syncIfTransactionless(getOfy().save().entities(entities));
}
@Override
public void putWithoutBackup(Object entity) {
syncIfTransactionless(getOfy().saveWithoutBackup().entities(entity));
}
@Override
public void putAllWithoutBackup(ImmutableCollection<?> entities) {
syncIfTransactionless(getOfy().saveWithoutBackup().entities(entities));
}
@Override
public void update(Object entity) {
saveEntity(entity);
put(entity);
}
@Override
public void updateAll(ImmutableCollection<?> entities) {
getOfy().save().entities(entities);
putAll(entities);
}
@Override
public void updateWithoutBackup(Object entity) {
putWithoutBackup(entity);
}
@Override
public void updateAllWithoutBackup(ImmutableCollection<?> entities) {
putAllWithoutBackup(entities);
}
@Override
@@ -158,6 +190,11 @@ public class DatastoreTransactionManager implements TransactionManager {
return result;
}
@Override
public <T> T load(T entity) {
return ofy().load().entity(entity).now();
}
@Override
public <T> ImmutableMap<VKey<? extends T>, T> load(Iterable<? extends VKey<? extends T>> keys) {
// Keep track of the Key -> VKey mapping so we can translate them back.
@@ -175,13 +212,17 @@ public class DatastoreTransactionManager implements TransactionManager {
@Override
public <T> ImmutableList<T> loadAll(Class<T> clazz) {
// We can do a ofy().load().type(clazz), but this doesn't work in a transaction.
throw new UnsupportedOperationException("Not available in the Datastore transaction manager");
return ImmutableList.copyOf(getOfy().load().type(clazz));
}
@Override
public <T> ImmutableList<T> loadAll(Iterable<T> entities) {
return ImmutableList.copyOf(getOfy().load().entities(entities).values());
}
@Override
public void delete(VKey<?> key) {
getOfy().delete().key(key.getOfyKey()).now();
syncIfTransactionless(getOfy().delete().key(key.getOfyKey()));
}
@Override
@@ -192,7 +233,35 @@ public class DatastoreTransactionManager implements TransactionManager {
StreamSupport.stream(vKeys.spliterator(), false)
.map(VKey::getOfyKey)
.collect(toImmutableList());
getOfy().delete().keys(list).now();
syncIfTransactionless(getOfy().delete().keys(list));
}
@Override
public void delete(Object entity) {
syncIfTransactionless(getOfy().delete().entity(entity));
}
@Override
public void deleteWithoutBackup(VKey<?> key) {
syncIfTransactionless(getOfy().deleteWithoutBackup().key(key.getOfyKey()));
}
@Override
public void deleteWithoutBackup(Iterable<? extends VKey<?>> keys) {
syncIfTransactionless(
getOfy()
.deleteWithoutBackup()
.keys(Streams.stream(keys).map(VKey::getOfyKey).collect(toImmutableList())));
}
@Override
public void deleteWithoutBackup(Object entity) {
syncIfTransactionless(getOfy().deleteWithoutBackup().entity(entity));
}
@Override
public void clearSessionCache() {
getOfy().clearSessionCache();
}
/**
@@ -209,7 +278,7 @@ public class DatastoreTransactionManager implements TransactionManager {
if (entity instanceof HistoryEntry) {
entity = ((HistoryEntry) entity).asHistoryEntry();
}
getOfy().save().entity(entity);
syncIfTransactionless(getOfy().save().entity(entity));
}
@SuppressWarnings("unchecked")
@@ -227,4 +296,19 @@ public class DatastoreTransactionManager implements TransactionManager {
private <T> T loadNullable(VKey<T> key) {
return toChildHistoryEntryIfPossible(getOfy().load().key(key.getOfyKey()).now());
}
/**
* Executes the given {@link Result} instance synchronously if not in a transaction.
*
* <p>The {@link Result} instance contains a task that will be executed by Objectify
* asynchronously. If it is in a transaction, we don't need to execute the task immediately
* because it is guaranteed to be done by the end of the transaction. However, if it is not in a
* transaction, we need to execute it in case the following code expects that happens before
* themselves.
*/
private void syncIfTransactionless(Result<?> result) {
if (!inTransaction()) {
result.now();
}
}
}
@@ -32,6 +32,7 @@ import static google.registry.model.common.EntityGroupRoot.getCrossTldKey;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.model.registry.Registries.assertTldsExist;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.persistence.transaction.TransactionManagerUtil.transactIfJpaTm;
import static google.registry.util.CollectionUtils.nullToEmptyImmutableCopy;
import static google.registry.util.CollectionUtils.nullToEmptyImmutableSortedCopy;
import static google.registry.util.PasswordUtils.SALT_SUPPLIER;
@@ -704,10 +705,18 @@ public class Registrar extends ImmutableObject
return new Builder(clone(this));
}
/** Creates a {@link VKey} for this instance. */
public VKey<Registrar> createVKey() {
return VKey.create(Registrar.class, clientIdentifier, Key.create(this));
}
/** Creates a {@link VKey} for the given {@code registrarId}. */
public static VKey<Registrar> createVKey(String registrarId) {
checkArgumentNotNull(registrarId, "registrarId must be specified");
return VKey.create(
Registrar.class, registrarId, Key.create(getCrossTldKey(), Registrar.class, registrarId));
}
/** A builder for constructing {@link Registrar}, since it is immutable. */
public static class Builder extends Buildable.Builder<Registrar> {
public Builder() {}
@@ -991,8 +1000,7 @@ public class Registrar extends ImmutableObject
/** Loads and returns a registrar entity by its client id directly from Datastore. */
public static Optional<Registrar> loadByClientId(String clientId) {
checkArgument(!Strings.isNullOrEmpty(clientId), "clientId must be specified");
return Optional.ofNullable(
ofy().load().type(Registrar.class).parent(getCrossTldKey()).id(clientId).now());
return transactIfJpaTm(() -> tm().maybeLoad(createVKey(clientId)));
}
/**
@@ -25,6 +25,7 @@ import static google.registry.model.CacheUtils.memoizeWithShortExpiration;
import static google.registry.model.common.EntityGroupRoot.getCrossTldKey;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.persistence.transaction.TransactionManagerUtil.ofyOrJpaTm;
import static google.registry.util.CollectionUtils.entriesToImmutableMap;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
@@ -59,15 +60,21 @@ public final class Registries {
tm().doTransactionless(
() -> {
ImmutableSet<String> tlds =
ofy()
.load()
.type(Registry.class)
.ancestor(getCrossTldKey())
.keys()
.list()
.stream()
.map(Key::getName)
.collect(toImmutableSet());
ofyOrJpaTm(
() ->
ofy()
.load()
.type(Registry.class)
.ancestor(getCrossTldKey())
.keys()
.list()
.stream()
.map(Key::getName)
.collect(toImmutableSet()),
() ->
tm().loadAll(Registry.class).stream()
.map(Registry::getTldStr)
.collect(toImmutableSet()));
return Registry.getAll(tlds).stream()
.map(e -> Maps.immutableEntry(e.getTldStr(), e.getTldType()))
.collect(entriesToImmutableMap());
@@ -22,7 +22,6 @@ import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.common.collect.Maps.toMap;
import static google.registry.config.RegistryConfig.getSingletonCacheRefreshDuration;
import static google.registry.model.common.EntityGroupRoot.getCrossTldKey;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.util.CollectionUtils.nullToEmptyImmutableCopy;
import static google.registry.util.DateTimeUtils.END_OF_TIME;
@@ -61,6 +60,7 @@ import google.registry.model.domain.fee.BaseFee.FeeType;
import google.registry.model.domain.fee.Fee;
import google.registry.model.registry.label.PremiumList;
import google.registry.model.registry.label.ReservedList;
import google.registry.persistence.VKey;
import google.registry.schema.replay.DatastoreAndSqlEntity;
import google.registry.util.Idn;
import java.util.Map;
@@ -267,28 +267,24 @@ public class Registry extends ImmutableObject implements Buildable, DatastoreAnd
public Optional<Registry> load(final String tld) {
// Enter a transaction-less context briefly; we don't want to enroll every TLD in
// a transaction that might be wrapping this call.
return Optional.ofNullable(
tm().doTransactionless(
() ->
ofy()
.load()
.key(Key.create(getCrossTldKey(), Registry.class, tld))
.now()));
return tm().doTransactionless(() -> tm().maybeLoad(createVKey(tld)));
}
@Override
public Map<String, Optional<Registry>> loadAll(Iterable<? extends String> tlds) {
ImmutableMap<String, Key<Registry>> keysMap =
toMap(
ImmutableSet.copyOf(tlds),
tld -> Key.create(getCrossTldKey(), Registry.class, tld));
Map<Key<Registry>, Registry> entities =
tm().doTransactionless(() -> ofy().load().keys(keysMap.values()));
ImmutableMap<String, VKey<Registry>> keysMap =
toMap(ImmutableSet.copyOf(tlds), Registry::createVKey);
Map<VKey<? extends Registry>, Registry> entities =
tm().doTransactionless(() -> tm().load(keysMap.values()));
return Maps.transformEntries(
keysMap, (k, v) -> Optional.ofNullable(entities.getOrDefault(v, null)));
}
});
public static VKey<Registry> createVKey(String tld) {
return VKey.create(Registry.class, tld, Key.create(getCrossTldKey(), Registry.class, tld));
}
/**
* The name of the pricing engine that this TLD uses.
*
@@ -386,7 +382,7 @@ public class Registry extends ImmutableObject implements Buildable, DatastoreAnd
CreateAutoTimestamp creationTime = CreateAutoTimestamp.create(null);
/** The set of reserved lists that are applicable to this registry. */
@Column(name = "reserved_list_names", nullable = false)
@Column(name = "reserved_list_names")
Set<Key<ReservedList>> reservedLists;
/** Retrieves an ImmutableSet of all ReservedLists associated with this tld. */
@@ -193,7 +193,7 @@ public class HistoryEntry extends ImmutableObject implements Buildable, Datastor
* transaction counts (such as contact or host mutations).
*/
@Transient // domain-specific
Set<DomainTransactionRecord> domainTransactionRecords;
protected Set<DomainTransactionRecord> domainTransactionRecords;
public long getId() {
// For some reason, Hibernate throws NPE during some initialization phase if we don't deal with
@@ -273,7 +273,8 @@ public class HistoryEntry extends ImmutableObject implements Buildable, Datastor
/** This method exists solely to satisfy Hibernate. Use the {@link Builder} instead. */
@SuppressWarnings("UnusedMethod")
private void setDomainTransactionRecords(Set<DomainTransactionRecord> domainTransactionRecords) {
this.domainTransactionRecords = ImmutableSet.copyOf(domainTransactionRecords);
this.domainTransactionRecords =
domainTransactionRecords == null ? null : ImmutableSet.copyOf(domainTransactionRecords);
}
public static VKey<HistoryEntry> createVKey(Key<HistoryEntry> key) {
@@ -16,6 +16,7 @@ package google.registry.model.server;
import static google.registry.model.common.EntityGroupRoot.getCrossTldKey;
import com.google.common.collect.ImmutableList;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.annotation.Entity;
import com.googlecode.objectify.annotation.Id;
@@ -23,11 +24,13 @@ import com.googlecode.objectify.annotation.Parent;
import google.registry.model.ImmutableObject;
import google.registry.model.annotations.ReportedOn;
import google.registry.model.common.EntityGroupRoot;
import google.registry.schema.replay.DatastoreEntity;
import google.registry.schema.replay.SqlEntity;
/** Pointer to the latest {@link KmsSecretRevision}. */
@Entity
@ReportedOn
public class KmsSecret extends ImmutableObject {
public class KmsSecret extends ImmutableObject implements DatastoreEntity {
/** The unique name of this {@link KmsSecret}. */
@Id String name;
@@ -45,6 +48,11 @@ public class KmsSecret extends ImmutableObject {
return latestRevision;
}
@Override
public ImmutableList<SqlEntity> toSqlEntities() {
return ImmutableList.of(); // not persisted in SQL
}
public static KmsSecret create(String name, KmsSecretRevision latestRevision) {
KmsSecret instance = new KmsSecret();
instance.name = name;
@@ -17,14 +17,24 @@ package google.registry.model.server;
import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.model.common.EntityGroupRoot.getCrossTldKey;
import com.google.common.collect.ImmutableList;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.annotation.Entity;
import com.googlecode.objectify.annotation.Id;
import com.googlecode.objectify.annotation.Ignore;
import com.googlecode.objectify.annotation.OnLoad;
import com.googlecode.objectify.annotation.Parent;
import google.registry.model.Buildable;
import google.registry.model.CreateAutoTimestamp;
import google.registry.model.ImmutableObject;
import google.registry.model.annotations.ReportedOn;
import google.registry.schema.replay.DatastoreEntity;
import google.registry.schema.replay.SqlEntity;
import javax.persistence.Column;
import javax.persistence.Index;
import javax.persistence.PostLoad;
import javax.persistence.Table;
import javax.persistence.Transient;
/**
* An encrypted value.
@@ -35,13 +45,22 @@ import google.registry.model.annotations.ReportedOn;
*
* <p>The value can be encrypted and decrypted using Cloud KMS.
*
* <p>Note that the primary key of this entity is {@link #revisionKey}, 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 revisions that differ only by revisionKey.
* This is fine though, because we only use the revision with the highest revisionKey.
*
* <p>TODO: remove Datastore-specific fields post-Registry-3.0-migration and rename to KmsSecret.
*
* @see <a href="https://cloud.google.com/kms/docs/">Google Cloud Key Management Service
* Documentation</a>
* @see google.registry.keyring.kms.KmsKeyring
*/
@Entity
@ReportedOn
public class KmsSecretRevision extends ImmutableObject {
@javax.persistence.Entity(name = "KmsSecret")
@Table(indexes = {@Index(columnList = "secretName")})
public class KmsSecretRevision extends ImmutableObject implements DatastoreEntity, SqlEntity {
/**
* The maximum allowable secret size. Although Datastore allows entities up to 1 MB in size,
@@ -49,18 +68,31 @@ public class KmsSecretRevision extends ImmutableObject {
*/
private static final int MAX_SECRET_SIZE_BYTES = 64 * 1024 * 1024;
/** The revision of this secret. */
@Id long revisionKey;
/**
* The revision of this secret.
*
* <p>TODO: change name of the variable to revisionId once we're off Datastore
*/
@Id
@javax.persistence.Id
@Column(name = "revisionId")
long revisionKey;
/** The parent {@link KmsSecret} which contains metadata about this {@link KmsSecretRevision}. */
@Parent Key<KmsSecret> parent;
@Parent @Transient Key<KmsSecret> parent;
@Column(nullable = false)
@Ignore
String secretName;
/**
* The name of the {@code cryptoKeyVersion} associated with this {@link KmsSecretRevision}.
*
* <p>TODO: change name of the variable to cryptoKeyVersionName once we're off Datastore
*
* @see <a
* href="https://cloud.google.com/kms/docs/reference/rest/v1/projects.locations.keyRings.cryptoKeys.cryptoKeyVersions">projects.locations.keyRings.cryptoKeys.cryptoKeyVersions</a>
*/
@Column(nullable = false, name = "cryptoKeyVersionName")
String kmsCryptoKeyVersionName;
/**
@@ -70,9 +102,11 @@ public class KmsSecretRevision extends ImmutableObject {
* @see <a
* href="https://cloud.google.com/kms/docs/reference/rest/v1/projects.locations.keyRings.cryptoKeys/encrypt">projects.locations.keyRings.cryptoKeys.encrypt</a>
*/
@Column(nullable = false)
String encryptedValue;
/** An automatically managed creation timestamp. */
@Column(nullable = false)
CreateAutoTimestamp creationTime = CreateAutoTimestamp.create(null);
public String getKmsCryptoKeyVersionName() {
@@ -83,6 +117,28 @@ public class KmsSecretRevision extends ImmutableObject {
return encryptedValue;
}
// When loading from SQL, fill out the Datastore-specific field
@PostLoad
void postLoad() {
parent = Key.create(getCrossTldKey(), KmsSecret.class, secretName);
}
// When loading from Datastore, fill out the SQL-specific field
@OnLoad
void onLoad() {
secretName = parent.getName();
}
@Override
public ImmutableList<SqlEntity> toSqlEntities() {
return ImmutableList.of(); // This is dually-written, as we do not care about history
}
@Override
public ImmutableList<DatastoreEntity> toDatastoreEntities() {
return ImmutableList.of(); // This is dually-written, as we do not care about history
}
/** A builder for constructing {@link KmsSecretRevision} entities, since they are immutable. */
public static class Builder extends Buildable.Builder<KmsSecretRevision> {
@@ -108,6 +164,7 @@ public class KmsSecretRevision extends ImmutableObject {
*/
public Builder setParent(String secretName) {
getInstance().parent = Key.create(getCrossTldKey(), KmsSecret.class, secretName);
getInstance().secretName = secretName;
return this;
}
}
@@ -0,0 +1,54 @@
// 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.server;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Strings.isNullOrEmpty;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import java.util.Optional;
/**
* A {@link KmsSecretRevision} DAO for Cloud SQL.
*
* <p>TODO: Rename this class to KmsSecretDao after migrating to Cloud SQL.
*/
public class KmsSecretRevisionSqlDao {
private KmsSecretRevisionSqlDao() {}
/** Saves the given KMS secret revision. */
public static void save(KmsSecretRevision kmsSecretRevision) {
checkArgumentNotNull(kmsSecretRevision, "kmsSecretRevision cannot be null");
jpaTm().assertInTransaction();
jpaTm().put(kmsSecretRevision);
}
/** Returns the latest revision for the secret name given, or absent if nonexistent. */
public static Optional<KmsSecretRevision> getLatestRevision(String secretName) {
checkArgument(!isNullOrEmpty(secretName), "secretName cannot be null or empty");
jpaTm().assertInTransaction();
return jpaTm()
.getEntityManager()
.createQuery(
"FROM KmsSecret ks WHERE ks.revisionKey IN (SELECT MAX(revisionKey) FROM "
+ "KmsSecret subKs WHERE subKs.secretName = :secretName)",
KmsSecretRevision.class)
.setParameter("secretName", secretName)
.getResultStream()
.findFirst();
}
}
@@ -28,8 +28,12 @@ import static google.registry.util.DateTimeUtils.isBeforeOrAt;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.collect.MapDifference;
import com.google.common.collect.Maps;
import com.google.common.flogger.FluentLogger;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.annotation.EmbedMap;
import com.googlecode.objectify.annotation.Entity;
@@ -41,8 +45,19 @@ import google.registry.model.ImmutableObject;
import google.registry.model.annotations.NotBackedUp;
import google.registry.model.annotations.NotBackedUp.Reason;
import google.registry.model.common.EntityGroupRoot;
import google.registry.schema.replay.DatastoreEntity;
import google.registry.schema.replay.SqlEntity;
import google.registry.util.CollectionUtils;
import java.util.Map;
import java.util.Optional;
import javax.persistence.CollectionTable;
import javax.persistence.Column;
import javax.persistence.ElementCollection;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.JoinColumn;
import javax.persistence.MapKeyColumn;
import javax.persistence.Transient;
import org.joda.time.DateTime;
/**
@@ -56,35 +71,50 @@ import org.joda.time.DateTime;
* order to avoid exceeding the one megabyte max entity size limit, we'll also be sharding that
* entity into multiple entities, each entity containing {@value #SHARD_SIZE} rows.
*
* <p>TODO: We can remove the sharding once we have converted entirely to Cloud SQL storage during
* the Registry 3.0 migration. Then, the entire table will be stored conceptually as one entity (in
* fact in SignedMarkRevocationList and SignedMarkRevocationEntry tables).
*
* @see google.registry.tmch.SmdrlCsvParser
* @see <a href="http://tools.ietf.org/html/draft-lozano-tmch-func-spec-08#section-6.2">
* TMCH functional specifications - SMD Revocation List</a>
* @see <a href="http://tools.ietf.org/html/draft-lozano-tmch-func-spec-08#section-6.2">TMCH
* functional specifications - SMD Revocation List</a>
*/
@Entity
@javax.persistence.Entity
@NotBackedUp(reason = Reason.EXTERNALLY_SOURCED)
public class SignedMarkRevocationList extends ImmutableObject {
public class SignedMarkRevocationList extends ImmutableObject
implements DatastoreEntity, SqlEntity {
@VisibleForTesting
static final int SHARD_SIZE = 10000;
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@VisibleForTesting static final int SHARD_SIZE = 10000;
/** Common ancestor for queries. */
@Parent
Key<EntityGroupRoot> parent = getCrossTldKey();
@Parent @Transient Key<EntityGroupRoot> parent = getCrossTldKey();
/** ID for the sharded entity. */
@Id
long id;
@Id @Transient long id;
@Ignore
@javax.persistence.Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Long revisionId;
/** Time when this list was last updated, as specified in the first line of the CSV file. */
DateTime creationTime;
/** A map from SMD IDs to revocation time. */
@EmbedMap
@ElementCollection
@CollectionTable(
name = "SignedMarkRevocationEntry",
joinColumns = @JoinColumn(name = "revisionId", referencedColumnName = "revisionId"))
@MapKeyColumn(name = "smdId")
@Column(name = "revocationTime", nullable = false)
Map</*@MatchesPattern("[0-9]+-[0-9]+")*/ String, DateTime> revokes;
/** Indicates that this is a shard rather than a "full" list. */
@Ignore
boolean isShard;
@Ignore @Transient boolean isShard;
/**
* A cached supplier that fetches the SMDRL shards from Datastore and recombines them into a
@@ -92,32 +122,16 @@ public class SignedMarkRevocationList extends ImmutableObject {
*/
private static final Supplier<SignedMarkRevocationList> CACHE =
memoizeWithShortExpiration(
() ->
tm()
.transactNewReadOnly(
() -> {
Iterable<SignedMarkRevocationList> shards =
ofy()
.load()
.type(SignedMarkRevocationList.class)
.ancestor(getCrossTldKey());
DateTime creationTime =
isEmpty(shards)
? START_OF_TIME
: checkNotNull(
Iterables.get(shards, 0).creationTime, "creationTime");
ImmutableMap.Builder<String, DateTime> revokes =
new ImmutableMap.Builder<>();
for (SignedMarkRevocationList shard : shards) {
revokes.putAll(shard.revokes);
checkState(
creationTime.equals(shard.creationTime),
"Inconsistent creation times: %s vs. %s",
creationTime,
shard.creationTime);
}
return create(creationTime, revokes.build());
}));
() -> {
SignedMarkRevocationList datastoreList = loadFromDatastore();
// 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 signed mark revocation lists.");
}
return datastoreList;
});
/** Return a single logical instance that combines all Datastore shards. */
public static SignedMarkRevocationList get() {
@@ -149,10 +163,39 @@ public class SignedMarkRevocationList extends ImmutableObject {
return revokes.size();
}
/** Save this list to Datastore in sharded form. Returns {@code this}. */
/** Save this list to Datastore in sharded form and to Cloud SQL. Returns {@code this}. */
public SignedMarkRevocationList save() {
tm()
.transact(
saveToDatastore();
SignedMarkRevocationListDao.trySave(this);
return this;
}
/** Loads the shards from Datastore and combines them into one list. */
private static SignedMarkRevocationList loadFromDatastore() {
return tm().transactNewReadOnly(
() -> {
Iterable<SignedMarkRevocationList> shards =
ofy().load().type(SignedMarkRevocationList.class).ancestor(getCrossTldKey());
DateTime creationTime =
isEmpty(shards)
? START_OF_TIME
: checkNotNull(Iterables.get(shards, 0).creationTime, "creationTime");
ImmutableMap.Builder<String, DateTime> revokes = new ImmutableMap.Builder<>();
for (SignedMarkRevocationList shard : shards) {
revokes.putAll(shard.revokes);
checkState(
creationTime.equals(shard.creationTime),
"Inconsistent creation times: %s vs. %s",
creationTime,
shard.creationTime);
}
return create(creationTime, revokes.build());
});
}
/** Save this list to Datastore in sharded form. */
private SignedMarkRevocationList saveToDatastore() {
tm().transact(
() -> {
ofy()
.deleteWithoutBackup()
@@ -165,8 +208,7 @@ public class SignedMarkRevocationList extends ImmutableObject {
ofy()
.saveWithoutBackup()
.entities(
CollectionUtils.partitionMap(revokes, SHARD_SIZE)
.stream()
CollectionUtils.partitionMap(revokes, SHARD_SIZE).stream()
.map(
shardRevokes -> {
SignedMarkRevocationList shard = create(creationTime, shardRevokes);
@@ -180,6 +222,38 @@ public class SignedMarkRevocationList extends ImmutableObject {
return this;
}
private static void loadAndCompareCloudSqlList(SignedMarkRevocationList datastoreList) {
// Lifted with some modifications from ClaimsListShard
Optional<SignedMarkRevocationList> maybeCloudSqlList =
SignedMarkRevocationListDao.getLatestRevision();
if (maybeCloudSqlList.isPresent()) {
SignedMarkRevocationList cloudSqlList = maybeCloudSqlList.get();
MapDifference<String, DateTime> diff =
Maps.difference(datastoreList.revokes, cloudSqlList.revokes);
if (!diff.areEqual()) {
if (diff.entriesDiffering().size() > 10) {
logger.atWarning().log(
String.format(
"Unequal SM revocation lists detected, Cloud SQL list with revision id %d has %d"
+ " different records than the current Datastore list.",
cloudSqlList.revisionId, diff.entriesDiffering().size()));
} else {
StringBuilder diffMessage = new StringBuilder("Unequal SM revocation lists detected:\n");
diff.entriesDiffering()
.forEach(
(label, valueDiff) ->
diffMessage.append(
String.format(
"SMD %s has key %s in Datastore and key %s in Cloud SQL.\n",
label, valueDiff.leftValue(), valueDiff.rightValue())));
logger.atWarning().log(diffMessage.toString());
}
}
} else {
logger.atWarning().log("Signed mark revocation list in Cloud SQL is empty.");
}
}
/** As a safety mechanism, fail if someone tries to save this class directly. */
@OnSave
void disallowUnshardedSaves() {
@@ -188,6 +262,16 @@ public class SignedMarkRevocationList extends ImmutableObject {
}
}
@Override
public ImmutableList<SqlEntity> toSqlEntities() {
return ImmutableList.of(); // Dually-written every day
}
@Override
public ImmutableList<DatastoreEntity> toDatastoreEntities() {
return ImmutableList.of(); // Dually-written every day
}
/** Exception when trying to directly save a {@link SignedMarkRevocationList} without sharding. */
public static class UnshardedSaveException extends RuntimeException {}
}
@@ -0,0 +1,76 @@
// 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.smd;
import static google.registry.model.CacheUtils.memoizeWithShortExpiration;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import com.google.common.base.Supplier;
import com.google.common.flogger.FluentLogger;
import java.util.Optional;
import javax.persistence.EntityManager;
public class SignedMarkRevocationListDao {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private static final Supplier<Optional<SignedMarkRevocationList>> CACHE =
memoizeWithShortExpiration(SignedMarkRevocationListDao::getLatestRevision);
/** Returns the most recent revision of the {@link SignedMarkRevocationList}, from cache. */
public static Optional<SignedMarkRevocationList> getLatestRevisionCached() {
return CACHE.get();
}
public static Optional<SignedMarkRevocationList> getLatestRevision() {
return jpaTm()
.transact(
() -> {
EntityManager em = jpaTm().getEntityManager();
Long revisionId =
em.createQuery("SELECT MAX(revisionId) FROM SignedMarkRevocationList", Long.class)
.getSingleResult();
return em.createQuery(
"FROM SignedMarkRevocationList smrl LEFT JOIN FETCH smrl.revokes "
+ "WHERE smrl.revisionId = :revisionId",
SignedMarkRevocationList.class)
.setParameter("revisionId", revisionId)
.getResultStream()
.findFirst();
});
}
/**
* Try to save the given {@link SignedMarkRevocationList} into Cloud SQL. If the save fails, the
* error will be logged but no exception will be thrown.
*
* <p>This method is used during the dual-write phase of database migration as Datastore is still
* the authoritative database.
*/
static void trySave(SignedMarkRevocationList signedMarkRevocationList) {
try {
SignedMarkRevocationListDao.save(signedMarkRevocationList);
logger.atInfo().log(
"Inserted %,d signed mark revocations into Cloud SQL",
signedMarkRevocationList.revokes.size());
} catch (Throwable e) {
logger.atSevere().withCause(e).log("Error inserting signed mark revocations into Cloud SQL");
}
}
private static void save(SignedMarkRevocationList signedMarkRevocationList) {
jpaTm().transact(() -> jpaTm().getEntityManager().persist(signedMarkRevocationList));
}
}
@@ -17,7 +17,6 @@ package google.registry.module.frontend;
import com.google.monitoring.metrics.MetricReporter;
import dagger.Component;
import dagger.Lazy;
import google.registry.config.CertificateCheckerModule;
import google.registry.config.CredentialModule;
import google.registry.config.RegistryConfig.ConfigModule;
import google.registry.flows.ServerTridProviderModule;
@@ -45,7 +44,6 @@ import javax.inject.Singleton;
@Component(
modules = {
AuthModule.class,
CertificateCheckerModule.class,
ConfigModule.class,
ConsoleConfigModule.class,
CredentialModule.class,
@@ -15,6 +15,7 @@
package google.registry.persistence.transaction;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
@@ -25,6 +26,7 @@ import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Streams;
import com.google.common.flogger.FluentLogger;
import google.registry.config.RegistryConfig;
import google.registry.persistence.JpaRetries;
@@ -238,6 +240,16 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
entities.forEach(this::insert);
}
@Override
public void insertWithoutBackup(Object entity) {
insert(entity);
}
@Override
public void insertAllWithoutBackup(ImmutableCollection<?> entities) {
insertAll(entities);
}
@Override
public void put(Object entity) {
checkArgumentNotNull(entity, "entity must be specified");
@@ -253,6 +265,16 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
entities.forEach(this::put);
}
@Override
public void putWithoutBackup(Object entity) {
put(entity);
}
@Override
public void putAllWithoutBackup(ImmutableCollection<?> entities) {
putAll(entities);
}
@Override
public void update(Object entity) {
checkArgumentNotNull(entity, "entity must be specified");
@@ -269,6 +291,16 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
entities.forEach(this::update);
}
@Override
public void updateWithoutBackup(Object entity) {
update(entity);
}
@Override
public void updateAllWithoutBackup(ImmutableCollection<?> entities) {
updateAll(entities);
}
@Override
public <T> boolean exists(VKey<T> key) {
checkArgumentNotNull(key, "key must be specified");
@@ -315,6 +347,14 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
return result;
}
@Override
public <T> T load(T entity) {
checkArgumentNotNull(entity, "entity must be specified");
assertInTransaction();
return (T)
load(VKey.createSql(entity.getClass(), emf.getPersistenceUnitUtil().getIdentifier(entity)));
}
@Override
public <T> ImmutableMap<VKey<? extends T>, T> load(Iterable<? extends VKey<? extends T>> keys) {
checkArgumentNotNull(keys, "keys must be specified");
@@ -342,6 +382,11 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
.getResultList());
}
@Override
public <T> ImmutableList<T> loadAll(Iterable<T> entities) {
return Streams.stream(entities).map(this::load).collect(toImmutableList());
}
private int internalDelete(VKey<?> key) {
checkArgumentNotNull(key, "key must be specified");
assertInTransaction();
@@ -366,6 +411,37 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
vKeys.forEach(this::internalDelete);
}
@Override
public void delete(Object entity) {
checkArgumentNotNull(entity, "entity must be specified");
assertInTransaction();
Object managedEntity = entity;
if (!getEntityManager().contains(entity)) {
managedEntity = getEntityManager().merge(entity);
}
getEntityManager().remove(managedEntity);
}
@Override
public void deleteWithoutBackup(VKey<?> key) {
delete(key);
}
@Override
public void deleteWithoutBackup(Iterable<? extends VKey<?>> keys) {
delete(keys);
}
@Override
public void deleteWithoutBackup(Object entity) {
delete(entity);
}
@Override
public void clearSessionCache() {
// This is an intended no-op method as there is no session cache in Postgresql.
}
@Override
public <T> void assertDelete(VKey<T> key) {
if (internalDelete(key) != 1) {
@@ -91,18 +91,90 @@ public interface TransactionManager {
/** Persists all new entities in the database, throws exception if any entity already exists. */
void insertAll(ImmutableCollection<?> entities);
/**
* Persists a new entity in the database without writing any backup if the underlying database is
* Datastore.
*
* <p>This method is for the sake of keeping a single code path when replacing ofy() with tm() in
* the application code. When the method is invoked with Datastore, it won't write the commit log
* backup; when invoked with Cloud SQL, it behaves the same as the method which doesn't have
* "WithoutBackup" in its method name because it is not necessary to have the backup mechanism in
* SQL.
*/
void insertWithoutBackup(Object entity);
/**
* Persists all new entities in the database without writing any backup if the underlying database
* is Datastore.
*
* <p>This method is for the sake of keeping a single code path when replacing ofy() with tm() in
* the application code. When the method is invoked with Datastore, it won't write the commit log
* backup; when invoked with Cloud SQL, it behaves the same as the method which doesn't have
* "WithoutBackup" in its method name because it is not necessary to have the backup mechanism in
* SQL.
*/
void insertAllWithoutBackup(ImmutableCollection<?> entities);
/** Persists a new entity or update the existing entity in the database. */
void put(Object entity);
/** Persists all new entities or update the existing entities in the database. */
void putAll(ImmutableCollection<?> entities);
/**
* Persists a new entity or update the existing entity in the database without writing any backup
* if the underlying database is Datastore.
*
* <p>This method is for the sake of keeping a single code path when replacing ofy() with tm() in
* the application code. When the method is invoked with Datastore, it won't write the commit log
* backup; when invoked with Cloud SQL, it behaves the same as the method which doesn't have
* "WithoutBackup" in its method name because it is not necessary to have the backup mechanism in
* SQL.
*/
void putWithoutBackup(Object entity);
/**
* Persists all new entities or update the existing entities in the database without writing any
* backup if the underlying database is Datastore.
*
* <p>This method is for the sake of keeping a single code path when replacing ofy() with tm() in
* the application code. When the method is invoked with Datastore, it won't write the commit log
* backup; when invoked with Cloud SQL, it behaves the same as the method which doesn't have
* "WithoutBackup" in its method name because it is not necessary to have the backup mechanism in
* SQL.
*/
void putAllWithoutBackup(ImmutableCollection<?> entities);
/** Updates an entity in the database, throws exception if the entity does not exist. */
void update(Object entity);
/** Updates all entities in the database, throws exception if any entity does not exist. */
void updateAll(ImmutableCollection<?> entities);
/**
* Updates an entity in the database without writing any backup if the underlying database is
* Datastore.
*
* <p>This method is for the sake of keeping a single code path when replacing ofy() with tm() in
* the application code. When the method is invoked with Datastore, it won't write the commit log
* backup; when invoked with Cloud SQL, it behaves the same as the method which doesn't have
* "WithoutBackup" in its method name because it is not necessary to have the backup mechanism in
* SQL.
*/
void updateWithoutBackup(Object entity);
/**
* Updates all entities in the database without writing any backup if the underlying database is
* Datastore.
*
* <p>This method is for the sake of keeping a single code path when replacing ofy() with tm() in
* the application code. When the method is invoked with Datastore, it won't write the commit log
* backup; when invoked with Cloud SQL, it behaves the same as the method which doesn't have
* "WithoutBackup" in its method name because it is not necessary to have the backup mechanism in
* SQL.
*/
void updateAllWithoutBackup(ImmutableCollection<?> entities);
/** Returns whether the given entity with same ID exists. */
boolean exists(Object entity);
@@ -115,6 +187,11 @@ public interface TransactionManager {
/** Loads the entity by its id, throws NoSuchElementException if it doesn't exist. */
<T> T load(VKey<T> key);
/**
* Loads the given entity from the database, throws NoSuchElementException if it doesn't exist.
*/
<T> T load(T entity);
/**
* Loads the set of entities by their key id.
*
@@ -125,9 +202,38 @@ public interface TransactionManager {
/** Loads all entities of the given type, returns empty if there is no such entity. */
<T> ImmutableList<T> loadAll(Class<T> clazz);
/**
* Loads all given entities from the database, throws NoSuchElementException if it doesn't exist.
*/
<T> ImmutableList<T> loadAll(Iterable<T> entities);
/** Deletes the entity by its id. */
void delete(VKey<?> key);
/** Deletes the set of entities by their key id. */
void delete(Iterable<? extends VKey<?>> keys);
/** Deletes the given entity from the database. */
void delete(Object entity);
/**
* Deletes the entity by its id without writing any backup if the underlying database is
* Datastore.
*/
void deleteWithoutBackup(VKey<?> key);
/**
* Deletes the set of entities by their key id without writing any backup if the underlying
* database is Datastore.
*/
void deleteWithoutBackup(Iterable<? extends VKey<?>> keys);
/**
* Deletes the given entity from the database without writing any backup if the underlying
* database is Datastore.
*/
void deleteWithoutBackup(Object entity);
/** Clears the session cache if the underlying database is Datastore, otherwise it is a no-op. */
void clearSessionCache();
}
@@ -0,0 +1,110 @@
// 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.persistence.transaction;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import google.registry.model.ofy.DatastoreTransactionManager;
import java.util.function.Supplier;
/** Utility class that provides supplementary methods for {@link TransactionManager}. */
public class TransactionManagerUtil {
/**
* Returns the result of the given {@link Supplier}.
*
* <p>If {@link TransactionManagerFactory#tm()} returns a {@link JpaTransactionManager} instance,
* the {@link Supplier} is executed in a transaction.
*/
public static <T> T transactIfJpaTm(Supplier<T> supplier) {
if (tm() instanceof JpaTransactionManager) {
return tm().transact(supplier);
} else {
return supplier.get();
}
}
/**
* Executes the given {@link Runnable}.
*
* <p>If {@link TransactionManagerFactory#tm()} returns a {@link JpaTransactionManager} instance,
* the {@link Runnable} is executed in a transaction.
*/
public static void transactIfJpaTm(Runnable runnable) {
transactIfJpaTm(
() -> {
runnable.run();
return null;
});
}
/**
* Executes either {@code ofyRunnable} if {@link TransactionManagerFactory#tm()} returns a {@link
* JpaTransactionManager} instance, or {@code jpaRunnable} if {@link
* TransactionManagerFactory#tm()} returns a {@link DatastoreTransactionManager} instance.
*/
public static void ofyOrJpaTm(Runnable ofyRunnable, Runnable jpaRunnable) {
ofyOrJpaTm(
() -> {
ofyRunnable.run();
return null;
},
() -> {
jpaRunnable.run();
return null;
});
}
/**
* Returns the result from either {@code ofySupplier} if {@link TransactionManagerFactory#tm()}
* returns a {@link JpaTransactionManager} instance, or {@code jpaSupplier} if {@link
* TransactionManagerFactory#tm()} returns a {@link DatastoreTransactionManager} instance.
*/
public static <T> T ofyOrJpaTm(Supplier<T> ofySupplier, Supplier<T> jpaSupplier) {
if (tm() instanceof DatastoreTransactionManager) {
return ofySupplier.get();
} else if (tm() instanceof JpaTransactionManager) {
return jpaSupplier.get();
} else {
throw new IllegalStateException(
"Expected tm() to be DatastoreTransactionManager or JpaTransactionManager, but got "
+ tm().getClass());
}
}
/**
* Executes the given {@link Runnable} if {@link TransactionManagerFactory#tm()} returns a {@link
* DatastoreTransactionManager} instance, otherwise does nothing.
*/
public static void ofyTmOrDoNothing(Runnable ofyRunnable) {
if (tm() instanceof DatastoreTransactionManager) {
ofyRunnable.run();
}
}
/**
* Returns the result from the given {@link Supplier} if {@link TransactionManagerFactory#tm()}
* returns a {@link DatastoreTransactionManager} instance, otherwise returns null.
*/
public static <T> T ofyTmOrDoNothing(Supplier<T> ofySupplier) {
if (tm() instanceof DatastoreTransactionManager) {
return ofySupplier.get();
} else {
return null;
}
}
private TransactionManagerUtil() {}
}
@@ -30,6 +30,7 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.google.common.flogger.FluentLogger;
import google.registry.flows.certs.CertificateChecker;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarAddress;
import google.registry.model.registry.Registry;
@@ -38,7 +39,6 @@ import google.registry.tools.params.OptionalLongParameter;
import google.registry.tools.params.OptionalPhoneNumberParameter;
import google.registry.tools.params.OptionalStringParameter;
import google.registry.tools.params.PathParameter;
import google.registry.util.CertificateChecker;
import google.registry.util.CidrAddressBlock;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -20,7 +20,6 @@ import dagger.Lazy;
import google.registry.batch.BatchModule;
import google.registry.beam.initsql.BeamJpaModule;
import google.registry.bigquery.BigqueryModule;
import google.registry.config.CertificateCheckerModule;
import google.registry.config.CredentialModule.LocalCredentialJson;
import google.registry.config.RegistryConfig.Config;
import google.registry.config.RegistryConfig.ConfigModule;
@@ -61,7 +60,6 @@ import javax.inject.Singleton;
BatchModule.class,
BeamJpaModule.class,
BigqueryModule.class,
CertificateCheckerModule.class,
ConfigModule.class,
CloudDnsWriterModule.class,
DatastoreAdminModule.class,
@@ -38,6 +38,7 @@ import com.google.common.collect.Sets;
import com.google.common.collect.Streams;
import com.google.common.flogger.FluentLogger;
import google.registry.config.RegistryEnvironment;
import google.registry.flows.certs.CertificateChecker;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarContact;
import google.registry.model.registrar.RegistrarContact.Type;
@@ -56,7 +57,6 @@ import google.registry.ui.forms.FormFieldException;
import google.registry.ui.server.RegistrarFormFields;
import google.registry.ui.server.SendEmailUtils;
import google.registry.util.AppEngineServiceUtils;
import google.registry.util.CertificateChecker;
import google.registry.util.CollectionUtils;
import google.registry.util.DiffUtils;
import java.util.HashSet;
@@ -47,6 +47,7 @@
<class>google.registry.model.domain.DomainHistory</class>
<class>google.registry.model.domain.GracePeriod</class>
<class>google.registry.model.domain.secdns.DelegationSignerData</class>
<class>google.registry.model.domain.secdns.DomainDsDataHistory</class>
<class>google.registry.model.domain.token.AllocationToken</class>
<class>google.registry.model.host.HostHistory</class>
<class>google.registry.model.host.HostResource</class>
@@ -61,6 +62,8 @@
<class>google.registry.model.registry.Registry</class>
<class>google.registry.model.reporting.DomainTransactionRecord</class>
<class>google.registry.model.reporting.Spec11ThreatMatch</class>
<class>google.registry.model.server.KmsSecretRevision</class>
<class>google.registry.model.smd.SignedMarkRevocationList</class>
<class>google.registry.model.tmch.ClaimsListShard</class>
<class>google.registry.persistence.transaction.TransactionEntity</class>
<class>google.registry.schema.cursor.Cursor</class>
@@ -480,8 +480,7 @@ public class ExpandRecurringBillingEventsActionTest
.setSyntheticCreationTime(testTime)
.build());
}
assertBillingEventsForResource(
domain, Iterables.toArray(expectedEvents, BillingEvent.class));
assertBillingEventsForResource(domain, Iterables.toArray(expectedEvents, BillingEvent.class));
assertCursorAt(testTime);
}
@@ -805,30 +805,35 @@ class EppLifecycleDomainTest extends EppTestCase {
// As the losing registrar, read the request poll message, and then ack it.
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
String messageId = "1-C-EXAMPLE-20-26-2001";
assertThatCommand("poll.xml")
.atTime("2001-01-01T00:01:00Z")
.hasResponse("poll_response_domain_transfer_request.xml");
assertThatCommand("poll_ack.xml", ImmutableMap.of("ID", "1-C-EXAMPLE-17-23-2001"))
.hasResponse("poll_response_domain_transfer_request.xml", ImmutableMap.of("ID", messageId));
assertThatCommand("poll_ack.xml", ImmutableMap.of("ID", messageId))
.atTime("2001-01-01T00:01:00Z")
.hasResponse("poll_ack_response_empty.xml");
// Five days in the future, expect a server approval poll message to the loser, and ack it.
messageId = "1-C-EXAMPLE-20-25-2001";
assertThatCommand("poll.xml")
.atTime("2001-01-06T00:01:00Z")
.hasResponse("poll_response_domain_transfer_server_approve_loser.xml");
assertThatCommand("poll_ack.xml", ImmutableMap.of("ID", "1-C-EXAMPLE-17-22-2001"))
.hasResponse(
"poll_response_domain_transfer_server_approve_loser.xml",
ImmutableMap.of("ID", messageId));
assertThatCommand("poll_ack.xml", ImmutableMap.of("ID", messageId))
.atTime("2001-01-06T00:01:00Z")
.hasResponse("poll_ack_response_empty.xml");
assertThatLogoutSucceeds();
// Also expect a server approval poll message to the winner, with the transfer request trid.
messageId = "1-C-EXAMPLE-20-24-2001";
assertThatLoginSucceeds("TheRegistrar", "password2");
assertThatCommand("poll.xml")
.atTime("2001-01-06T00:02:00Z")
.hasResponse(
"poll_response_domain_transfer_server_approve_winner.xml",
ImmutableMap.of("SERVER_TRID", transferRequestTrid));
assertThatCommand("poll_ack.xml", ImmutableMap.of("ID", "1-C-EXAMPLE-17-21-2001"))
ImmutableMap.of("SERVER_TRID", transferRequestTrid, "ID", messageId));
assertThatCommand("poll_ack.xml", ImmutableMap.of("ID", messageId))
.atTime("2001-01-06T00:02:00Z")
.hasResponse("poll_ack_response_empty.xml");
assertThatLogoutSucceeds();
@@ -12,19 +12,22 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.util;
package google.registry.flows.certs;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.util.CertificateChecker.CertificateViolation.ALGORITHM_CONSTRAINED;
import static google.registry.util.CertificateChecker.CertificateViolation.EXPIRED;
import static google.registry.util.CertificateChecker.CertificateViolation.NOT_YET_VALID;
import static google.registry.util.CertificateChecker.CertificateViolation.RSA_KEY_LENGTH_TOO_SHORT;
import static google.registry.util.CertificateChecker.CertificateViolation.VALIDITY_LENGTH_TOO_LONG;
import static google.registry.flows.certs.CertificateChecker.CertificateViolation.ALGORITHM_CONSTRAINED;
import static google.registry.flows.certs.CertificateChecker.CertificateViolation.EXPIRED;
import static google.registry.flows.certs.CertificateChecker.CertificateViolation.NOT_YET_VALID;
import static google.registry.flows.certs.CertificateChecker.CertificateViolation.RSA_KEY_LENGTH_TOO_SHORT;
import static google.registry.flows.certs.CertificateChecker.CertificateViolation.VALIDITY_LENGTH_TOO_LONG;
import static google.registry.testing.CertificateSamples.SAMPLE_CERT;
import static google.registry.testing.CertificateSamples.SAMPLE_CERT3;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static org.junit.jupiter.api.Assertions.assertThrows;
import com.google.common.collect.ImmutableSortedMap;
import google.registry.testing.FakeClock;
import google.registry.util.SelfSignedCaCertificate;
import java.security.KeyPairGenerator;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
@@ -36,54 +39,6 @@ import org.junit.jupiter.api.Test;
class CertificateCheckerTest {
private static final String SSL_HOST = "www.example.tld";
private static final String GOOD_CERTIFICATE =
"-----BEGIN CERTIFICATE-----\n"
+ "MIIDyzCCArOgAwIBAgIUJnhiVrxAxgwkLJzHPm1w/lBoNs4wDQYJKoZIhvcNAQEL\n"
+ "BQAwdTELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE5ldyBZb3JrMREwDwYDVQQHDAhO\n"
+ "ZXcgWW9yazEPMA0GA1UECgwGR29vZ2xlMR0wGwYDVQQLDBRkb21haW4tcmVnaXN0\n"
+ "cnktdGVzdDEQMA4GA1UEAwwHY2xpZW50MTAeFw0yMDEwMTIxNzU5NDFaFw0yMTA0\n"
+ "MzAxNzU5NDFaMHUxCzAJBgNVBAYTAlVTMREwDwYDVQQIDAhOZXcgWW9yazERMA8G\n"
+ "A1UEBwwITmV3IFlvcmsxDzANBgNVBAoMBkdvb2dsZTEdMBsGA1UECwwUZG9tYWlu\n"
+ "LXJlZ2lzdHJ5LXRlc3QxEDAOBgNVBAMMB2NsaWVudDEwggEiMA0GCSqGSIb3DQEB\n"
+ "AQUAA4IBDwAwggEKAoIBAQC0msirO7kXyGEC93stsNYGc02Z77Q2qfHFwaGYkUG8\n"
+ "QvOF5SWN+jwTo5Td6Jj26A26a8MLCtK45TCBuMRNcUsHhajhT19ocphO20iY3zhi\n"
+ "ycwV1id0iwME4kPd1m57BELRE9tUPOxF81/JQXdR1fwT5KRVHYRDWZhaZ5aBmlZY\n"
+ "3t/H9Ly0RBYyApkMaGs3nlb94OOug6SouUfRt02S59ja3wsE2SVF/Eui647OXP7O\n"
+ "QdYXofxuqLoNkE8EnAdl43/enGLiCIVd0G2lABibFF+gbxTtfgbg7YtfUZJdL+Mb\n"
+ "RAcAtuLXEamNQ9H63JgVF16PlQVCDz2XyI3uCfPpDDiBAgMBAAGjUzBRMB0GA1Ud\n"
+ "DgQWBBQ26bWk8qfEBjXs/xZ4m8JZyalnITAfBgNVHSMEGDAWgBQ26bWk8qfEBjXs\n"
+ "/xZ4m8JZyalnITAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAZ\n"
+ "VcsgslBKanKOieJ5ik2d9qzOMXKfBuWPRFWbkC3t9i5awhHqnGAaj6nICnnMZIyt\n"
+ "rdx5lZW5aaQyf0EP/90JAA8Xmty4A6MXmEjQAMiCOpP3A7eeS6Xglgi8IOZl4/bg\n"
+ "LonW62TUkilo5IiFt/QklFTeHIjXB+OvA8+2Quqyd+zp7v6KnhXjvaomim78DhwE\n"
+ "0PIUnjmiRpGpHfTVioTdfhPHZ2Y93Y8K7juL93sQog9aBu5m9XRJCY6wGyWPE83i\n"
+ "kmLfGzjcnaJ6kqCd9xQRFZ0JwHmGlkAQvFoeengbNUqSyjyVgsOoNkEsrWwe/JFO\n"
+ "iqBvjEhJlvRoefvkdR98\n"
+ "-----END CERTIFICATE-----\n";
private static final String BAD_CERTIFICATE =
"-----BEGIN CERTIFICATE-----\n"
+ "MIIDvTCCAqWgAwIBAgIJANoEy6mYwalPMA0GCSqGSIb3DQEBCwUAMHUxCzAJBgNV\n"
+ "BAYTAlVTMREwDwYDVQQIDAhOZXcgWW9yazERMA8GA1UEBwwITmV3IFlvcmsxDzAN\n"
+ "BgNVBAoMBkdvb2dsZTEdMBsGA1UECwwUZG9tYWluLXJlZ2lzdHJ5LXRlc3QxEDAO\n"
+ "BgNVBAMMB2NsaWVudDIwHhcNMTUwODI2MTkyODU3WhcNNDMwMTExMTkyODU3WjB1\n"
+ "MQswCQYDVQQGEwJVUzERMA8GA1UECAwITmV3IFlvcmsxETAPBgNVBAcMCE5ldyBZ\n"
+ "b3JrMQ8wDQYDVQQKDAZHb29nbGUxHTAbBgNVBAsMFGRvbWFpbi1yZWdpc3RyeS10\n"
+ "ZXN0MRAwDgYDVQQDDAdjbGllbnQyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB\n"
+ "CgKCAQEAw2FtuDyoR+rUJHp6k7KwaoHGHPV1xnC8IpG9O0SZubOXrFrnBHggBsbu\n"
+ "+DsknbHXjmoihSFFem0KQqJg5y34aDAHXQV3iqa7nDfb1x4oc5voVz9gqjdmGKNm\n"
+ "WF4MTIPNMu8KY52M852mMCxODK+6MZYp7wCmVa63KdCm0bW/XsLgoA/+FVGwKLhf\n"
+ "UqFzt10Cf+87zl4VHrSaJqcHBYM6yAO5lvkr5VC6g8rRQ+dJ+pBT2D99YpSF1aFc\n"
+ "rWbBreIypixZAnXm/Xoogu6RnohS29VCJp2dXFAJmKXGwyKNQFXfEKxZBaBi8uKH\n"
+ "XF459795eyF9xHgSckEgu7jZlxOk6wIDAQABo1AwTjAdBgNVHQ4EFgQUv26AsQyc\n"
+ "kLOjkhqcFLOuueB33l4wHwYDVR0jBBgwFoAUv26AsQyckLOjkhqcFLOuueB33l4w\n"
+ "DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEANBuV+QDISSnGAEHKbR40\n"
+ "zUYdOjdZ399zcFNqTSPHwmE0Qu8pbmXhofpBfjzrcv0tkVbhSLYnT22qhx7aDmhb\n"
+ "bOS8CeVYCwl5eiDTkJly3pRZLzJpy+UT5z8SPxO3MrTqn+wuj0lBpWRTBCWYAUpr\n"
+ "IFRmgVB3IwVb60UIuxhmuk8TVss2SzNrdhdt36eAIPJ0RWEb0KHYHi35Y6lt4f+t\n"
+ "iVk+ZR0cCbHUs7Q1RqREXHd/ICuMRLY/MsadVQ9WDqVOridh198X/OIqdx/p9kvJ\n"
+ "1R80jDcVGNhYVXLmHu4ho4xrOaliSYvUJSCmaaSEGVZ/xE5PI7S6A8RMdj0iXLSt\n"
+ "Bg==\n"
+ "-----END CERTIFICATE-----\n";
private FakeClock fakeClock = new FakeClock();
private CertificateChecker certificateChecker =
@@ -241,8 +196,8 @@ class CertificateCheckerTest {
@Test
void test_checkCertificate_validCertificateString() throws Exception {
fakeClock.setTo(DateTime.parse("2020-11-01T00:00:00Z"));
assertThat(certificateChecker.checkCertificate(GOOD_CERTIFICATE)).isEmpty();
assertThat(certificateChecker.checkCertificate(BAD_CERTIFICATE))
assertThat(certificateChecker.checkCertificate(SAMPLE_CERT3)).isEmpty();
assertThat(certificateChecker.checkCertificate(SAMPLE_CERT))
.containsExactly(VALIDITY_LENGTH_TOO_LONG);
}
@@ -32,9 +32,11 @@ import com.googlecode.objectify.annotation.Id;
import google.registry.schema.replay.EntityTest.EntityForTesting;
import google.registry.testing.AppEngineExtension;
import google.registry.util.CidrAddressBlock;
import java.lang.reflect.Field;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Deque;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -320,4 +322,40 @@ public class ImmutableObjectTest {
root.set = ImmutableSet.of(Key.create(persistResource(ValueObject.create(1, "foo"))));
assertThat(root.toHydratedString()).contains("foo");
}
@Test
void testInsignificantFields() {
HasInsignificantFields instance1 =
HasInsignificantFields.create("significant", "insignificant");
HasInsignificantFields instance2 = HasInsignificantFields.create("significant", "other");
assertThat(instance1).isEqualTo(instance2);
// The hash code test test is implicit in "equals", it is added here just for clarity.
assertThat(instance1.hashCode()).isEqualTo(instance2.hashCode());
assertThat(instance1.toString()).matches(
"(?s)HasInsignificantFields (.*): \\{\\s*significant=significant\\s*\\}\\s*");
}
static class HasInsignificantFields extends ImmutableObject {
String significant;
String insignificant;
static HasInsignificantFields create(String significant, String insignificant) {
HasInsignificantFields instance = new HasInsignificantFields();
instance.significant = significant;
instance.insignificant = insignificant;
return instance;
}
@Override
protected Map<Field, Object> getSignificantFields() {
Map<Field, Object> result = new LinkedHashMap();
for (Map.Entry<Field, Object> entry : ModelUtils.getFieldValues(this).entrySet()) {
if (!entry.getKey().getName().equals("insignificant")) {
result.put(entry.getKey(), entry.getValue());
}
}
return result;
}
}
}
@@ -17,11 +17,12 @@ package google.registry.model.billing;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.domain.token.AllocationToken.TokenType.UNLIMITED_USE;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.persistence.transaction.TransactionManagerUtil.ofyTmOrDoNothing;
import static google.registry.persistence.transaction.TransactionManagerUtil.transactIfJpaTm;
import static google.registry.testing.DatastoreHelper.createTld;
import static google.registry.testing.DatastoreHelper.persistActiveDomain;
import static google.registry.testing.DatastoreHelper.persistResource;
import static google.registry.testing.SqlHelper.saveRegistrar;
import static google.registry.util.DateTimeUtils.END_OF_TIME;
import static org.joda.money.CurrencyUnit.USD;
import static org.joda.time.DateTimeZone.UTC;
@@ -39,13 +40,17 @@ import google.registry.model.domain.rgp.GracePeriodStatus;
import google.registry.model.domain.token.AllocationToken;
import google.registry.model.domain.token.AllocationToken.TokenStatus;
import google.registry.model.reporting.HistoryEntry;
import google.registry.persistence.VKey;
import google.registry.testing.DualDatabaseTest;
import google.registry.testing.TestOfyAndSql;
import google.registry.testing.TestOfyOnly;
import google.registry.util.DateTimeUtils;
import org.joda.money.Money;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link BillingEvent}. */
@DualDatabaseTest
public class BillingEventTest extends EntityTestCase {
private final DateTime now = DateTime.now(UTC);
@@ -67,16 +72,26 @@ public class BillingEventTest extends EntityTestCase {
void setUp() {
createTld("tld");
domain = persistActiveDomain("foo.tld");
historyEntry = persistResource(
new HistoryEntry.Builder()
.setParent(domain)
.setModificationTime(now)
.build());
historyEntry2 = persistResource(
new HistoryEntry.Builder()
.setParent(domain)
.setModificationTime(now.plusDays(1))
.build());
historyEntry =
persistResource(
new HistoryEntry.Builder()
.setParent(domain)
.setModificationTime(now)
.setRequestedByRegistrar(false)
.setType(HistoryEntry.Type.DOMAIN_CREATE)
.setXmlBytes(new byte[0])
.build()
.toChildHistoryEntity());
historyEntry2 =
persistResource(
new HistoryEntry.Builder()
.setParent(domain)
.setModificationTime(now.plusDays(1))
.setRequestedByRegistrar(false)
.setType(HistoryEntry.Type.DOMAIN_CREATE)
.setXmlBytes(new byte[0])
.build()
.toChildHistoryEntity());
AllocationToken allocationToken =
persistResource(
@@ -149,74 +164,38 @@ public class BillingEventTest extends EntityTestCase {
.setEventTime(now.plusDays(1))
.setBillingTime(now.plusYears(1).plusDays(45))
.setRecurringEventKey(recurring.createVKey())));
modification = persistResource(commonInit(
new BillingEvent.Modification.Builder()
.setParent(historyEntry2)
.setReason(Reason.CREATE)
.setCost(Money.of(USD, 1))
.setDescription("Something happened")
.setEventTime(now.plusDays(1))
.setEventKey(Key.create(oneTime))));
modification =
ofyTmOrDoNothing(
() ->
persistResource(
commonInit(
new BillingEvent.Modification.Builder()
.setParent(historyEntry2)
.setReason(Reason.CREATE)
.setCost(Money.of(USD, 1))
.setDescription("Something happened")
.setEventTime(now.plusDays(1))
.setEventKey(Key.create(oneTime)))));
}
private <E extends BillingEvent, B extends BillingEvent.Builder<E, B>> E commonInit(B builder) {
return builder
.setClientId("a registrar")
.setTargetId("foo.tld")
.build();
return builder.setClientId("TheRegistrar").setTargetId("foo.tld").build();
}
private void saveNewBillingEvent(BillingEvent billingEvent) {
jpaTm().transact(() -> jpaTm().insert(billingEvent));
}
@Test
void testCloudSqlPersistence_OneTime() {
saveRegistrar("a registrar");
saveNewBillingEvent(oneTime);
BillingEvent.OneTime persisted = jpaTm().transact(() -> jpaTm().load(oneTime.createVKey()));
// TODO(b/168325240): Remove this fix after VKeyConverter generates symmetric key for
// AllocationToken.
BillingEvent.OneTime fixed =
persisted.asBuilder().setAllocationToken(oneTime.getAllocationToken().get()).build();
assertThat(fixed).isEqualTo(oneTime);
}
@Test
void testCloudSqlPersistence_Cancellation() {
saveRegistrar("a registrar");
saveNewBillingEvent(oneTime);
saveNewBillingEvent(cancellationOneTime);
BillingEvent.Cancellation persisted =
jpaTm().transact(() -> jpaTm().load(cancellationOneTime.createVKey()));
// TODO(b/168537779): Remove this fix after VKey<OneTime> can be reconstructed correctly.
BillingEvent.Cancellation fixed =
persisted.asBuilder().setOneTimeEventKey(oneTime.createVKey()).build();
assertThat(fixed).isEqualTo(cancellationOneTime);
}
@Test
void testCloudSqlPersistence_Recurring() {
saveRegistrar("a registrar");
saveNewBillingEvent(recurring);
BillingEvent.Recurring persisted = jpaTm().transact(() -> jpaTm().load(recurring.createVKey()));
assertThat(persisted).isEqualTo(recurring);
}
@Test
@TestOfyAndSql
void testPersistence() {
assertThat(ofy().load().entity(oneTime).now()).isEqualTo(oneTime);
assertThat(ofy().load().entity(oneTimeSynthetic).now()).isEqualTo(oneTimeSynthetic);
assertThat(ofy().load().entity(recurring).now()).isEqualTo(recurring);
assertThat(ofy().load().entity(cancellationOneTime).now()).isEqualTo(cancellationOneTime);
assertThat(ofy().load().entity(cancellationRecurring).now()).isEqualTo(cancellationRecurring);
assertThat(ofy().load().entity(modification).now()).isEqualTo(modification);
assertThat(transactIfJpaTm(() -> tm().load(oneTime))).isEqualTo(oneTime);
assertThat(transactIfJpaTm(() -> tm().load(oneTimeSynthetic))).isEqualTo(oneTimeSynthetic);
assertThat(transactIfJpaTm(() -> tm().load(recurring))).isEqualTo(recurring);
assertThat(transactIfJpaTm(() -> tm().load(cancellationOneTime)))
.isEqualTo(cancellationOneTime);
assertThat(transactIfJpaTm(() -> tm().load(cancellationRecurring)))
.isEqualTo(cancellationRecurring);
ofyTmOrDoNothing(() -> assertThat(tm().load(modification)).isEqualTo(modification));
}
@Test
@TestOfyOnly
void testParenting() {
// Note that these are all tested separately because BillingEvent is an abstract base class that
// lacks the @Entity annotation, and thus we cannot call .type(BillingEvent.class)
@@ -238,19 +217,14 @@ public class BillingEventTest extends EntityTestCase {
.containsExactly(modification);
}
@Test
@TestOfyAndSql
void testCancellationMatching() {
Key<?> recurringKey =
ofy()
.load()
.entity(oneTimeSynthetic)
.now()
.getCancellationMatchingBillingEvent()
.getOfyKey();
assertThat(ofy().load().key(recurringKey).now()).isEqualTo(recurring);
VKey<?> recurringKey =
transactIfJpaTm(() -> tm().load(oneTimeSynthetic).getCancellationMatchingBillingEvent());
assertThat(transactIfJpaTm(() -> tm().load(recurringKey))).isEqualTo(recurring);
}
@Test
@TestOfyOnly
void testIndexing() throws Exception {
verifyIndexing(
oneTime,
@@ -272,7 +246,7 @@ public class BillingEventTest extends EntityTestCase {
verifyIndexing(modification, "clientId", "eventTime");
}
@Test
@TestOfyAndSql
void testFailure_syntheticFlagWithoutCreationTime() {
IllegalStateException thrown =
assertThrows(
@@ -288,7 +262,7 @@ public class BillingEventTest extends EntityTestCase {
.contains("Synthetic creation time must be set if and only if the SYNTHETIC flag is set.");
}
@Test
@TestOfyAndSql
void testFailure_syntheticCreationTimeWithoutFlag() {
IllegalStateException thrown =
assertThrows(
@@ -299,7 +273,7 @@ public class BillingEventTest extends EntityTestCase {
.contains("Synthetic creation time must be set if and only if the SYNTHETIC flag is set");
}
@Test
@TestOfyAndSql
void testFailure_syntheticFlagWithoutCancellationMatchingKey() {
IllegalStateException thrown =
assertThrows(
@@ -317,7 +291,7 @@ public class BillingEventTest extends EntityTestCase {
+ "if and only if the SYNTHETIC flag is set");
}
@Test
@TestOfyAndSql
void testFailure_cancellationMatchingKeyWithoutFlag() {
IllegalStateException thrown =
assertThrows(
@@ -334,7 +308,7 @@ public class BillingEventTest extends EntityTestCase {
+ "if and only if the SYNTHETIC flag is set");
}
@Test
@TestOfyAndSql
void testSuccess_cancellation_forGracePeriod_withOneTime() {
BillingEvent.Cancellation newCancellation =
BillingEvent.Cancellation.forGracePeriod(
@@ -343,10 +317,15 @@ public class BillingEventTest extends EntityTestCase {
"foo.tld");
// Set ID to be the same to ignore for the purposes of comparison.
newCancellation = newCancellation.asBuilder().setId(cancellationOneTime.getId()).build();
assertThat(newCancellation).isEqualTo(cancellationOneTime);
// TODO(b/168537779): Remove setRecurringEventKey after symmetric VKey can be reconstructed
// correctly.
assertThat(newCancellation)
.isEqualTo(
cancellationOneTime.asBuilder().setOneTimeEventKey(oneTime.createVKey()).build());
}
@Test
@TestOfyAndSql
void testSuccess_cancellation_forGracePeriod_withRecurring() {
BillingEvent.Cancellation newCancellation =
BillingEvent.Cancellation.forGracePeriod(
@@ -354,16 +333,21 @@ public class BillingEventTest extends EntityTestCase {
GracePeriodStatus.AUTO_RENEW,
domain.getRepoId(),
now.plusYears(1).plusDays(45),
"a registrar",
"TheRegistrar",
recurring.createVKey()),
historyEntry2,
"foo.tld");
// Set ID to be the same to ignore for the purposes of comparison.
newCancellation = newCancellation.asBuilder().setId(cancellationRecurring.getId()).build();
assertThat(newCancellation).isEqualTo(cancellationRecurring);
// TODO(b/168537779): Remove setRecurringEventKey after symmetric VKey can be reconstructed
// correctly.
assertThat(newCancellation)
.isEqualTo(
cancellationRecurring.asBuilder().setRecurringEventKey(recurring.createVKey()).build());
}
@Test
@TestOfyAndSql
void testFailure_cancellation_forGracePeriodWithoutBillingEvent() {
IllegalArgumentException thrown =
assertThrows(
@@ -380,7 +364,7 @@ public class BillingEventTest extends EntityTestCase {
assertThat(thrown).hasMessageThat().contains("grace period without billing event");
}
@Test
@TestOfyAndSql
void testFailure_cancellationWithNoBillingEvent() {
IllegalStateException thrown =
assertThrows(
@@ -394,7 +378,7 @@ public class BillingEventTest extends EntityTestCase {
assertThat(thrown).hasMessageThat().contains("exactly one billing event");
}
@Test
@TestOfyAndSql
void testFailure_cancellationWithBothBillingEvents() {
IllegalStateException thrown =
assertThrows(
@@ -408,7 +392,7 @@ public class BillingEventTest extends EntityTestCase {
assertThat(thrown).hasMessageThat().contains("exactly one billing event");
}
@Test
@TestOfyAndSql
void testDeadCodeThatDeletedScrapCommandsReference() {
assertThat(recurring.getParentKey()).isEqualTo(Key.create(historyEntry));
new BillingEvent.OneTime.Builder().setParent(Key.create(historyEntry));
@@ -78,6 +78,7 @@ public class DomainBaseSqlTest {
private HostResource host;
private ContactResource contact;
private ContactResource contact2;
private ImmutableSet<GracePeriod> gracePeriods;
@BeforeEach
void setUp() {
@@ -506,6 +507,20 @@ public class DomainBaseSqlTest {
.setServerApproveAutorenewEvent(billEvent.createVKey())
.setServerApproveAutorenewPollMessage(autorenewPollMessage.createVKey())
.build();
gracePeriods =
ImmutableSet.of(
GracePeriod.create(
GracePeriodStatus.ADD,
"4-COM",
END_OF_TIME,
"registrar1",
oneTimeBillingEvent.createVKey()),
GracePeriod.createForRecurring(
GracePeriodStatus.AUTO_RENEW,
"4-COM",
END_OF_TIME,
"registrar1",
billEvent.createVKey()));
jpaTm().insert(contact);
jpaTm().insert(contact2);
@@ -517,6 +532,7 @@ public class DomainBaseSqlTest {
.setAutorenewPollMessage(autorenewPollMessage.createVKey())
.setDeletePollMessage(deletePollMessage.createVKey())
.setTransferData(transferData)
.setGracePeriods(gracePeriods)
.build();
historyEntry = historyEntry.asBuilder().setDomainContent(domain).build();
jpaTm().insert(historyEntry);
@@ -553,6 +569,7 @@ public class DomainBaseSqlTest {
.isEqualTo(originalTransferData.getServerApproveAutorenewEvent());
assertThat(persistedTransferData.getServerApproveAutorenewPollMessage())
.isEqualTo(originalTransferData.getServerApproveAutorenewPollMessage());
assertThat(persisted.getGracePeriods()).isEqualTo(gracePeriods);
}
@Test
@@ -624,6 +641,21 @@ public class DomainBaseSqlTest {
createLegacyVKey(
PollMessage.Autorenew.class, autorenewPollMessage.getId()))
.build();
gracePeriods =
ImmutableSet.of(
GracePeriod.create(
GracePeriodStatus.ADD,
"4-COM",
END_OF_TIME,
"registrar1",
createLegacyVKey(
BillingEvent.OneTime.class, oneTimeBillingEvent.getId())),
GracePeriod.createForRecurring(
GracePeriodStatus.AUTO_RENEW,
"4-COM",
END_OF_TIME,
"registrar1",
createLegacyVKey(BillingEvent.Recurring.class, billEvent.getId())));
jpaTm().insert(contact);
jpaTm().insert(contact2);
@@ -639,6 +671,7 @@ public class DomainBaseSqlTest {
.setDeletePollMessage(
createLegacyVKey(PollMessage.OneTime.class, deletePollMessage.getId()))
.setTransferData(transferData)
.setGracePeriods(gracePeriods)
.build();
historyEntry = historyEntry.asBuilder().setDomainContent(domain).build();
jpaTm().insert(historyEntry);
@@ -675,6 +708,7 @@ public class DomainBaseSqlTest {
.isEqualTo(originalTransferData.getServerApproveAutorenewEvent());
assertThat(persistedTransferData.getServerApproveAutorenewPollMessage())
.isEqualTo(originalTransferData.getServerApproveAutorenewPollMessage());
assertThat(domain.getGracePeriods()).isEqualTo(gracePeriods);
}
private <T> VKey<T> createLegacyVKey(Class<T> clazz, long id) {
@@ -38,6 +38,7 @@ import com.google.common.collect.Ordering;
import com.google.common.collect.Streams;
import com.googlecode.objectify.Key;
import google.registry.model.EntityTestCase;
import google.registry.model.ImmutableObject;
import google.registry.model.billing.BillingEvent;
import google.registry.model.billing.BillingEvent.Reason;
import google.registry.model.contact.ContactResource;
@@ -830,4 +831,63 @@ public class DomainBaseTest extends EntityTestCase {
assertThat(getOnlyElement(clone.getGracePeriods()).getType())
.isEqualTo(GracePeriodStatus.TRANSFER);
}
@Test
void testHistoryIdRestoration() {
// Verify that history ids for billing events are restored during load from datastore. History
// ids are not used by business code or persisted in datastore, but only to reconstruct
// objectify keys when loading from SQL.
DateTime now = fakeClock.nowUtc();
domain =
persistResource(
domain
.asBuilder()
.setRegistrationExpirationTime(now.plusYears(1))
.setGracePeriods(
ImmutableSet.of(
GracePeriod.createForRecurring(
GracePeriodStatus.AUTO_RENEW,
domain.getRepoId(),
now.plusDays(1),
"NewRegistrar",
recurringBillKey),
GracePeriod.create(
GracePeriodStatus.RENEW,
domain.getRepoId(),
now.plusDays(1),
"NewRegistrar",
oneTimeBillKey)))
.build());
ImmutableSet<BillEventInfo> historyIds =
domain.getGracePeriods().stream()
.map(
gp ->
new BillEventInfo(
gp.getRecurringBillingEvent(), gp.billingEventRecurringHistoryId,
gp.getOneTimeBillingEvent(), gp.billingEventOneTimeHistoryId))
.collect(toImmutableSet());
assertThat(historyIds)
.isEqualTo(
ImmutableSet.of(
new BillEventInfo(null, null, oneTimeBillKey, historyEntryKey.getId()),
new BillEventInfo(recurringBillKey, historyEntryKey.getId(), null, null)));
}
static class BillEventInfo extends ImmutableObject {
VKey<BillingEvent.Recurring> billingEventRecurring;
Long billingEventRecurringHistoryId;
VKey<BillingEvent.OneTime> billingEventOneTime;
Long billingEventOneTimeHistoryId;
BillEventInfo(
VKey<BillingEvent.Recurring> billingEventRecurring,
Long billingEventRecurringHistoryId,
VKey<BillingEvent.OneTime> billingEventOneTime,
Long billingEventOneTimeHistoryId) {
this.billingEventRecurring = billingEventRecurring;
this.billingEventRecurringHistoryId = billingEventRecurringHistoryId;
this.billingEventOneTime = billingEventOneTime;
this.billingEventOneTimeHistoryId = billingEventOneTimeHistoryId;
}
}
}
@@ -44,6 +44,7 @@ public class GracePeriodTest {
private final DateTime now = DateTime.now(UTC);
private BillingEvent.OneTime onetime;
private VKey<BillingEvent.Recurring> recurringKey;
@BeforeEach
void before() {
@@ -59,6 +60,14 @@ public class GracePeriodTest {
.setPeriodYears(1)
.setTargetId("foo.google")
.build();
recurringKey =
VKey.create(
Recurring.class,
12345,
Key.create(
Key.create(Key.create(DomainBase.class, "1-TEST"), HistoryEntry.class, 343L),
Recurring.class,
12345));
}
@Test
@@ -71,6 +80,24 @@ public class GracePeriodTest {
assertThat(gracePeriod.getClientId()).isEqualTo("TheRegistrar");
assertThat(gracePeriod.getExpirationTime()).isEqualTo(now.plusDays(1));
assertThat(gracePeriod.hasBillingEvent()).isTrue();
assertThat(gracePeriod.billingEventOneTimeHistoryId).isEqualTo(12345L);
assertThat(gracePeriod.billingEventRecurringHistoryId).isNull();
}
@Test
void testSuccess_forRecurringEvent() {
GracePeriod gracePeriod =
GracePeriod.createForRecurring(
GracePeriodStatus.AUTO_RENEW, "1-TEST", now.plusDays(1), "TheRegistrar", recurringKey);
assertThat(gracePeriod.getType()).isEqualTo(GracePeriodStatus.AUTO_RENEW);
assertThat(gracePeriod.getDomainRepoId()).isEqualTo("1-TEST");
assertThat(gracePeriod.getOneTimeBillingEvent()).isNull();
assertThat(gracePeriod.getRecurringBillingEvent()).isEqualTo(recurringKey);
assertThat(gracePeriod.getClientId()).isEqualTo("TheRegistrar");
assertThat(gracePeriod.getExpirationTime()).isEqualTo(now.plusDays(1));
assertThat(gracePeriod.hasBillingEvent()).isTrue();
assertThat(gracePeriod.billingEventOneTimeHistoryId).isNull();
assertThat(gracePeriod.billingEventRecurringHistoryId).isEqualTo(343L);
}
@Test
@@ -98,11 +125,6 @@ public class GracePeriodTest {
@Test
void testFailure_createForRecurring_notAutoRenew() {
Key<Recurring> recurringKey =
Key.create(
Key.create(Key.create(DomainBase.class, "1-TEST"), HistoryEntry.class, 343L),
Recurring.class,
12345);
IllegalArgumentException thrown =
assertThrows(
IllegalArgumentException.class,
@@ -112,7 +134,7 @@ public class GracePeriodTest {
"1-TEST",
now.plusDays(1),
"TheRegistrar",
VKey.create(Recurring.class, 12345, recurringKey)));
recurringKey));
assertThat(thrown).hasMessageThat().contains("autorenew");
}
}
@@ -34,6 +34,7 @@ import google.registry.model.domain.DomainBase;
import google.registry.model.domain.DomainContent;
import google.registry.model.domain.DomainHistory;
import google.registry.model.domain.Period;
import google.registry.model.domain.secdns.DelegationSignerData;
import google.registry.model.eppcommon.Trid;
import google.registry.model.host.HostResource;
import google.registry.model.reporting.DomainTransactionRecord;
@@ -141,6 +142,7 @@ public class DomainHistoryTest extends EntityTestCase {
newDomainBase("example.tld", "domainRepoId", contact)
.asBuilder()
.setNameservers(host.createVKey())
.setDsData(ImmutableSet.of(DelegationSignerData.create(1, 2, 3, new byte[] {0, 1, 2})))
.build();
jpaTm().transact(() -> jpaTm().insert(domain));
return domain;
@@ -116,7 +116,8 @@ public class LegacyHistoryObjectTest extends EntityTestCase {
// The objects will be mostly the same, but the DomainHistory object has a couple extra fields
assertAboutImmutableObjects()
.that(legacyHistoryEntry)
.isEqualExceptFields(fromObjectify, "domainContent", "domainRepoId", "nsHosts");
.isEqualExceptFields(
fromObjectify, "domainContent", "domainRepoId", "nsHosts", "dsDataHistories");
assertThat(fromObjectify instanceof DomainHistory).isTrue();
DomainHistory legacyDomainHistory = (DomainHistory) fromObjectify;
@@ -129,7 +130,12 @@ public class LegacyHistoryObjectTest extends EntityTestCase {
.that(legacyDomainHistory)
.isEqualExceptFields(
// NB: period, transaction records, and other client ID are added in #794
legacyHistoryFromSql, "period", "domainTransactionRecords", "otherClientId", "nsHosts");
legacyHistoryFromSql,
"period",
"domainTransactionRecords",
"otherClientId",
"nsHosts",
"dsDataHistories");
assertThat(nullToEmpty(legacyDomainHistory.getNsHosts()))
.isEqualTo(nullToEmpty(legacyHistoryFromSql.getNsHosts()));
}
@@ -23,9 +23,9 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
import google.registry.model.EntityTestCase;
import google.registry.testing.DualDatabaseTest;
import google.registry.testing.TestOfyAndSql;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.TestTemplate;
/** Unit tests for {@link RdeRevision}. */
@DualDatabaseTest
@@ -40,20 +40,20 @@ public class RdeRevisionTest extends EntityTestCase {
fakeClock.setTo(DateTime.parse("1984-12-18TZ"));
}
@TestTemplate
@TestOfyAndSql
void testGetNextRevision_objectDoesntExist_returnsZero() {
tm().transact(
() -> assertThat(getNextRevision("torment", fakeClock.nowUtc(), FULL)).isEqualTo(0));
}
@TestTemplate
@TestOfyAndSql
void testGetNextRevision_objectExistsAtZero_returnsOne() {
save("sorrow", fakeClock.nowUtc(), FULL, 0);
tm().transact(
() -> assertThat(getNextRevision("sorrow", fakeClock.nowUtc(), FULL)).isEqualTo(1));
}
@TestTemplate
@TestOfyAndSql
void testSaveRevision_objectDoesntExist_newRevisionIsZero_nextRevIsOne() {
tm().transact(() -> saveRevision("despondency", fakeClock.nowUtc(), FULL, 0));
tm().transact(
@@ -61,7 +61,7 @@ public class RdeRevisionTest extends EntityTestCase {
assertThat(getNextRevision("despondency", fakeClock.nowUtc(), FULL)).isEqualTo(1));
}
@TestTemplate
@TestOfyAndSql
void testSaveRevision_objectDoesntExist_newRevisionIsOne_throwsVe() {
IllegalArgumentException thrown =
assertThrows(
@@ -74,7 +74,7 @@ public class RdeRevisionTest extends EntityTestCase {
+ "when trying to save new revision 1");
}
@TestTemplate
@TestOfyAndSql
void testSaveRevision_objectExistsAtZero_newRevisionIsZero_throwsVe() {
save("melancholy", fakeClock.nowUtc(), FULL, 0);
IllegalArgumentException thrown =
@@ -84,7 +84,7 @@ public class RdeRevisionTest extends EntityTestCase {
assertThat(thrown).hasMessageThat().contains("object already created");
}
@TestTemplate
@TestOfyAndSql
void testSaveRevision_objectExistsAtZero_newRevisionIsOne_nextRevIsTwo() {
DateTime startOfDay = fakeClock.nowUtc().withTimeAtStartOfDay();
save("melancholy", startOfDay, FULL, 0);
@@ -93,7 +93,7 @@ public class RdeRevisionTest extends EntityTestCase {
tm().transact(() -> assertThat(getNextRevision("melancholy", startOfDay, FULL)).isEqualTo(2));
}
@TestTemplate
@TestOfyAndSql
void testSaveRevision_objectExistsAtZero_newRevisionIsTwo_throwsVe() {
save("melancholy", fakeClock.nowUtc(), FULL, 0);
IllegalArgumentException thrown =
@@ -105,7 +105,7 @@ public class RdeRevisionTest extends EntityTestCase {
.contains("RDE revision object should be at revision 1 but was");
}
@TestTemplate
@TestOfyAndSql
void testSaveRevision_negativeRevision_throwsIae() {
IllegalArgumentException thrown =
assertThrows(
@@ -114,7 +114,7 @@ public class RdeRevisionTest extends EntityTestCase {
assertThat(thrown).hasMessageThat().contains("Negative revision");
}
@TestTemplate
@TestOfyAndSql
void testSaveRevision_callerNotInTransaction_throwsIse() {
IllegalStateException thrown =
assertThrows(
@@ -0,0 +1,86 @@
// 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.server;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.ImmutableObjectSubject.assertAboutImmutableObjects;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
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 org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
/** Tests for {@link google.registry.model.server.KmsSecretRevisionSqlDao}. */
public class KmsSecretRevisionSqlDaoTest {
private final FakeClock fakeClock = new FakeClock();
@RegisterExtension
@Order(value = 1)
DatastoreEntityExtension datastoreEntityExtension = new DatastoreEntityExtension();
@RegisterExtension
JpaIntegrationWithCoverageExtension jpa =
new JpaTestRules.Builder().withClock(fakeClock).buildIntegrationWithCoverageExtension();
@Test
void testSaveAndRetrieve() {
KmsSecretRevision revision = createRevision();
jpaTm().transact(() -> KmsSecretRevisionSqlDao.save(revision));
Optional<KmsSecretRevision> fromSql =
jpaTm().transact(() -> KmsSecretRevisionSqlDao.getLatestRevision("secretName"));
assertThat(fromSql.isPresent()).isTrue();
assertAboutImmutableObjects().that(revision).isEqualExceptFields(fromSql.get(), "creationTime");
}
@Test
void testMultipleRevisions() {
KmsSecretRevision revision = createRevision();
jpaTm().transact(() -> KmsSecretRevisionSqlDao.save(revision));
KmsSecretRevision secondRevision = createRevision();
secondRevision.encryptedValue = "someOtherValue";
jpaTm().transact(() -> KmsSecretRevisionSqlDao.save(secondRevision));
Optional<KmsSecretRevision> fromSql =
jpaTm().transact(() -> KmsSecretRevisionSqlDao.getLatestRevision("secretName"));
assertThat(fromSql.isPresent()).isTrue();
assertThat(fromSql.get().getEncryptedValue()).isEqualTo("someOtherValue");
}
@Test
void testNonexistent() {
KmsSecretRevision revision = createRevision();
jpaTm().transact(() -> KmsSecretRevisionSqlDao.save(revision));
assertThat(
jpaTm()
.transact(() -> KmsSecretRevisionSqlDao.getLatestRevision("someOtherSecretName"))
.isPresent())
.isFalse();
}
private KmsSecretRevision createRevision() {
return new KmsSecretRevision.Builder()
.setEncryptedValue("encrypted")
.setKmsCryptoKeyVersionName("version")
.setParent("secretName")
.build();
}
}
@@ -0,0 +1,104 @@
// 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.smd;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.ImmutableObjectSubject.assertAboutImmutableObjects;
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.DualDatabaseTest;
import google.registry.testing.FakeClock;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
@DualDatabaseTest
public class SignedMarkRevocationListDaoTest {
private final FakeClock fakeClock = new FakeClock();
@RegisterExtension
final JpaIntegrationWithCoverageExtension jpa =
new JpaTestRules.Builder().withClock(fakeClock).buildIntegrationWithCoverageExtension();
@RegisterExtension
@Order(value = 1)
final DatastoreEntityExtension datastoreEntityExtension = new DatastoreEntityExtension();
@Test
void testSave_success() {
SignedMarkRevocationList list =
SignedMarkRevocationList.create(
fakeClock.nowUtc(), ImmutableMap.of("mark", fakeClock.nowUtc().minusHours(1)));
SignedMarkRevocationListDao.trySave(list);
SignedMarkRevocationList fromDb = SignedMarkRevocationListDao.getLatestRevision().get();
assertAboutImmutableObjects().that(fromDb).isEqualExceptFields(list);
}
@Test
void trySave_failureIsSwallowed() {
SignedMarkRevocationList list =
SignedMarkRevocationList.create(
fakeClock.nowUtc(), ImmutableMap.of("mark", fakeClock.nowUtc().minusHours(1)));
SignedMarkRevocationListDao.trySave(list);
SignedMarkRevocationList fromDb = SignedMarkRevocationListDao.getLatestRevision().get();
assertAboutImmutableObjects().that(fromDb).isEqualExceptFields(list);
// This should throw an exception, which is swallowed and nothing changed
SignedMarkRevocationListDao.trySave(list);
SignedMarkRevocationList secondFromDb = SignedMarkRevocationListDao.getLatestRevision().get();
assertAboutImmutableObjects().that(secondFromDb).isEqualExceptFields(fromDb);
}
@Test
void testRetrieval_notPresent() {
assertThat(SignedMarkRevocationListDao.getLatestRevision().isPresent()).isFalse();
}
@Test
void testSaveAndRetrieval_emptyList() {
SignedMarkRevocationList list =
SignedMarkRevocationList.create(fakeClock.nowUtc(), ImmutableMap.of());
SignedMarkRevocationListDao.trySave(list);
SignedMarkRevocationList fromDb = SignedMarkRevocationListDao.getLatestRevision().get();
assertAboutImmutableObjects().that(fromDb).isEqualExceptFields(list);
}
@Test
void testSave_multipleVersions() {
SignedMarkRevocationList list =
SignedMarkRevocationList.create(
fakeClock.nowUtc(), ImmutableMap.of("mark", fakeClock.nowUtc().minusHours(1)));
SignedMarkRevocationListDao.trySave(list);
assertThat(
SignedMarkRevocationListDao.getLatestRevision()
.get()
.isSmdRevoked("mark", fakeClock.nowUtc()))
.isTrue();
// Now remove the revocation
SignedMarkRevocationList secondList =
SignedMarkRevocationList.create(fakeClock.nowUtc(), ImmutableMap.of());
SignedMarkRevocationListDao.trySave(secondList);
assertThat(
SignedMarkRevocationListDao.getLatestRevision()
.get()
.isSmdRevoked("mark", fakeClock.nowUtc()))
.isFalse();
}
}
@@ -15,6 +15,7 @@
package google.registry.model.smd;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.ImmutableObjectSubject.assertAboutImmutableObjects;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.model.smd.SignedMarkRevocationList.SHARD_SIZE;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
@@ -72,10 +73,11 @@ public class SignedMarkRevocationListTest {
revokes.put(Integer.toString(i), clock.nowUtc());
}
// Save it with sharding, and make sure that reloading it works.
SignedMarkRevocationList unsharded = SignedMarkRevocationList
.create(clock.nowUtc(), revokes.build())
.save();
assertThat(SignedMarkRevocationList.get()).isEqualTo(unsharded);
SignedMarkRevocationList unsharded =
SignedMarkRevocationList.create(clock.nowUtc(), revokes.build()).save();
assertAboutImmutableObjects()
.that(SignedMarkRevocationList.get())
.isEqualExceptFields(unsharded, "revisionId");
assertThat(ofy().load().type(SignedMarkRevocationList.class).count()).isEqualTo(2);
}
@@ -91,7 +93,9 @@ public class SignedMarkRevocationListTest {
SignedMarkRevocationList unsharded = SignedMarkRevocationList
.create(clock.nowUtc(), revokes.build())
.save();
assertThat(SignedMarkRevocationList.get()).isEqualTo(unsharded);
assertAboutImmutableObjects()
.that(SignedMarkRevocationList.get())
.isEqualExceptFields(unsharded, "revisionId");
assertThat(ofy().load().type(SignedMarkRevocationList.class).count()).isEqualTo(4);
}
@@ -33,6 +33,8 @@ import google.registry.testing.AppEngineExtension;
import google.registry.testing.DualDatabaseTest;
import google.registry.testing.FakeClock;
import google.registry.testing.InjectExtension;
import google.registry.testing.TestOfyAndSql;
import google.registry.testing.TestOfyOnly;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Set;
@@ -40,7 +42,6 @@ import java.util.stream.Stream;
import javax.persistence.Embeddable;
import javax.persistence.MappedSuperclass;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.TestTemplate;
import org.junit.jupiter.api.extension.RegisterExtension;
/**
@@ -78,28 +79,28 @@ public class TransactionManagerTest {
fakeClock.advanceOneMilli();
}
@TestTemplate
@TestOfyAndSql
void inTransaction_returnsCorrespondingResult() {
assertThat(tm().inTransaction()).isFalse();
tm().transact(() -> assertThat(tm().inTransaction()).isTrue());
assertThat(tm().inTransaction()).isFalse();
}
@TestTemplate
@TestOfyAndSql
void assertInTransaction_throwsExceptionWhenNotInTransaction() {
assertThrows(IllegalStateException.class, () -> tm().assertInTransaction());
tm().transact(() -> tm().assertInTransaction());
assertThrows(IllegalStateException.class, () -> tm().assertInTransaction());
}
@TestTemplate
@TestOfyAndSql
void getTransactionTime_throwsExceptionWhenNotInTransaction() {
assertThrows(IllegalStateException.class, () -> tm().getTransactionTime());
tm().transact(() -> assertThat(tm().getTransactionTime()).isEqualTo(fakeClock.nowUtc()));
assertThrows(IllegalStateException.class, () -> tm().getTransactionTime());
}
@TestTemplate
@TestOfyAndSql
void transact_hasNoEffectWithPartialSuccess() {
assertEntityNotExist(theEntity);
assertThrows(
@@ -113,21 +114,21 @@ public class TransactionManagerTest {
assertEntityNotExist(theEntity);
}
@TestTemplate
@TestOfyAndSql
void transact_reusesExistingTransaction() {
assertEntityNotExist(theEntity);
tm().transact(() -> tm().transact(() -> tm().insert(theEntity)));
assertEntityExists(theEntity);
}
@TestTemplate
@TestOfyAndSql
void transactNew_succeeds() {
assertEntityNotExist(theEntity);
tm().transactNew(() -> tm().insert(theEntity));
assertEntityExists(theEntity);
}
@TestTemplate
@TestOfyAndSql
void transactNewReadOnly_succeeds() {
assertEntityNotExist(theEntity);
tm().transact(() -> tm().insert(theEntity));
@@ -136,7 +137,7 @@ public class TransactionManagerTest {
assertThat(persisted).isEqualTo(theEntity);
}
@TestTemplate
@TestOfyAndSql
void transactNewReadOnly_throwsWhenWritingEntity() {
assertEntityNotExist(theEntity);
assertThrows(
@@ -144,7 +145,7 @@ public class TransactionManagerTest {
assertEntityNotExist(theEntity);
}
@TestTemplate
@TestOfyAndSql
void saveNew_succeeds() {
assertEntityNotExist(theEntity);
tm().transact(() -> tm().insert(theEntity));
@@ -152,14 +153,14 @@ public class TransactionManagerTest {
assertThat(tm().transact(() -> tm().load(theEntity.key()))).isEqualTo(theEntity);
}
@TestTemplate
@TestOfyAndSql
void saveAllNew_succeeds() {
assertAllEntitiesNotExist(moreEntities);
tm().transact(() -> tm().insertAll(moreEntities));
assertAllEntitiesExist(moreEntities);
}
@TestTemplate
@TestOfyAndSql
void saveNewOrUpdate_persistsNewEntity() {
assertEntityNotExist(theEntity);
tm().transact(() -> tm().put(theEntity));
@@ -167,7 +168,7 @@ public class TransactionManagerTest {
assertThat(tm().transact(() -> tm().load(theEntity.key()))).isEqualTo(theEntity);
}
@TestTemplate
@TestOfyAndSql
void saveNewOrUpdate_updatesExistingEntity() {
tm().transact(() -> tm().insert(theEntity));
TestEntity persisted = tm().transact(() -> tm().load(theEntity.key()));
@@ -179,14 +180,14 @@ public class TransactionManagerTest {
assertThat(persisted.data).isEqualTo("bar");
}
@TestTemplate
@TestOfyAndSql
void saveNewOrUpdateAll_succeeds() {
assertAllEntitiesNotExist(moreEntities);
tm().transact(() -> tm().putAll(moreEntities));
assertAllEntitiesExist(moreEntities);
}
@TestTemplate
@TestOfyAndSql
void update_succeeds() {
tm().transact(() -> tm().insert(theEntity));
TestEntity persisted =
@@ -202,7 +203,7 @@ public class TransactionManagerTest {
assertThat(persisted.data).isEqualTo("bar");
}
@TestTemplate
@TestOfyAndSql
void load_succeeds() {
assertEntityNotExist(theEntity);
tm().transact(() -> tm().insert(theEntity));
@@ -211,14 +212,14 @@ public class TransactionManagerTest {
assertThat(persisted.data).isEqualTo("foo");
}
@TestTemplate
@TestOfyAndSql
void load_throwsOnMissingElement() {
assertEntityNotExist(theEntity);
assertThrows(
NoSuchElementException.class, () -> tm().transact(() -> tm().load(theEntity.key())));
}
@TestTemplate
@TestOfyAndSql
void maybeLoad_succeeds() {
assertEntityNotExist(theEntity);
tm().transact(() -> tm().insert(theEntity));
@@ -227,13 +228,13 @@ public class TransactionManagerTest {
assertThat(persisted.data).isEqualTo("foo");
}
@TestTemplate
@TestOfyAndSql
void maybeLoad_nonExistentObject() {
assertEntityNotExist(theEntity);
assertThat(tm().transact(() -> tm().maybeLoad(theEntity.key())).isPresent()).isFalse();
}
@TestTemplate
@TestOfyAndSql
void delete_succeeds() {
tm().transact(() -> tm().insert(theEntity));
assertEntityExists(theEntity);
@@ -242,14 +243,14 @@ public class TransactionManagerTest {
assertEntityNotExist(theEntity);
}
@TestTemplate
@TestOfyAndSql
void delete_doNothingWhenEntityNotExist() {
assertEntityNotExist(theEntity);
tm().transact(() -> tm().delete(theEntity.key()));
assertEntityNotExist(theEntity);
}
@TestTemplate
@TestOfyAndSql
void delete_succeedsForEntitySet() {
assertAllEntitiesNotExist(moreEntities);
tm().transact(() -> tm().insertAll(moreEntities));
@@ -261,7 +262,7 @@ public class TransactionManagerTest {
assertAllEntitiesNotExist(moreEntities);
}
@TestTemplate
@TestOfyAndSql
void delete_ignoreNonExistentEntity() {
assertAllEntitiesNotExist(moreEntities);
tm().transact(() -> tm().insertAll(moreEntities));
@@ -276,7 +277,16 @@ public class TransactionManagerTest {
assertAllEntitiesNotExist(moreEntities);
}
@TestTemplate
@TestOfyAndSql
void delete_deletesTheGivenEntity() {
tm().transact(() -> tm().insert(theEntity));
assertEntityExists(theEntity);
fakeClock.advanceOneMilli();
tm().transact(() -> tm().delete(theEntity));
assertEntityNotExist(theEntity);
}
@TestOfyAndSql
void load_multi() {
assertAllEntitiesNotExist(moreEntities);
tm().transact(() -> tm().insertAll(moreEntities));
@@ -286,7 +296,7 @@ public class TransactionManagerTest {
.isEqualTo(Maps.uniqueIndex(moreEntities, TestEntity::key));
}
@TestTemplate
@TestOfyAndSql
void load_multiWithDuplicateKeys() {
assertAllEntitiesNotExist(moreEntities);
tm().transact(() -> tm().insertAll(moreEntities));
@@ -298,7 +308,7 @@ public class TransactionManagerTest {
.isEqualTo(Maps.uniqueIndex(moreEntities, TestEntity::key));
}
@TestTemplate
@TestOfyAndSql
void load_multiMissingKeys() {
assertAllEntitiesNotExist(moreEntities);
tm().transact(() -> tm().insertAll(moreEntities));
@@ -310,6 +320,14 @@ public class TransactionManagerTest {
.isEqualTo(Maps.uniqueIndex(moreEntities, TestEntity::key));
}
@TestOfyOnly
void loadAllForOfyTm_throwsExceptionInTransaction() {
assertAllEntitiesNotExist(moreEntities);
tm().transact(() -> tm().insertAll(moreEntities));
assertThrows(
IllegalArgumentException.class, () -> tm().transact(() -> tm().loadAll(TestEntity.class)));
}
private static void assertEntityExists(TestEntity entity) {
assertThat(tm().transact(() -> tm().exists(entity))).isTrue();
}
@@ -29,6 +29,8 @@ import google.registry.model.registry.RegistryLockDaoTest;
import google.registry.model.registry.RegistryTest;
import google.registry.model.registry.label.ReservedListSqlDaoTest;
import google.registry.model.reporting.Spec11ThreatMatchTest;
import google.registry.model.server.KmsSecretRevisionSqlDaoTest;
import google.registry.model.smd.SignedMarkRevocationListDaoTest;
import google.registry.model.tmch.ClaimsListDaoTest;
import google.registry.persistence.transaction.JpaEntityCoverageExtension;
import google.registry.persistence.transaction.JpaTestRules.JpaIntegrationWithCoverageExtension;
@@ -84,6 +86,7 @@ import org.junit.runner.RunWith;
DomainBaseSqlTest.class,
DomainHistoryTest.class,
HostHistoryTest.class,
KmsSecretRevisionSqlDaoTest.class,
LockDaoTest.class,
PollMessageTest.class,
PremiumListDaoTest.class,
@@ -92,6 +95,7 @@ import org.junit.runner.RunWith;
RegistryTest.class,
ReservedListSqlDaoTest.class,
RegistryLockDaoTest.class,
SignedMarkRevocationListDaoTest.class,
Spec11ThreatMatchTest.class,
// AfterSuiteTest must be the last entry. See class javadoc for details.
AfterSuiteTest.class
@@ -61,7 +61,7 @@ public enum Fixture {
createTlds("xn--q9jyb4c", "example");
// Used for OT&E TLDs
persistPremiumList("default_sandbox_list");
persistPremiumList("default_sandbox_list", "sandbox,USD 1000");
try {
OteStatsTestHelper.setupCompleteOte("otefinished");
@@ -49,7 +49,7 @@ abstract class AbstractEppResourceSubject<
this.actual = subject;
}
private List<HistoryEntry> getHistoryEntries() {
private List<? extends HistoryEntry> getHistoryEntries() {
return DatastoreHelper.getHistoryEntries(actual);
}
@@ -120,7 +120,7 @@ abstract class AbstractEppResourceSubject<
// TODO(weiminyu): Remove after next Truth update
@SuppressWarnings("UnnecessaryParentheses")
public Which<HistoryEntrySubject> hasHistoryEntryAtIndex(int index) {
List<HistoryEntry> historyEntries = getHistoryEntries();
List<? extends HistoryEntry> historyEntries = getHistoryEntries();
check("getHistoryEntries().size()").that(historyEntries.size()).isAtLeast(index + 1);
return new Which<>(
check("getHistoryEntries(%s)", index)
@@ -17,6 +17,7 @@ package google.registry.testing;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.io.Files.asCharSink;
import static com.google.common.truth.Truth.assertWithMessage;
import static google.registry.persistence.transaction.TransactionManagerUtil.ofyOrJpaTm;
import static google.registry.testing.DatastoreHelper.persistSimpleResources;
import static google.registry.testing.DualDatabaseTestInvocationContextProvider.injectTmForDualDatabaseTest;
import static google.registry.testing.DualDatabaseTestInvocationContextProvider.restoreTmAfterDualDatabaseTest;
@@ -384,6 +385,17 @@ public final class AppEngineExtension implements BeforeEachCallback, AfterEachCa
if (isWithDatastoreAndCloudSql()) {
injectTmForDualDatabaseTest(context);
}
ofyOrJpaTm(
() -> {
if (withDatastore && !withoutCannedData) {
loadInitialData();
}
},
() -> {
if (withCloudSql && !withJpaUnitTest && !withoutCannedData) {
loadInitialData();
}
});
}
/**
@@ -458,9 +470,6 @@ public final class AppEngineExtension implements BeforeEachCallback, AfterEachCa
ObjectifyService.initOfy();
// Reset id allocation in ObjectifyService so that ids are deterministic in tests.
ObjectifyService.resetNextTestId();
if (!withoutCannedData) {
loadInitialData();
}
this.ofyTestEntities.forEach(AppEngineExtension::register);
}
}
@@ -18,6 +18,7 @@ import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.base.Suppliers.memoize;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static com.google.common.collect.Iterables.toArray;
import static com.google.common.collect.MoreCollectors.onlyElement;
import static com.google.common.truth.Truth.assertThat;
@@ -33,6 +34,9 @@ import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.model.registry.Registry.TldState.GENERAL_AVAILABILITY;
import static google.registry.model.registry.label.PremiumListUtils.parentPremiumListEntriesOnRevision;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.persistence.transaction.TransactionManagerUtil.ofyOrJpaTm;
import static google.registry.persistence.transaction.TransactionManagerUtil.ofyTmOrDoNothing;
import static google.registry.persistence.transaction.TransactionManagerUtil.transactIfJpaTm;
import static google.registry.pricing.PricingEngineProxy.getDomainRenewCost;
import static google.registry.util.CollectionUtils.difference;
import static google.registry.util.CollectionUtils.union;
@@ -44,6 +48,7 @@ import static google.registry.util.PreconditionsUtils.checkArgumentPresent;
import static google.registry.util.ResourceUtils.readResourceUtf8;
import static java.util.Arrays.asList;
import static org.joda.money.CurrencyUnit.USD;
import static org.junit.jupiter.api.Assertions.fail;
import com.google.common.base.Ascii;
import com.google.common.base.Splitter;
@@ -60,22 +65,27 @@ import google.registry.dns.writer.VoidDnsWriter;
import google.registry.model.Buildable;
import google.registry.model.EppResource;
import google.registry.model.EppResource.ForeignKeyedEppResource;
import google.registry.model.ImmutableObject;
import google.registry.model.billing.BillingEvent;
import google.registry.model.billing.BillingEvent.Flag;
import google.registry.model.billing.BillingEvent.Reason;
import google.registry.model.contact.ContactAuthInfo;
import google.registry.model.contact.ContactBase;
import google.registry.model.contact.ContactHistory;
import google.registry.model.contact.ContactResource;
import google.registry.model.domain.DesignatedContact;
import google.registry.model.domain.DesignatedContact.Type;
import google.registry.model.domain.DomainAuthInfo;
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.rgp.GracePeriodStatus;
import google.registry.model.domain.token.AllocationToken;
import google.registry.model.eppcommon.AuthInfo.PasswordAuth;
import google.registry.model.eppcommon.StatusValue;
import google.registry.model.eppcommon.Trid;
import google.registry.model.host.HostBase;
import google.registry.model.host.HostHistory;
import google.registry.model.host.HostResource;
import google.registry.model.index.EppResourceIndex;
import google.registry.model.index.EppResourceIndexBucket;
@@ -101,10 +111,14 @@ import google.registry.persistence.VKey;
import google.registry.tmch.LordnTaskUtils;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;
import org.joda.money.CurrencyUnit;
import org.joda.money.Money;
import org.joda.time.DateTime;
import org.joda.time.DateTimeComparator;
import org.joda.time.DateTimeZone;
/** Static utils for setting up test resources. */
public class DatastoreHelper {
@@ -325,18 +339,40 @@ public class DatastoreHelper {
* the requirement to have monotonically increasing timestamps.
*/
public static PremiumList persistPremiumList(String listName, String... lines) {
PremiumList premiumList = new PremiumList.Builder().setName(listName).build();
ImmutableMap<String, PremiumListEntry> entries = premiumList.parse(asList(lines));
checkState(lines.length != 0, "Must provide at least one premium entry");
PremiumList partialPremiumList = new PremiumList.Builder().setName(listName).build();
ImmutableMap<String, PremiumListEntry> entries = partialPremiumList.parse(asList(lines));
CurrencyUnit currencyUnit =
entries.entrySet().iterator().next().getValue().getValue().getCurrencyUnit();
PremiumList premiumList =
partialPremiumList
.asBuilder()
.setCreationTime(DateTime.now(DateTimeZone.UTC))
.setCurrency(currencyUnit)
.setLabelsToPrices(
entries.entrySet().stream()
.collect(
toImmutableMap(
Map.Entry::getKey, entry -> entry.getValue().getValue().getAmount())))
.build();
PremiumListRevision revision = PremiumListRevision.create(premiumList, entries.keySet());
ofy()
.saveWithoutBackup()
.entities(premiumList.asBuilder().setRevision(Key.create(revision)).build(), revision)
.now();
ofy()
.saveWithoutBackup()
.entities(parentPremiumListEntriesOnRevision(entries.values(), Key.create(revision)))
.now();
return ofy().load().entity(premiumList).now();
ofyOrJpaTm(
() -> {
tm().putAllWithoutBackup(
ImmutableList.of(
premiumList.asBuilder().setRevision(Key.create(revision)).build(), revision));
tm().putAllWithoutBackup(
parentPremiumListEntriesOnRevision(entries.values(), Key.create(revision)));
},
() -> tm().transact(() -> tm().insert(premiumList)));
// The above premiumList is in the session cache and it is different from the corresponding
// entity stored in Datastore because it has some @Ignore fields set dedicated for SQL. This
// breaks the assumption we have in our application code, see
// PremiumListUtils.savePremiumListAndEntries(). Clearing the session cache can help make sure
// we always get the same list.
tm().clearSessionCache();
return transactIfJpaTm(() -> tm().load(premiumList));
}
/** Creates and persists a tld. */
@@ -672,17 +708,28 @@ public class DatastoreHelper {
}
private static Iterable<BillingEvent> getBillingEvents() {
return Iterables.concat(
ofy().load().type(BillingEvent.OneTime.class),
ofy().load().type(BillingEvent.Recurring.class),
ofy().load().type(BillingEvent.Cancellation.class));
return transactIfJpaTm(
() ->
Iterables.concat(
tm().loadAll(BillingEvent.OneTime.class),
tm().loadAll(BillingEvent.Recurring.class),
tm().loadAll(BillingEvent.Cancellation.class)));
}
private static Iterable<BillingEvent> getBillingEvents(EppResource resource) {
return Iterables.concat(
ofy().load().type(BillingEvent.OneTime.class).ancestor(resource),
ofy().load().type(BillingEvent.Recurring.class).ancestor(resource),
ofy().load().type(BillingEvent.Cancellation.class).ancestor(resource));
return transactIfJpaTm(
() ->
Iterables.concat(
tm().loadAll(BillingEvent.OneTime.class).stream()
.filter(oneTime -> oneTime.getDomainRepoId().equals(resource.getRepoId()))
.collect(toImmutableList()),
tm().loadAll(BillingEvent.Recurring.class).stream()
.filter(recurring -> recurring.getDomainRepoId().equals(resource.getRepoId()))
.collect(toImmutableList()),
tm().loadAll(BillingEvent.Cancellation.class).stream()
.filter(
cancellation -> cancellation.getDomainRepoId().equals(resource.getRepoId()))
.collect(toImmutableList())));
}
/** Assert that the actual billing event matches the expected one, ignoring IDs. */
@@ -751,41 +798,63 @@ public class DatastoreHelper {
assertPollMessagesEqual(getPollMessages(), Arrays.asList(expected));
}
public static void assertPollMessagesForResource(EppResource resource, PollMessage... expected) {
assertPollMessagesEqual(getPollMessages(resource), Arrays.asList(expected));
public static void assertPollMessagesForResource(DomainContent domain, PollMessage... expected) {
assertPollMessagesEqual(getPollMessages(domain), Arrays.asList(expected));
}
public static ImmutableList<PollMessage> getPollMessages() {
return ImmutableList.copyOf(ofy().load().type(PollMessage.class));
return ImmutableList.copyOf(transactIfJpaTm(() -> tm().loadAll(PollMessage.class)));
}
public static ImmutableList<PollMessage> getPollMessages(String clientId) {
return ImmutableList.copyOf(ofy().load().type(PollMessage.class).filter("clientId", clientId));
return transactIfJpaTm(
() ->
tm().loadAll(PollMessage.class).stream()
.filter(pollMessage -> pollMessage.getClientId().equals(clientId))
.collect(toImmutableList()));
}
public static ImmutableList<PollMessage> getPollMessages(EppResource resource) {
return ImmutableList.copyOf(ofy().load().type(PollMessage.class).ancestor(resource));
public static ImmutableList<PollMessage> getPollMessages(DomainContent domain) {
return transactIfJpaTm(
() ->
tm().loadAll(PollMessage.class).stream()
.filter(
pollMessage ->
pollMessage.getParentKey().getParent().getName().equals(domain.getRepoId()))
.collect(toImmutableList()));
}
public static ImmutableList<PollMessage> getPollMessages(String clientId, DateTime now) {
return ImmutableList.copyOf(
ofy()
.load()
.type(PollMessage.class)
.filter("clientId", clientId)
.filter("eventTime <=", now.toDate()));
return transactIfJpaTm(
() ->
tm().loadAll(PollMessage.class).stream()
.filter(pollMessage -> pollMessage.getClientId().equals(clientId))
.filter(
pollMessage ->
pollMessage.getEventTime().isEqual(now)
|| pollMessage.getEventTime().isBefore(now))
.collect(toImmutableList()));
}
/** Gets all PollMessages associated with the given EppResource. */
public static ImmutableList<PollMessage> getPollMessages(
EppResource resource, String clientId, DateTime now) {
return ImmutableList.copyOf(
ofy()
.load()
.type(PollMessage.class)
.ancestor(resource)
.filter("clientId", clientId)
.filter("eventTime <=", now.toDate()));
return transactIfJpaTm(
() ->
tm().loadAll(PollMessage.class).stream()
.filter(
pollMessage ->
pollMessage
.getParentKey()
.getParent()
.getName()
.equals(resource.getRepoId()))
.filter(pollMessage -> pollMessage.getClientId().equals(clientId))
.filter(
pollMessage ->
pollMessage.getEventTime().isEqual(now)
|| pollMessage.getEventTime().isBefore(now))
.collect(toImmutableList()));
}
public static PollMessage getOnlyPollMessage(String clientId) {
@@ -808,19 +877,15 @@ public class DatastoreHelper {
}
public static PollMessage getOnlyPollMessage(
EppResource resource,
String clientId,
DateTime now,
Class<? extends PollMessage> subType) {
return getPollMessages(resource, clientId, now)
.stream()
DomainContent domain, String clientId, DateTime now, Class<? extends PollMessage> subType) {
return getPollMessages(domain, clientId, now).stream()
.filter(subType::isInstance)
.map(subType::cast)
.collect(onlyElement());
}
public static void assertAllocationTokens(AllocationToken... expectedTokens) {
assertThat(ofy().load().type(AllocationToken.class).list())
assertThat(transactIfJpaTm(() -> tm().loadAll(AllocationToken.class)))
.comparingElementsUsing(immutableObjectCorrespondence("updateTimestamp", "creationTime"))
.containsExactlyElementsIn(expectedTokens);
}
@@ -861,13 +926,17 @@ public class DatastoreHelper {
}
private static <R> void saveResource(R resource, boolean wantBackup) {
Saver saver = wantBackup ? ofy().save() : ofy().saveWithoutBackup();
saver.entity(resource);
if (resource instanceof EppResource) {
EppResource eppResource = (EppResource) resource;
persistEppResourceExtras(
eppResource, EppResourceIndex.create(Key.create(eppResource)), saver);
}
ofyOrJpaTm(
() -> {
Saver saver = wantBackup ? ofy().save() : ofy().saveWithoutBackup();
saver.entity(resource);
if (resource instanceof EppResource) {
EppResource eppResource = (EppResource) resource;
persistEppResourceExtras(
eppResource, EppResourceIndex.create(Key.create(eppResource)), saver);
}
},
() -> tm().put(resource));
}
private static <R extends EppResource> void persistEppResourceExtras(
@@ -890,23 +959,25 @@ public class DatastoreHelper {
// Datastore and not from the session cache. This is needed to trigger Objectify's load process
// (unmarshalling entity protos to POJOs, nulling out empty collections, calling @OnLoad
// methods, etc.) which is bypassed for entities loaded from the session cache.
ofy().clearSessionCache();
return ofy().load().entity(resource).now();
tm().clearSessionCache();
return transactIfJpaTm(() -> tm().load(resource));
}
/** Persists an EPP resource with the {@link EppResourceIndex} always going into bucket one. */
public static <R extends EppResource> R persistEppResourceInFirstBucket(final R resource) {
final EppResourceIndex eppResourceIndex =
EppResourceIndex.create(Key.create(EppResourceIndexBucket.class, 1), Key.create(resource));
tm()
.transact(
() -> {
Saver saver = ofy().save();
saver.entity(resource);
persistEppResourceExtras(resource, eppResourceIndex, saver);
});
ofy().clearSessionCache();
return ofy().load().entity(resource).now();
tm().transact(
() ->
ofyOrJpaTm(
() -> {
Saver saver = ofy().save();
saver.entity(resource);
persistEppResourceExtras(resource, eppResourceIndex, saver);
},
() -> tm().put(resource)));
tm().clearSessionCache();
return transactIfJpaTm(() -> tm().load(resource));
}
public static <R> void persistResources(final Iterable<R> resources) {
@@ -925,9 +996,9 @@ public class DatastoreHelper {
}
// Force the session to be cleared so that when we read it back, we read from Datastore
// and not from the transaction's session cache.
ofy().clearSessionCache();
tm().clearSessionCache();
for (R resource : resources) {
ofy().load().entity(resource).now();
transactIfJpaTm(() -> tm().load(resource));
}
}
@@ -943,31 +1014,57 @@ public class DatastoreHelper {
*/
public static <R extends EppResource> R persistEppResource(final R resource) {
checkState(!tm().inTransaction());
tm()
.transact(
tm().transact(
() -> {
ofy()
.save()
.<ImmutableObject>entities(
resource,
tm().put(resource);
tm().put(
new HistoryEntry.Builder()
.setParent(resource)
.setType(getHistoryEntryType(resource))
.setModificationTime(tm().getTransactionTime())
.build());
ofy().save().entity(ForeignKeyIndex.create(resource, resource.getDeletionTime()));
.build()
.toChildHistoryEntity());
ofyTmOrDoNothing(
() -> tm().put(ForeignKeyIndex.create(resource, resource.getDeletionTime())));
});
ofy().clearSessionCache();
return ofy().load().entity(resource).safe();
tm().clearSessionCache();
return transactIfJpaTm(() -> tm().load(resource));
}
/** Returns all of the history entries that are parented off the given EppResource. */
public static List<HistoryEntry> getHistoryEntries(EppResource resource) {
return ofy().load()
.type(HistoryEntry.class)
.ancestor(resource)
.order("modificationTime")
.list();
public static List<? extends HistoryEntry> getHistoryEntries(EppResource resource) {
return ofyOrJpaTm(
() ->
ofy()
.load()
.type(HistoryEntry.class)
.ancestor(resource)
.order("modificationTime")
.list(),
() ->
tm().transact(
() -> {
ImmutableList<? extends HistoryEntry> unsorted = null;
if (resource instanceof ContactBase) {
unsorted = tm().loadAll(ContactHistory.class);
} else if (resource instanceof HostBase) {
unsorted = tm().loadAll(HostHistory.class);
} else if (resource instanceof DomainContent) {
unsorted = tm().loadAll(DomainHistory.class);
} else {
fail("Expected an EppResource instance, but got " + resource.getClass());
}
ImmutableList<? extends HistoryEntry> filtered =
unsorted.stream()
.filter(
historyEntry ->
historyEntry
.getParent()
.getName()
.equals(resource.getRepoId()))
.collect(toImmutableList());
return ImmutableList.sortedCopyOf(DateTimeComparator.getInstance(), filtered);
}));
}
/**
@@ -1009,9 +1106,13 @@ public class DatastoreHelper {
}
public static PollMessage getOnlyPollMessageForHistoryEntry(HistoryEntry historyEntry) {
return Iterables.getOnlyElement(ofy().load()
.type(PollMessage.class)
.ancestor(historyEntry));
return Iterables.getOnlyElement(
transactIfJpaTm(
() ->
tm().loadAll(PollMessage.class).stream()
.filter(
pollMessage -> pollMessage.getParentKey().equals(Key.create(historyEntry)))
.collect(toImmutableList())));
}
public static <T extends EppResource> HistoryEntry createHistoryEntryForEppResource(
@@ -1029,23 +1130,30 @@ public class DatastoreHelper {
* ForeignKeyedEppResources.
*/
public static <R> ImmutableList<R> persistSimpleResources(final Iterable<R> resources) {
tm().transact(() -> ofy().saveWithoutBackup().entities(resources));
tm().transact(() -> tm().putAllWithoutBackup(ImmutableList.copyOf(resources)));
// Force the session to be cleared so that when we read it back, we read from Datastore
// and not from the transaction's session cache.
ofy().clearSessionCache();
return ImmutableList.copyOf(ofy().load().entities(resources).values());
tm().clearSessionCache();
return transactIfJpaTm(() -> tm().loadAll(resources));
}
public static void deleteResource(final Object resource) {
ofy().deleteWithoutBackup().entity(resource).now();
transactIfJpaTm(() -> tm().deleteWithoutBackup(resource));
// Force the session to be cleared so that when we read it back, we read from Datastore and
// not from the transaction's session cache.
ofy().clearSessionCache();
tm().clearSessionCache();
}
/** Force the create and update timestamps to get written into the resource. **/
public static <R> R cloneAndSetAutoTimestamps(final R resource) {
return tm().transact(() -> ofy().load().fromEntity(ofy().save().toEntity(resource)));
return tm().transact(
() ->
ofyOrJpaTm(
() -> ofy().load().fromEntity(ofy().save().toEntity(resource)),
() -> {
tm().put(resource);
return tm().load(resource);
}));
}
/** Returns the entire map of {@link PremiumListEntry}s for the given {@link PremiumList}. */
@@ -21,6 +21,7 @@ import com.google.common.collect.ImmutableList;
import google.registry.persistence.transaction.TransactionManager;
import google.registry.persistence.transaction.TransactionManagerFactory;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.List;
import java.util.function.Supplier;
import java.util.stream.Stream;
@@ -52,9 +53,21 @@ class DualDatabaseTestInvocationContextProvider implements TestTemplateInvocatio
@Override
public Stream<TestTemplateInvocationContext> provideTestTemplateInvocationContexts(
ExtensionContext context) {
return Stream.of(
createInvocationContext("Test Datastore", TransactionManagerFactory::ofyTm),
createInvocationContext("Test PostgreSQL", TransactionManagerFactory::jpaTm));
TestTemplateInvocationContext ofyContext =
createInvocationContext("Test Datastore", TransactionManagerFactory::ofyTm);
TestTemplateInvocationContext sqlContext =
createInvocationContext("Test PostgreSQL", TransactionManagerFactory::jpaTm);
Method testMethod = context.getTestMethod().orElseThrow(IllegalStateException::new);
if (testMethod.isAnnotationPresent(TestOfyAndSql.class)) {
return Stream.of(ofyContext, sqlContext);
} else if (testMethod.isAnnotationPresent(TestOfyOnly.class)) {
return Stream.of(ofyContext);
} else if (testMethod.isAnnotationPresent(TestSqlOnly.class)) {
return Stream.of(sqlContext);
} else {
throw new IllegalStateException(
"Test method must be annotated with @TestOfyAndSql, @TestOfyOnly or @TestSqlOnly");
}
}
private TestTemplateInvocationContext createInvocationContext(
@@ -104,6 +117,18 @@ class DualDatabaseTestInvocationContextProvider implements TestTemplateInvocatio
static void injectTmForDualDatabaseTest(ExtensionContext context) {
if (isDualDatabaseTest(context)) {
context
.getTestMethod()
.ifPresent(
testMethod -> {
if (!testMethod.isAnnotationPresent(TestOfyAndSql.class)
&& !testMethod.isAnnotationPresent(TestOfyOnly.class)
&& !testMethod.isAnnotationPresent(TestSqlOnly.class)) {
throw new IllegalStateException(
"Test method must be annotated with @TestOfyAndSql, @TestOfyOnly or"
+ " @TestSqlOnly");
}
});
context.getStore(NAMESPACE).put(ORIGINAL_TM_KEY, tm());
Supplier<? extends TransactionManager> tmSupplier =
(Supplier<? extends TransactionManager>)
@@ -19,37 +19,67 @@ import static google.registry.persistence.transaction.TransactionManagerFactory.
import google.registry.model.ofy.DatastoreTransactionManager;
import google.registry.persistence.transaction.JpaTransactionManager;
import google.registry.persistence.transaction.TransactionManager;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.TestTemplate;
import org.junit.jupiter.api.extension.RegisterExtension;
/**
* Test to verify that {@link DualDatabaseTestInvocationContextProvider} extension executes {@link
* TestTemplate} test twice with different databases.
* Test to verify that {@link DualDatabaseTestInvocationContextProvider} extension executes tests
* with corresponding {@link TransactionManager}.
*/
@DualDatabaseTest
public class DualDatabaseTestInvocationContextProviderTest {
private static int datastoreTestCounter = 0;
private static int postgresqlTestCounter = 0;
private static int testBothDbsOfyCounter = 0;
private static int testBothDbsSqlCounter = 0;
private static int testOfyOnlyOfyCounter = 0;
private static int testOfyOnlySqlCounter = 0;
private static int testSqlOnlyOfyCounter = 0;
private static int testSqlOnlySqlCounter = 0;
@RegisterExtension
public final AppEngineExtension appEngine =
AppEngineExtension.builder().withDatastoreAndCloudSql().build();
@TestTemplate
void testToUseTransactionManager() {
@TestOfyAndSql
void testToVerifyBothOfyAndSqlTmAreUsed() {
if (tm() instanceof DatastoreTransactionManager) {
datastoreTestCounter++;
testBothDbsOfyCounter++;
}
if (tm() instanceof JpaTransactionManager) {
postgresqlTestCounter++;
testBothDbsSqlCounter++;
}
}
@TestOfyOnly
void testToVerifyOnlyOfyTmIsUsed() {
if (tm() instanceof DatastoreTransactionManager) {
testOfyOnlyOfyCounter++;
}
if (tm() instanceof JpaTransactionManager) {
testOfyOnlySqlCounter++;
}
}
@TestSqlOnly
void testToVerifyOnlySqlTmIsUsed() {
if (tm() instanceof DatastoreTransactionManager) {
testSqlOnlyOfyCounter++;
}
if (tm() instanceof JpaTransactionManager) {
testSqlOnlySqlCounter++;
}
}
@AfterAll
static void assertEachTransactionManagerIsUsed() {
assertThat(datastoreTestCounter).isEqualTo(1);
assertThat(postgresqlTestCounter).isEqualTo(1);
assertThat(testBothDbsOfyCounter).isEqualTo(1);
assertThat(testBothDbsSqlCounter).isEqualTo(1);
assertThat(testOfyOnlyOfyCounter).isEqualTo(1);
assertThat(testOfyOnlySqlCounter).isEqualTo(0);
assertThat(testSqlOnlyOfyCounter).isEqualTo(0);
assertThat(testSqlOnlySqlCounter).isEqualTo(1);
}
}
@@ -0,0 +1,31 @@
// 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.testing;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import org.junit.jupiter.api.TestTemplate;
/**
* Annotation to indicate a test method will be executed twice with Datastore and Postgresql
* respectively.
*/
@Target({METHOD})
@Retention(RUNTIME)
@TestTemplate
public @interface TestOfyAndSql {}
@@ -0,0 +1,28 @@
// 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.testing;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import org.junit.jupiter.api.TestTemplate;
/** Annotation to indicate a test method will be executed only with Datastore. */
@Target({METHOD})
@Retention(RUNTIME)
@TestTemplate
public @interface TestOfyOnly {}
@@ -0,0 +1,28 @@
// 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.testing;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import org.junit.jupiter.api.TestTemplate;
/** Annotation to indicate a test method will be executed only with Postgresql. */
@Target({METHOD})
@Retention(RUNTIME)
@TestTemplate
public @interface TestSqlOnly {}
@@ -37,8 +37,8 @@ import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.Range;
import com.google.common.net.MediaType;
import google.registry.flows.certs.CertificateChecker;
import google.registry.model.registrar.Registrar;
import google.registry.util.CertificateChecker;
import java.io.IOException;
import java.util.Optional;
import org.joda.money.CurrencyUnit;
@@ -108,7 +108,7 @@ class EppLifecycleToolsTest extends EppTestCase {
.atTime("2001-06-08T00:00:00Z")
.hasResponse("poll_response_unrenew.xml");
assertThatCommand("poll_ack.xml", ImmutableMap.of("ID", "1-8-TLD-17-18-2001"))
assertThatCommand("poll_ack.xml", ImmutableMap.of("ID", "1-8-TLD-23-24-2001"))
.atTime("2001-06-08T00:00:01Z")
.hasResponse("poll_ack_response_empty.xml");
@@ -129,7 +129,7 @@ class EppLifecycleToolsTest extends EppTestCase {
.hasResponse(
"poll_response_autorenew.xml",
ImmutableMap.of(
"ID", "1-8-TLD-17-20-2003",
"ID", "1-8-TLD-23-26-2003",
"QDATE", "2003-06-01T00:02:00Z",
"DOMAIN", "example.tld",
"EXDATE", "2004-06-01T00:02:00Z"));
@@ -34,12 +34,12 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;
import google.registry.flows.certs.CertificateChecker;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.Registrar.State;
import google.registry.model.registrar.Registrar.Type;
import google.registry.persistence.VKey;
import google.registry.testing.AppEngineExtension;
import google.registry.util.CertificateChecker;
import google.registry.util.CidrAddressBlock;
import java.util.Optional;
import org.joda.money.CurrencyUnit;
@@ -34,6 +34,7 @@ import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.truth.Truth;
import google.registry.flows.certs.CertificateChecker;
import google.registry.model.ofy.Ofy;
import google.registry.model.registrar.RegistrarContact;
import google.registry.request.JsonActionRunner;
@@ -48,7 +49,6 @@ import google.registry.testing.FakeClock;
import google.registry.testing.InjectExtension;
import google.registry.ui.server.SendEmailUtils;
import google.registry.util.AppEngineServiceUtils;
import google.registry.util.CertificateChecker;
import google.registry.util.EmailMessage;
import google.registry.util.SendEmailService;
import java.io.PrintWriter;
@@ -3,7 +3,7 @@
<result code="1301">
<msg>Command completed successfully; ack to dequeue</msg>
</result>
<msgQ count="1" id="1-C-EXAMPLE-17-23-2001">
<msgQ count="1" id="%ID%">
<qDate>2001-01-01T00:00:00Z</qDate>
<msg>Transfer requested.</msg>
</msgQ>
@@ -3,7 +3,7 @@
<result code="1301">
<msg>Command completed successfully; ack to dequeue</msg>
</result>
<msgQ count="1" id="1-C-EXAMPLE-17-22-2001">
<msgQ count="1" id="%ID%">
<qDate>2001-01-06T00:00:00Z</qDate>
<msg>Transfer approved.</msg>
</msgQ>
@@ -4,7 +4,7 @@
<result code="1301">
<msg>Command completed successfully; ack to dequeue</msg>
</result>
<msgQ count="1" id="1-C-EXAMPLE-17-21-2001">
<msgQ count="1" id="%ID%">
<qDate>2001-01-06T00:00:00Z</qDate>
<msg>Transfer approved.</msg>
</msgQ>
@@ -3,7 +3,7 @@
<result code="1301">
<msg>Command completed successfully; ack to dequeue</msg>
</result>
<msgQ count="1" id="1-8-TLD-17-18-2001">
<msgQ count="1" id="1-8-TLD-23-24-2001">
<qDate>2001-06-07T00:00:00Z</qDate>
<msg>Domain example.tld was unrenewed by 3 years; now expires at 2003-06-01T00:02:00.000Z.</msg>
</msgQ>
@@ -273,6 +273,7 @@ class google.registry.model.domain.DomainHistory {
java.lang.String clientId;
java.lang.String otherClientId;
java.lang.String reason;
java.util.Set<google.registry.model.domain.secdns.DomainDsDataHistory> dsDataHistories;
java.util.Set<google.registry.model.reporting.DomainTransactionRecord> domainTransactionRecords;
org.joda.time.DateTime modificationTime;
}
@@ -315,6 +316,14 @@ class google.registry.model.domain.secdns.DelegationSignerData {
int digestType;
int keyTag;
}
class google.registry.model.domain.secdns.DomainDsDataHistory {
byte[] digest;
int algorithm;
int digestType;
int keyTag;
java.lang.Long domainHistoryRevisionId;
java.lang.Long dsDataHistoryRevisionId;
}
class google.registry.model.domain.token.AllocationToken {
@Id java.lang.String token;
boolean discountPremiums;
+11
View File
@@ -3,6 +3,17 @@
This project contains Nomulus's Cloud SQL schema and schema-deployment
utilities.
### ER Diagrams
The following links are the ER diagrams generated from the current SQL schema:
* [Full ER diagram](https://storage.googleapis.com/domain-registry-dev-er-diagram/full_er_diagram.html):
shows all columns, foreign keys and indexes.
* [Brief ER diagram](https://storage.googleapis.com/domain-registry-dev-er-diagram/brief_er_diagram.html):
shows only significant columns, such as primary and foreign key columns, and
columns that are part of unique indexes.
### Database Roles and Privileges
Nomulus uses the 'postgres' database in the 'public' schema. The following
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+5
View File
@@ -64,3 +64,8 @@ V63__add_schema_for_ds_data.sql
V64__transfer_history_columns.sql
V65__local_date_date_type.sql
V66__create_rde_revision.sql
V67__grace_period_history_ids.sql
V68__make_reserved_list_nullable_in_registry.sql
V69__change_primary_key_and_add_history_table_for_delegation_signer.sql
V70__signed_mark_revocation_list.sql
V71__create_kms_secret.sql
@@ -0,0 +1,26 @@
-- 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.
ALTER TABLE "GracePeriod" ADD COLUMN billing_event_history_id int8;
ALTER TABLE "GracePeriod" ADD COLUMN billing_recurrence_history_id int8;
ALTER TABLE ONLY public."GracePeriod"
DROP CONSTRAINT fk2mys4hojm6ev2g9tmy5aq6m7g;
ALTER TABLE ONLY public."GracePeriod"
ADD CONSTRAINT fk_grace_period_domain_repo_id
FOREIGN KEY (domain_repo_id) REFERENCES public."Domain"(repo_id)
DEFERRABLE INITIALLY DEFERRED;
ALTER TABLE "GracePeriod" ALTER COLUMN id drop default;
DROP SEQUENCE "GracePeriod_id_seq";
@@ -0,0 +1,15 @@
-- 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.
ALTER TABLE "Tld" ALTER COLUMN reserved_list_names DROP NOT NULL;
@@ -0,0 +1,35 @@
-- 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.
alter table "DelegationSignerData" drop constraint "DelegationSignerData_pkey";
alter table "DelegationSignerData"
add constraint "DelegationSignerData_pkey"
primary key (domain_repo_id, key_tag, algorithm, digest_type, digest);
create table "DomainDsDataHistory" (
ds_data_history_revision_id int8 not null,
algorithm int4 not null,
digest bytea not null,
digest_type int4 not null,
domain_history_revision_id int8 not null,
key_tag int4 not null,
domain_repo_id text,
primary key (ds_data_history_revision_id)
);
alter table if exists "DomainDsDataHistory"
add constraint FKo4ilgyyfnvppbpuivus565i0j
foreign key (domain_repo_id, domain_history_revision_id)
references "DomainHistory";
@@ -0,0 +1,31 @@
-- 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.
create table "SignedMarkRevocationEntry" (
revision_id int8 not null,
revocation_time timestamptz not null,
smd_id text not null,
primary key (revision_id, smd_id)
);
create table "SignedMarkRevocationList" (
revision_id bigserial not null,
creation_time timestamptz,
primary key (revision_id)
);
alter table if exists "SignedMarkRevocationEntry"
add constraint FK5ivlhvs3121yx2li5tqh54u4
foreign key (revision_id)
references "SignedMarkRevocationList";
@@ -0,0 +1,24 @@
-- 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.
CREATE TABLE "KmsSecret" (
revision_id int8 NOT NULL,
creation_time timestamptz NOT NULL,
encrypted_value text NOT NULL,
crypto_key_version_name text NOT NULL,
secret_name text NOT NULL,
PRIMARY KEY (revision_id)
);
CREATE INDEX IDXli9nil3s4t4p21i3xluvvilb7 ON "KmsSecret" (secret_name);
@@ -227,12 +227,12 @@
);
create table "DelegationSignerData" (
domain_repo_id text not null,
key_tag int4 not null,
algorithm int4 not null,
algorithm int4 not null,
digest bytea not null,
digest_type int4 not null,
primary key (domain_repo_id, key_tag)
domain_repo_id text not null,
key_tag int4 not null,
primary key (algorithm, digest, digest_type, domain_repo_id, key_tag)
);
create table "Domain" (
@@ -291,6 +291,17 @@
primary key (repo_id)
);
create table "DomainDsDataHistory" (
ds_data_history_revision_id int8 not null,
algorithm int4 not null,
digest bytea not null,
digest_type int4 not null,
domain_history_revision_id int8 not null,
domain_repo_id text,
key_tag int4 not null,
primary key (ds_data_history_revision_id)
);
create table "DomainHistory" (
domain_repo_id text not null,
history_revision_id int8 not null,
@@ -383,9 +394,11 @@
);
create table "GracePeriod" (
id bigserial not null,
id int8 not null,
billing_event_id int8,
billing_event_history_id int8,
billing_recurrence_id int8,
billing_recurrence_history_id int8,
registrar_id text not null,
domain_repo_id text not null,
expiration_time timestamptz not null,
@@ -439,6 +452,15 @@
primary key (host_repo_id, history_revision_id)
);
create table "KmsSecret" (
revision_id int8 not null,
creation_time timestamptz not null,
encrypted_value text not null,
crypto_key_version_name text not null,
secret_name text not null,
primary key (revision_id)
);
create table "Lock" (
resource_name text not null,
tld text not null,
@@ -604,6 +626,19 @@
primary key (revision_id)
);
create table "SignedMarkRevocationEntry" (
revision_id int8 not null,
revocation_time timestamptz not null,
smd_id text not null,
primary key (revision_id, smd_id)
);
create table "SignedMarkRevocationList" (
revision_id bigserial not null,
creation_time timestamptz,
primary key (revision_id)
);
create table "Spec11ThreatMatch" (
id bigserial not null,
check_date date not null,
@@ -644,7 +679,7 @@
registry_lock_or_unlock_cost_currency text,
renew_billing_cost_transitions hstore not null,
renew_grace_period_length interval not null,
reserved_list_names text[] not null,
reserved_list_names text[],
restore_billing_cost_amount numeric(19, 2),
restore_billing_cost_currency text,
roid_suffix text,
@@ -701,6 +736,7 @@ create index IDX1iy7njgb7wjmj9piml4l2g0qi on "HostHistory" (history_registrar_id
create index IDXkkwbwcwvrdkkqothkiye4jiff on "HostHistory" (host_name);
create index IDXknk8gmj7s47q56cwpa6rmpt5l on "HostHistory" (history_type);
create index IDX67qwkjtlq5q8dv6egtrtnhqi7 on "HostHistory" (history_modification_time);
create index IDXli9nil3s4t4p21i3xluvvilb7 on "KmsSecret" (secret_name);
create index IDXe7wu46c7wpvfmfnj4565abibp on "PollMessage" (registrar_id);
create index IDXaydgox62uno9qx8cjlj5lauye on "PollMessage" (event_time);
create index premiumlist_name_idx on "PremiumList" (name);
@@ -727,6 +763,11 @@ create index spec11threatmatch_check_date_idx on "Spec11ThreatMatch" (check_date
foreign key (domain_repo_id)
references "Domain";
alter table if exists "DomainDsDataHistory"
add constraint FKo4ilgyyfnvppbpuivus565i0j
foreign key (domain_repo_id, domain_history_revision_id)
references "DomainHistory";
alter table if exists "DomainHistoryHost"
add constraint FKa9woh3hu8gx5x0vly6bai327n
foreign key (domain_history_domain_repo_id, domain_history_history_revision_id)
@@ -761,3 +802,8 @@ create index spec11threatmatch_check_date_idx on "Spec11ThreatMatch" (check_date
add constraint FKgq03rk0bt1hb915dnyvd3vnfc
foreign key (revision_id)
references "ReservedList";
alter table if exists "SignedMarkRevocationEntry"
add constraint FK5ivlhvs3121yx2li5tqh54u4
foreign key (revision_id)
references "SignedMarkRevocationList";
@@ -376,6 +376,21 @@ CREATE TABLE public."Domain" (
);
--
-- Name: DomainDsDataHistory; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public."DomainDsDataHistory" (
ds_data_history_revision_id bigint NOT NULL,
algorithm integer NOT NULL,
digest bytea NOT NULL,
digest_type integer NOT NULL,
domain_history_revision_id bigint NOT NULL,
key_tag integer NOT NULL,
domain_repo_id text
);
--
-- Name: DomainHistory; Type: TABLE; Schema: public; Owner: -
--
@@ -515,29 +530,12 @@ CREATE TABLE public."GracePeriod" (
registrar_id text NOT NULL,
domain_repo_id text NOT NULL,
expiration_time timestamp with time zone NOT NULL,
type text NOT NULL
type text NOT NULL,
billing_event_history_id bigint,
billing_recurrence_history_id bigint
);
--
-- Name: GracePeriod_id_seq; Type: SEQUENCE; Schema: public; Owner: -
--
CREATE SEQUENCE public."GracePeriod_id_seq"
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--
-- Name: GracePeriod_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
--
ALTER SEQUENCE public."GracePeriod_id_seq" OWNED BY public."GracePeriod".id;
--
-- Name: Host; Type: TABLE; Schema: public; Owner: -
--
@@ -592,6 +590,19 @@ CREATE TABLE public."HostHistory" (
);
--
-- Name: KmsSecret; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public."KmsSecret" (
revision_id bigint NOT NULL,
creation_time timestamp with time zone NOT NULL,
encrypted_value text NOT NULL,
crypto_key_version_name text NOT NULL,
secret_name text NOT NULL
);
--
-- Name: Lock; Type: TABLE; Schema: public; Owner: -
--
@@ -888,6 +899,46 @@ CREATE SEQUENCE public."SafeBrowsingThreat_id_seq"
ALTER SEQUENCE public."SafeBrowsingThreat_id_seq" OWNED BY public."Spec11ThreatMatch".id;
--
-- Name: SignedMarkRevocationEntry; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public."SignedMarkRevocationEntry" (
revision_id bigint NOT NULL,
revocation_time timestamp with time zone NOT NULL,
smd_id text NOT NULL
);
--
-- Name: SignedMarkRevocationList; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public."SignedMarkRevocationList" (
revision_id bigint NOT NULL,
creation_time timestamp with time zone
);
--
-- Name: SignedMarkRevocationList_revision_id_seq; Type: SEQUENCE; Schema: public; Owner: -
--
CREATE SEQUENCE public."SignedMarkRevocationList_revision_id_seq"
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--
-- Name: SignedMarkRevocationList_revision_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
--
ALTER SEQUENCE public."SignedMarkRevocationList_revision_id_seq" OWNED BY public."SignedMarkRevocationList".revision_id;
--
-- Name: Tld; Type: TABLE; Schema: public; Owner: -
--
@@ -921,7 +972,7 @@ CREATE TABLE public."Tld" (
registry_lock_or_unlock_cost_currency text,
renew_billing_cost_transitions public.hstore NOT NULL,
renew_grace_period_length interval NOT NULL,
reserved_list_names text[] NOT NULL,
reserved_list_names text[],
restore_billing_cost_amount numeric(19,2),
restore_billing_cost_currency text,
roid_suffix text,
@@ -977,13 +1028,6 @@ ALTER TABLE ONLY public."ClaimsList" ALTER COLUMN revision_id SET DEFAULT nextva
ALTER TABLE ONLY public."DomainTransactionRecord" ALTER COLUMN id SET DEFAULT nextval('public."DomainTransactionRecord_id_seq"'::regclass);
--
-- Name: GracePeriod id; Type: DEFAULT; Schema: public; Owner: -
--
ALTER TABLE ONLY public."GracePeriod" ALTER COLUMN id SET DEFAULT nextval('public."GracePeriod_id_seq"'::regclass);
--
-- Name: PremiumList revision_id; Type: DEFAULT; Schema: public; Owner: -
--
@@ -1005,6 +1049,13 @@ ALTER TABLE ONLY public."RegistryLock" ALTER COLUMN revision_id SET DEFAULT next
ALTER TABLE ONLY public."ReservedList" ALTER COLUMN revision_id SET DEFAULT nextval('public."ReservedList_revision_id_seq"'::regclass);
--
-- Name: SignedMarkRevocationList revision_id; Type: DEFAULT; Schema: public; Owner: -
--
ALTER TABLE ONLY public."SignedMarkRevocationList" ALTER COLUMN revision_id SET DEFAULT nextval('public."SignedMarkRevocationList_revision_id_seq"'::regclass);
--
-- Name: Spec11ThreatMatch id; Type: DEFAULT; Schema: public; Owner: -
--
@@ -1096,7 +1147,15 @@ ALTER TABLE ONLY public."Cursor"
--
ALTER TABLE ONLY public."DelegationSignerData"
ADD CONSTRAINT "DelegationSignerData_pkey" PRIMARY KEY (domain_repo_id, key_tag);
ADD CONSTRAINT "DelegationSignerData_pkey" PRIMARY KEY (domain_repo_id, key_tag, algorithm, digest_type, digest);
--
-- Name: DomainDsDataHistory DomainDsDataHistory_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public."DomainDsDataHistory"
ADD CONSTRAINT "DomainDsDataHistory_pkey" PRIMARY KEY (ds_data_history_revision_id);
--
@@ -1147,6 +1206,14 @@ ALTER TABLE ONLY public."Host"
ADD CONSTRAINT "Host_pkey" PRIMARY KEY (repo_id);
--
-- Name: KmsSecret KmsSecret_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public."KmsSecret"
ADD CONSTRAINT "KmsSecret_pkey" PRIMARY KEY (revision_id);
--
-- Name: Lock Lock_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
@@ -1235,6 +1302,22 @@ ALTER TABLE ONLY public."Spec11ThreatMatch"
ADD CONSTRAINT "SafeBrowsingThreat_pkey" PRIMARY KEY (id);
--
-- Name: SignedMarkRevocationEntry SignedMarkRevocationEntry_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public."SignedMarkRevocationEntry"
ADD CONSTRAINT "SignedMarkRevocationEntry_pkey" PRIMARY KEY (revision_id, smd_id);
--
-- Name: SignedMarkRevocationList SignedMarkRevocationList_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public."SignedMarkRevocationList"
ADD CONSTRAINT "SignedMarkRevocationList_pkey" PRIMARY KEY (revision_id);
--
-- Name: Tld Tld_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
@@ -1476,6 +1559,13 @@ CREATE INDEX idxkjt9yaq92876dstimd93hwckh ON public."Domain" USING btree (curren
CREATE INDEX idxknk8gmj7s47q56cwpa6rmpt5l ON public."HostHistory" USING btree (history_type);
--
-- Name: idxli9nil3s4t4p21i3xluvvilb7; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX idxli9nil3s4t4p21i3xluvvilb7 ON public."KmsSecret" USING btree (secret_name);
--
-- Name: idxlrq7v63pc21uoh3auq6eybyhl; Type: INDEX; Schema: public; Owner: -
--
@@ -1640,14 +1730,6 @@ ALTER TABLE ONLY public."RegistryLock"
ADD CONSTRAINT fk2lhcwpxlnqijr96irylrh1707 FOREIGN KEY (relock_revision_id) REFERENCES public."RegistryLock"(revision_id);
--
-- Name: GracePeriod fk2mys4hojm6ev2g9tmy5aq6m7g; Type: FK CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public."GracePeriod"
ADD CONSTRAINT fk2mys4hojm6ev2g9tmy5aq6m7g FOREIGN KEY (domain_repo_id) REFERENCES public."Domain"(repo_id);
--
-- Name: Domain fk2u3srsfbei272093m3b3xwj23; Type: FK CONSTRAINT; Schema: public; Owner: -
--
@@ -1664,6 +1746,14 @@ ALTER TABLE ONLY public."HostHistory"
ADD CONSTRAINT fk3d09knnmxrt6iniwnp8j2ykga FOREIGN KEY (history_registrar_id) REFERENCES public."Registrar"(registrar_id);
--
-- Name: SignedMarkRevocationEntry fk5ivlhvs3121yx2li5tqh54u4; Type: FK CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public."SignedMarkRevocationEntry"
ADD CONSTRAINT fk5ivlhvs3121yx2li5tqh54u4 FOREIGN KEY (revision_id) REFERENCES public."SignedMarkRevocationList"(revision_id);
--
-- Name: ClaimsEntry fk6sc6at5hedffc0nhdcab6ivuq; Type: FK CONSTRAINT; Schema: public; Owner: -
--
@@ -1896,6 +1986,14 @@ ALTER TABLE ONLY public."GracePeriod"
ADD CONSTRAINT fk_grace_period_billing_recurrence_id FOREIGN KEY (billing_recurrence_id) REFERENCES public."BillingRecurrence"(billing_recurrence_id);
--
-- Name: GracePeriod fk_grace_period_domain_repo_id; Type: FK CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public."GracePeriod"
ADD CONSTRAINT fk_grace_period_domain_repo_id FOREIGN KEY (domain_repo_id) REFERENCES public."Domain"(repo_id) DEFERRABLE INITIALLY DEFERRED;
--
-- Name: Host fk_host_superordinate_domain; Type: FK CONSTRAINT; Schema: public; Owner: -
--
@@ -2040,6 +2138,14 @@ ALTER TABLE ONLY public."PremiumEntry"
ADD CONSTRAINT fko0gw90lpo1tuee56l0nb6y6g5 FOREIGN KEY (revision_id) REFERENCES public."PremiumList"(revision_id);
--
-- Name: DomainDsDataHistory fko4ilgyyfnvppbpuivus565i0j; Type: FK CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public."DomainDsDataHistory"
ADD CONSTRAINT fko4ilgyyfnvppbpuivus565i0j FOREIGN KEY (domain_repo_id, domain_history_revision_id) REFERENCES public."DomainHistory"(domain_repo_id, history_revision_id);
--
-- Name: DelegationSignerData fktr24j9v14ph2mfuw2gsmt12kq; Type: FK CONSTRAINT; Schema: public; Owner: -
--
@@ -1,14 +1,15 @@
# To run the build locally, install cloud-build-local first.
# See: https://cloud.google.com/cloud-build/docs/build-debug-locally
# In the root of a nomulus source tree, run:
# cloud-build-local --config=cloudbuild-javadoc.yaml --dryrun=false ..
# cloud-build-local --config=cloudbuild-dev-resource.yaml --dryrun=false ..
#
# This will compile javadoc for the FOSS version of the code base and replace
# the content at gs://${PROJECT_ID}-javadoc with the it. The compiled javadoc
# can then be accesssed at https://storage.googleapis.com/${PROJECT_ID}-javadoc
#
# To manually trigger a build on GCB, run:
# gcloud builds submit --config cloudbuild-javadoc.yaml ..
# It will also upload all ER diagrams under db/src/main/resource/sql/er_diagram
# to gs://${PROJECT_ID}-er-diagram. The ER diagrams can then be accesssed
# at https://storage.googleapis.com/${PROJECT_ID}-er-diagram
#
# To trigger a build automatically, follow the instructions below and add a trigger:
# https://cloud.google.com/cloud-build/docs/running-builds/automate-builds
@@ -24,7 +25,14 @@ steps:
- name: 'gcr.io/${PROJECT_ID}/builder'
entrypoint: /bin/bash
args: ['gsutil', '-m', 'rsync', '-d', '-r', 'build/docs/javadoc', 'gs://${PROJECT_ID}-javadoc']
# Upload the files to GCS
# We don't use GCB's built-in artifacts uploader because we want to delete
# the existing files in the bucket first, and we want to parallelize the
# uploading process.
- name: 'gcr.io/${PROJECT_ID}/builder'
entrypoint: /bin/bash
args: ['gsutil', '-m', 'rsync', '-d', '-r', 'db/src/main/resources/sql/er_diagram',
'gs://${PROJECT_ID}-er-diagram']
timeout: 3600s
options:
machineType: 'N1_HIGHCPU_8'
+151
View File
@@ -0,0 +1,151 @@
## Summary
This package contains an automated rollback tool for the Nomulus server on
AppEngine. When given the Nomulus tag of a deployed release, the tool directs
all traffics in the four recognized services (backend, default, pubapi, and
tools) to that release. In the process, it handles Nomulus tag to AppEngine
version ID translation, checks the target binary's compatibility with SQL
schema, starts/stops versions and redirects traffic in proper sequence, and
updates deployment metadata appropriately.
The tool has two limitations:
1. This tool only accepts one release tag as rollback target, which is applied
to all services.
2. The tool immediately migrates all traffic to the new versions. It does not
support gradual migration. This is not an issue now since gradual migration
is only available in automatically scaled versions, while none of versions
is using automatic scaling.
Although this tool is named a rollback tool, it can also reverse a rollback,
that is, rolling forward to a newer release.
## Prerequisites
This tool requires python version 3.7+. It also requires two GCP client
libraries: google-cloud-storage and google-api-python-client. They can be
installed using pip.
Registry team members should use either non-sudo pip3 or virtualenv/venv to
install the GCP libraries. A 'sudo pip install' may interfere with the Linux
tooling on your corp desktop. The non-sudo 'pip3 install' command installs the
libraries under $HOME/.local. The virtualenv or venv methods allow more control
over the installation location.
Below is an example of using virtualenv to install the libraries:
```shell
sudo apt-get install virtualenv python3-venv
python3 -m venv myproject
source myproject/bin/activate
pip install google-cloud-storage
pip install google-api-python-client
deactivate
```
If using virtualenv, make sure to run 'source myproject/bin/activate' before
running the rollback script.
## Usage
The tool can be invoked using the rollback_tool script in the Nomulus root
directory. The following parameters may be requested:
* dev_project: This is the GCP project that hosts the release and deployment
infrastructure, including the Spinnaker pipelines.
* project: This is the GCP project that hosts the Nomulus server to be rolled
back.
* env: This is the name of the Nomulus environment, e.g., sandbox or
production. Although the project to environment is available in Gradle
scripts and internal configuration files, it is not easy to extract them.
Therefore, we require the user to provide it for now.
A typical workflow goes as follows:
### Check Which Release is Serving
From the Nomulus root directory:
```shell
rollback_tool show_serving_release --dev_project ... --project ... --env ...
```
The output may look like:
```
backend nomulus-v049 nomulus-20201019-RC00
default nomulus-v049 nomulus-20201019-RC00
pubapi nomulus-v049 nomulus-20201019-RC00
tools nomulus-v049 nomulus-20201019-RC00
```
### Review Recent Deployments
```shell
rollback_tool show_recent_deployments --dev_project ... --project ... --env ...
```
This command displays up to 3 most recent deployments. The output (from sandbox
which only has two tracked deployments as of the writing of this document) may
look like:
```
backend nomulus-v048 nomulus-20201012-RC00
default nomulus-v048 nomulus-20201012-RC00
pubapi nomulus-v048 nomulus-20201012-RC00
tools nomulus-v048 nomulus-20201012-RC00
backend nomulus-v049 nomulus-20201019-RC00
default nomulus-v049 nomulus-20201019-RC00
pubapi nomulus-v049 nomulus-20201019-RC00
tools nomulus-v049 nomulus-20201019-RC00
```
### Roll to the Target Release
```shell
rollback_tool rollback --dev_project ... --project ... --env ... \
--targt_release {YOUR_CHOSEN_TAG} --run_mode ...
```
The rollback subcommand has two new parameters:
* target_release: This is the Nomulus tag of the target release, in the form
of nomulus-YYYYMMDD-RC[0-9][0-9]
* run_mode: This is the execution mode of the rollback action. There are three
modes:
1. dryrun: The tool will only output information about every step of the
rollback, including commands that a user can copy and run elsewhere.
2. interactive: The tool will prompt the user before executing each step.
The user may choose to abort the rollback, skip the step, or continue
with the step.
3. automatic: Tool will execute all steps in one shot.
The rollback steps are organized according to the following logic:
```
for service in ['backend', 'default', 'pubapi', 'tools']:
if service is on basicScaling: (See Notes # 1)
start the target version
if service is on manualScaling:
start the target version
set num_instances to its originally configured value
for service in ['backend', 'default', 'pubapi', 'tools']:
direct traffic to target version
for service in ['backend', 'default', 'pubapi', 'tools']:
if originally serving version is not the target version:
if originally serving version is on basicaScaling
stop the version
if originally serving version is on manualScaling:
stop the version
set_num_instances to 1 (See Notes #2)
```
Notes:
1. Versions on automatic scaling cannot be started or stopped by gcloud or the
AppEngine Admin REST API.
2. The minimum value assignable to num_instances through the REST API is 1.
This instance eventually will be released too.
+198
View File
@@ -0,0 +1,198 @@
# Copyright 2020 The Nomulus Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Helper for using the AppEngine Admin REST API."""
import time
from typing import Any, Dict, FrozenSet, Set
from googleapiclient import discovery
from googleapiclient import http
import common
# AppEngine services under management.
SERVICES = frozenset(['backend', 'default', 'pubapi', 'tools'])
# Forces 'list' calls (for services and versions) to return all
# results in one shot, to avoid having to handle pagination. This values
# should be greater than the maximum allowed services and versions in any
# project (
# https://cloud.google.com/appengine/docs/standard/python/an-overview-of-app-engine#limits).
_PAGE_SIZE = 250
# Number of times to check the status of an operation before timing out.
_STATUS_CHECK_TIMES = 5
# Delay between status checks of a long-running operation, in seconds
_STATUS_CHECK_INTERVAL = 5
class PagingError(Exception):
"""Error for unexpected partial results.
List calls in this module do not handle pagination. This error is raised
when a partial result is received.
"""
def __init__(self, uri: str):
super().__init__(
self, f'Received paged response unexpectedly when calling {uri}. '
'Consider increasing _PAGE_SIZE.')
class AppEngineAdmin:
"""Wrapper around the AppEngine Admin REST API client.
This class provides wrapper methods around the REST API for service and
version queries and for migrating between versions.
"""
def __init__(self,
project: str,
service_lookup: discovery.Resource = None,
status_check_interval: int = _STATUS_CHECK_INTERVAL) -> None:
"""Initialize this instance for an AppEngine(GCP) project."""
self._project = project
if service_lookup is not None:
apps = service_lookup.apps()
else:
apps = discovery.build('appengine', 'v1beta').apps()
self._services = apps.services()
self._operations = apps.operations()
self._status_check_interval = status_check_interval
@property
def project(self):
return self._project
def _checked_request(self, request: http.HttpRequest) -> Dict[str, Any]:
"""Verifies that all results are returned for a request."""
response = request.execute()
if 'nextPageToken' in response:
raise PagingError(request.uri)
return response
def get_serving_versions(self) -> FrozenSet[common.VersionKey]:
"""Returns the serving versions of every Nomulus service.
For each service in appengine.SERVICES, gets the version(s) actually
serving traffic. Services with the 'SERVING' status but no allocated
traffic are not included. Services not included in appengine.SERVICES
are also ignored.
Returns: An immutable collection of the serving versions grouped by
service.
"""
response = self._checked_request(
self._services.list(appsId=self._project, pageSize=_PAGE_SIZE))
# Response format is specified at
# http://googleapis.github.io/google-api-python-client/docs/dyn/appengine_v1beta5.apps.services.html#list.
versions = []
for service in response.get('services', []):
if service['id'] in SERVICES:
# yapf: disable
versions_with_traffic = (
service.get('split', {}).get('allocations', {}).keys())
# yapf: enable
for version in versions_with_traffic:
versions.append(common.VersionKey(service['id'], version))
return frozenset(versions)
# yapf: disable # argument indent wrong
def get_version_configs(
self, versions: Set[common.VersionKey]
) -> FrozenSet[common.VersionConfig]:
# yapf: enable
"""Returns the configuration of requested versions.
For each version in the request, gets the rollback-related data from
its static configuration (found in appengine-web.xml).
Args:
versions: A set of the VersionKey objects, each containing the
versions being queried in that service.
Returns:
The version configurations in an immutable set.
"""
requested_services = {version.service_id for version in versions}
version_configs = []
# Sort the requested services for ease of testing. For now the mocked
# AppEngine admin in appengine_test can only respond in a fixed order.
for service_id in sorted(requested_services):
response = self._checked_request(self._services.versions().list(
appsId=self._project,
servicesId=service_id,
pageSize=_PAGE_SIZE))
# Format of version_list is defined at
# https://googleapis.github.io/google-api-python-client/docs/dyn/appengine_v1beta5.apps.services.versions.html#list.
for version in response.get('versions', []):
if common.VersionKey(service_id, version['id']) in versions:
scalings = [
s for s in list(common.AppEngineScaling)
if s.value in version
]
if len(scalings) != 1:
raise common.CannotRollbackError(
f'Expecting exactly one scaling, found {scalings}')
scaling = common.AppEngineScaling(list(scalings)[0])
if scaling == common.AppEngineScaling.MANUAL:
manual_instances = version.get(
scaling.value).get('instances')
else:
manual_instances = None
version_configs.append(
common.VersionConfig(service_id, version['id'],
scaling, manual_instances))
return frozenset(version_configs)
def set_manual_scaling_num_instance(self, service_id: str, version_id: str,
manual_instances: int) -> None:
"""Creates an request to change an AppEngine version's status."""
update_mask = 'manualScaling.instances'
body = {'manualScaling': {'instances': manual_instances}}
response = self._services.versions().patch(appsId=self._project,
servicesId=service_id,
versionsId=version_id,
updateMask=update_mask,
body=body).execute()
operation_id = response.get('name').split('operations/')[1]
for _ in range(_STATUS_CHECK_TIMES):
if self.query_operation_status(operation_id):
return
time.sleep(self._status_check_interval)
raise common.CannotRollbackError(
f'Operation {operation_id} timed out.')
def query_operation_status(self, operation_id):
response = self._operations.get(appsId=self._project,
operationsId=operation_id).execute()
if response.get('response') is not None:
return True
if response.get('error') is not None:
raise common.CannotRollbackError(response['error'])
assert not response.get('done'), 'Operation done but no results.'
return False
+133
View File
@@ -0,0 +1,133 @@
# Copyright 2020 The Nomulus Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Unit tests for appengine."""
from typing import Any, Dict, List, Tuple, Union
import unittest
from unittest import mock
from unittest.mock import patch
import appengine
import common
def setup_appengine_admin() -> Tuple[object, object]:
"""Helper for setting up a mocked AppEngineAdmin instance.
Returns:
An AppEngineAdmin instance and a request with which API responses can
be mocked.
"""
# Assign mocked API response to mock_request.execute.
mock_request = mock.MagicMock()
mock_request.uri.return_value = 'myuri'
# Mocked resource shared by services, versions, and operations.
resource = mock.MagicMock()
resource.list.return_value = mock_request
resource.get.return_value = mock_request
resource.patch.return_value = mock_request
# Root resource of AppEngine API. Exact type unknown.
apps = mock.MagicMock()
apps.services.return_value = resource
resource.versions.return_value = resource
apps.operations.return_value = resource
service_lookup = mock.MagicMock()
service_lookup.apps.return_value = apps
appengine_admin = appengine.AppEngineAdmin('project', service_lookup, 1)
return (appengine_admin, mock_request)
class AppEngineTestCase(unittest.TestCase):
"""Unit tests for appengine."""
def setUp(self) -> None:
self._client, self._mock_request = setup_appengine_admin()
self.addCleanup(patch.stopall)
# yapf: disable
def _set_mocked_response(
self,
responses: Union[Dict[str, Any], List[Dict[str, Any]]]) -> None:
# yapf: enable
if isinstance(responses, list):
self._mock_request.execute.side_effect = responses
else:
self._mock_request.execute.return_value = responses
def test_checked_request_multipage_raises(self) -> None:
self._set_mocked_response({'nextPageToken': ''})
self.assertRaises(appengine.PagingError,
self._client.get_serving_versions)
def test_get_serving_versions(self) -> None:
self._set_mocked_response({
'services': [{
'split': {
'allocations': {
'my_version': 3.14,
}
},
'id': 'pubapi'
}, {
'split': {
'allocations': {
'another_version': 2.71,
}
},
'id': 'error_dashboard'
}]
})
self.assertEqual(
self._client.get_serving_versions(),
frozenset([common.VersionKey('pubapi', 'my_version')]))
def test_get_version_configs(self):
self._set_mocked_response({
'versions': [{
'basicScaling': {
'maxInstances': 10
},
'id': 'version'
}]
})
self.assertEqual(
self._client.get_version_configs(
frozenset([common.VersionKey('default', 'version')])),
frozenset([
common.VersionConfig('default', 'version',
common.AppEngineScaling.BASIC)
]))
def test_async_update(self):
self._set_mocked_response([
{
'name': 'project/operations/op_id',
'done': False
},
{
'name': 'project/operations/op_id',
'done': False
},
{
'name': 'project/operations/op_id',
'response': {},
'done': True
},
])
self._client.set_manual_scaling_num_instance('service', 'version', 1)
self.assertEqual(self._mock_request.execute.call_count, 3)
if __name__ == '__main__':
unittest.main()
+111
View File
@@ -0,0 +1,111 @@
# 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.
"""Declares data types that describe AppEngine services and versions."""
import dataclasses
import enum
import pathlib
import re
from typing import Optional
class CannotRollbackError(Exception):
"""Indicates that rollback cannot be done by this tool.
This error is for situations where rollbacks are either not allowed or
cannot be planned. Example scenarios include:
- The target release is incompatible with the SQL schema.
- The target release has never been deployed to AppEngine.
- The target release is no longer available, e.g., has been manually
deleted by the operators.
- A state-changing call to AppEngine Admin API has failed.
User must manually fix such problems before trying again to roll back.
"""
pass
class AppEngineScaling(enum.Enum):
"""Types of scaling schemes supported in AppEngine.
The value of each name is the property name in the REST API requests and
responses.
"""
AUTOMATIC = 'automaticScaling'
BASIC = 'basicScaling'
MANUAL = 'manualScaling'
@dataclasses.dataclass(frozen=True)
class VersionKey:
"""Identifier of a deployed version on AppEngine.
AppEngine versions as deployable units are managed on per-service basis.
Each instance of this class uniquely identifies an AppEngine version.
This class implements the __eq__ method so that its equality property
applies to subclasses by default unless they override it.
"""
service_id: str
version_id: str
def __eq__(self, other):
return (isinstance(other, VersionKey)
and self.service_id == other.service_id
and self.version_id == other.version_id)
@dataclasses.dataclass(frozen=True, eq=False)
class VersionConfig(VersionKey):
"""Rollback-related static configuration of an AppEngine version.
Contains data found from the application-web.xml for this version.
Attributes:
scaling: The scaling scheme of this version. This value determines what
steps are needed for the rollback. If a version is on automatic
scaling, we only need to direct traffic to it or away from it. The
version cannot be started, stopped, or have its number of instances
updated. If a version is on manual scaling, it not only needs to be
started or stopped explicitly, its instances need to be updated too
(to 1, the lowest allowed number) when it is shutdown, and to its
originally configured number of VM instances when brought up.
manual_scaling_instances: The originally configured VM instances to use
for each version that is on manual scaling.
"""
scaling: AppEngineScaling
manual_scaling_instances: Optional[int] = None
def get_nomulus_root() -> str:
"""Finds the current Nomulus root directory.
Returns:
The absolute path to the Nomulus root directory.
"""
for folder in pathlib.Path(__file__).parents:
if folder.name != 'nomulus':
continue
if not folder.joinpath('settings.gradle').exists():
continue
with open(folder.joinpath('settings.gradle'), 'r') as file:
for line in file:
if re.match(r"^rootProject.name\s*=\s*'nomulus'\s*$", line):
return folder.absolute()
raise RuntimeError(
'Do not move this file out of the Nomulus directory tree.')
+148
View File
@@ -0,0 +1,148 @@
# Copyright 2020 The Nomulus Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Helper for managing Nomulus deployment records on GCS."""
from typing import Dict, FrozenSet, Set
from google.cloud import storage
import common
def _get_version_map_name(env: str):
return f'nomulus.{env}.versions'
def _get_schema_tag_file(env: str):
return f'sql.{env}.tag'
class GcsClient:
"""Manages Nomulus deployment records on GCS."""
def __init__(self, project: str, gcs_client=None) -> None:
"""Initializes the instance for a GCP project.
Args:
project: The GCP project with Nomulus deployment records.
gcs_client: Optional API client to use.
"""
self._project = project
if gcs_client is not None:
self._client = gcs_client
else:
self._client = storage.Client(self._project)
@property
def project(self):
return self._project
def _get_deploy_bucket_name(self):
return f'{self._project}-deployed-tags'
def _get_release_to_version_mapping(
self, env: str) -> Dict[common.VersionKey, str]:
"""Returns the content of the release to version mapping file.
File content is returned in utf-8 encoding. Each line in the file is
in this format:
'{RELEASE_TAG},{APP_ENGINE_SERVICE_ID},{APP_ENGINE_VERSION}'.
"""
file_content = self._client.get_bucket(
self._get_deploy_bucket_name()).get_blob(
_get_version_map_name(env)).download_as_text()
mapping = {}
for line in file_content.splitlines(False):
tag, service_id, version_id = line.split(',')
mapping[common.VersionKey(service_id, version_id)] = tag
return mapping
def get_versions_by_release(self, env: str,
nom_tag: str) -> FrozenSet[common.VersionKey]:
"""Returns AppEngine version ids of a given Nomulus release tag.
Fetches the version mapping file maintained by the deployment process
and parses its content into a collection of VersionKey instances.
A release may map to multiple versions in a service if it has been
deployed multiple times. This is not intended behavior and may only
happen by mistake.
Args:
env: The environment of the deployed release, e.g., sandbox.
nom_tag: The Nomulus release tag.
Returns:
An immutable set of VersionKey instances.
"""
mapping = self._get_release_to_version_mapping(env)
return frozenset(
[version for version in mapping if mapping[version] == nom_tag])
def get_releases_by_versions(
self, env: str,
versions: Set[common.VersionKey]) -> Dict[common.VersionKey, str]:
"""Gets the release tags of the AppEngine versions.
Args:
env: The environment of the deployed release, e.g., sandbox.
versions: The AppEngine versions.
Returns:
A mapping of versions to release tags.
"""
mapping = self._get_release_to_version_mapping(env)
return {
version: tag
for version, tag in mapping.items() if version in versions
}
def get_recent_deployments(
self, env: str, num_records: int) -> Dict[common.VersionKey, str]:
"""Gets the most recent deployment records.
Deployment records are stored in a file, with one line per service.
Caller should adjust num_records according to the number of services
in AppEngine.
Args:
env: The environment of the deployed release, e.g., sandbox.
num_records: the number of lines to go back.
"""
file_content = self._client.get_bucket(
self._get_deploy_bucket_name()).get_blob(
_get_version_map_name(env)).download_as_text()
mapping = {}
for line in file_content.splitlines(False)[-num_records:]:
tag, service_id, version_id = line.split(',')
mapping[common.VersionKey(service_id, version_id)] = tag
return mapping
def get_schema_tag(self, env: str) -> str:
"""Gets the release tag of the SQL schema in the given environment.
This tag is needed for the server/schema compatibility test.
"""
file_content = self._client.get_bucket(
self._get_deploy_bucket_name()).get_blob(
_get_schema_tag_file(env)).download_as_text().splitlines(False)
assert len(
file_content
) == 1, f'Unexpected content in {_get_schema_tag_file(env)}.'
return file_content[0]
+152
View File
@@ -0,0 +1,152 @@
# Copyright 2020 The Nomulus Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Unit tests for gcs."""
import textwrap
import unittest
from unittest import mock
import common
import gcs
def setup_gcs_client(env: str):
"""Sets up a mocked GcsClient.
Args:
env: Name of the Nomulus environment.
Returns:
A GcsClient instance and two mocked blobs representing the two schema
tag file and version map file on GCS.
"""
schema_tag_blob = mock.MagicMock()
schema_tag_blob.download_as_text.return_value = 'tag\n'
version_map_blob = mock.MagicMock()
blobs_by_name = {
f'nomulus.{env}.versions': version_map_blob,
f'sql.{env}.tag': schema_tag_blob
}
bucket = mock.MagicMock()
bucket.get_blob.side_effect = lambda blob_name: blobs_by_name[blob_name]
google_client = mock.MagicMock()
google_client.get_bucket.return_value = bucket
gcs_client = gcs.GcsClient('project', google_client)
return (gcs_client, schema_tag_blob, version_map_blob)
class GcsTestCase(unittest.TestCase):
"""Unit tests for gcs."""
_ENV = 'crash'
def setUp(self) -> None:
self._client, self._schema_tag_blob, self._version_map_blob = \
setup_gcs_client(self._ENV)
self.addCleanup(mock.patch.stopall)
def test_get_schema_tag(self):
self.assertEqual(self._client.get_schema_tag(self._ENV), 'tag')
def test_get_versions_by_release(self):
self._version_map_blob.download_as_text.return_value = \
'nomulus-20200925-RC02,backend,nomulus-backend-v008'
self.assertEqual(
self._client.get_versions_by_release(self._ENV,
'nomulus-20200925-RC02'),
frozenset([common.VersionKey('backend', 'nomulus-backend-v008')]))
def test_get_versions_by_release_not_found(self):
self._version_map_blob.download_as_text.return_value = \
'nomulus-20200925-RC02,backend,nomulus-backend-v008'
self.assertEqual(
self._client.get_versions_by_release(self._ENV, 'no-such-tag'),
frozenset([]))
def test_get_versions_by_release_multiple_service(self):
self._version_map_blob.download_as_text.return_value = textwrap.dedent(
"""\
nomulus-20200925-RC02,backend,nomulus-backend-v008
nomulus-20200925-RC02,default,nomulus-default-v008
""")
self.assertEqual(
self._client.get_versions_by_release(self._ENV,
'nomulus-20200925-RC02'),
frozenset([
common.VersionKey('backend', 'nomulus-backend-v008'),
common.VersionKey('default', 'nomulus-default-v008')
]))
def test_get_versions_by_release_multiple_deployment(self):
self._version_map_blob.download_as_text.return_value = textwrap.dedent(
"""\
nomulus-20200925-RC02,backend,nomulus-backend-v008
nomulus-20200925-RC02,backend,nomulus-backend-v018
""")
self.assertEqual(
self._client.get_versions_by_release(self._ENV,
'nomulus-20200925-RC02'),
frozenset([
common.VersionKey('backend', 'nomulus-backend-v008'),
common.VersionKey('backend', 'nomulus-backend-v018')
]))
def test_get_releases_by_versions(self):
self._version_map_blob.download_as_text.return_value = textwrap.dedent(
"""\
nomulus-20200925-RC02,backend,nomulus-backend-v008
nomulus-20200925-RC02,default,nomulus-default-v008
""")
self.assertEqual(
self._client.get_releases_by_versions(
self._ENV, {
common.VersionKey('backend', 'nomulus-backend-v008'),
common.VersionKey('default', 'nomulus-default-v008')
}), {
common.VersionKey('backend', 'nomulus-backend-v008'):
'nomulus-20200925-RC02',
common.VersionKey('default', 'nomulus-default-v008'):
'nomulus-20200925-RC02',
})
def test_get_recent_deployments(self):
file_content = textwrap.dedent("""\
nomulus-20200925-RC02,backend,nomulus-backend-v008
nomulus-20200925-RC02,default,nomulus-default-v008
""")
self._version_map_blob.download_as_text.return_value = file_content
self.assertEqual(
self._client.get_recent_deployments(self._ENV, 2), {
common.VersionKey('default', 'nomulus-default-v008'):
'nomulus-20200925-RC02',
common.VersionKey('backend', 'nomulus-backend-v008'):
'nomulus-20200925-RC02'
})
def test_get_recent_deployments_fewer_lines(self):
self._version_map_blob.download_as_text.return_value = textwrap.dedent(
"""\
nomulus-20200925-RC02,backend,nomulus-backend-v008
nomulus-20200925-RC02,default,nomulus-default-v008
""")
self.assertEqual(
self._client.get_recent_deployments(self._ENV, 1), {
common.VersionKey('default', 'nomulus-default-v008'):
'nomulus-20200925-RC02'
})
if __name__ == '__main__':
unittest.main()
+198
View File
@@ -0,0 +1,198 @@
# Copyright 2020 The Nomulus Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Generates a sequence of operations for execution."""
from typing import FrozenSet, Tuple
import appengine
import common
import dataclasses
import gcs
import steps
@dataclasses.dataclass(frozen=True)
class ServiceRollback:
"""Data needed for rolling back one service.
Holds the configurations of both the currently serving version(s) and the
rollback target in a service.
Attributes:
target_version: The version to roll back to.
serving_versions: The currently serving versions to be stopped. This
set may be empty. It may also have multiple versions (when traffic
is split).
"""
target_version: common.VersionConfig
serving_versions: FrozenSet[common.VersionConfig]
def __post_init__(self):
"""Validates that all versions are for the same service."""
if self.serving_versions:
for config in self.serving_versions:
assert config.service_id == self.target_version.service_id
# yapf: disable
def _get_service_rollback_plan(
target_configs: FrozenSet[common.VersionConfig],
serving_configs: FrozenSet[common.VersionConfig]
) -> Tuple[ServiceRollback]:
# yapf: enable
"""Determines the versions to bring up/down in each service.
In each service, this method makes sure that at least one version is found
for the rollback target. If multiple versions are found, which may only
happen if the target release was deployed multiple times, randomly choose
one.
If a target version is already serving traffic, instead of checking if it
gets 100 percent of traffic, this method still generates operations to
start it and direct all traffic to it. This is not a problem since these
operations are idempotent.
Attributes:
target_configs: The rollback target versions in each managed service
(as defined in appengine.SERVICES).
serving_configs: The currently serving versions in each service.
Raises:
CannotRollbackError: Rollback is impossible because a target version
cannot be found for some service.
Returns:
For each service, the versions to bring up/down if applicable.
"""
targets_by_service = {}
for version in target_configs:
targets_by_service.setdefault(version.service_id, set()).add(version)
serving_by_service = {}
for version in serving_configs:
serving_by_service.setdefault(version.service_id, set()).add(version)
# The target_configs parameter only has configs for managed services.
# Since targets_by_service is derived from it, its keyset() should equal
# to appengine.SERVICES.
if targets_by_service.keys() != appengine.SERVICES:
cannot_rollback = appengine.SERVICES.difference(
targets_by_service.keys())
raise common.CannotRollbackError(
f'Target version(s) not found for {cannot_rollback}')
plan = []
for service_id, versions in targets_by_service.items():
serving_configs = serving_by_service.get(service_id, set())
versions_to_stop = serving_configs.difference(versions)
chosen_target = list(versions)[0]
plan.append(ServiceRollback(chosen_target,
frozenset(versions_to_stop)))
return tuple(plan)
# yapf: disable
def _generate_steps(
gcs_client: gcs.GcsClient,
appengine_admin: appengine.AppEngineAdmin,
env: str,
target_release: str,
rollback_plan: Tuple[ServiceRollback]
) -> Tuple[steps.RollbackStep, ...]:
# yapf: enable
"""Generates the sequence of operations for execution.
A rollback consists of the following steps:
1. Run schema compatibility test for the target release.
2. For each service,
a. If the target version does not use automatic scaling, start it.
i. If target version uses manual scaling, sets its instances to the
configured values.
b. If the target version uses automatic scaling, do nothing.
3. For each service, immediately direct all traffic to the target version.
4. For each service, go over its versions to be stopped:
a. If a version uses automatic scaling, do nothing.
b. If a version does not use automatic scaling, stop it.
i. If a version uses manual scaling, sets its instances to 1 (one, the
lowest value allowed on the REST API) to release the instances.
5. Update the appropriate deployed tag file on GCS with the target release
tag.
Returns:
The sequence of operations to execute for rollback.
"""
rollback_steps = [
steps.check_schema_compatibility(gcs_client.project, target_release,
gcs_client.get_schema_tag(env))
]
for plan in rollback_plan:
if plan.target_version.scaling != common.AppEngineScaling.AUTOMATIC:
rollback_steps.append(
steps.start_or_stop_version(appengine_admin.project, 'start',
plan.target_version))
if plan.target_version.scaling == common.AppEngineScaling.MANUAL:
rollback_steps.append(
steps.set_manual_scaling_instances(
appengine_admin, plan.target_version,
plan.target_version.manual_scaling_instances))
for plan in rollback_plan:
rollback_steps.append(
steps.direct_service_traffic_to_version(appengine_admin.project,
plan.target_version))
for plan in rollback_plan:
for version in plan.serving_versions:
if plan.target_version.scaling != common.AppEngineScaling.AUTOMATIC:
rollback_steps.append(
steps.start_or_stop_version(appengine_admin.project,
'stop', version))
if plan.target_version.scaling == common.AppEngineScaling.MANUAL:
# Release all but one instances. Cannot set num_instances to 0
# with this api.
rollback_steps.append(
steps.set_manual_scaling_instances(appengine_admin,
version, 1))
rollback_steps.append(
steps.update_deploy_tags(gcs_client.project, env, target_release))
rollback_steps.append(
steps.sync_live_release(gcs_client.project, target_release))
return tuple(rollback_steps)
def get_rollback_plan(gcs_client: gcs.GcsClient,
appengine_admin: appengine.AppEngineAdmin, env: str,
target_release: str) -> Tuple[steps.RollbackStep]:
"""Generates the sequence of rollback operations for execution."""
target_versions = gcs_client.get_versions_by_release(env, target_release)
serving_versions = appengine_admin.get_serving_versions()
all_version_configs = appengine_admin.get_version_configs(
target_versions.union(serving_versions))
target_configs = frozenset([
config for config in all_version_configs if config in target_versions
])
serving_configs = frozenset([
config for config in all_version_configs if config in serving_versions
])
rollback_plan = _get_service_rollback_plan(target_configs, serving_configs)
return _generate_steps(gcs_client, appengine_admin, env, target_release,
rollback_plan)
+130
View File
@@ -0,0 +1,130 @@
# Copyright 2020 The Nomulus Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""End-to-end test of rollback."""
import textwrap
from typing import Any, Dict
import unittest
from unittest import mock
import appengine_test
import gcs_test
import plan
def _make_serving_version(service: str, version: str) -> Dict[str, Any]:
"""Creates description of one serving version in API response."""
return {
'split': {
'allocations': {
version: 1,
}
},
'id': service
}
def _make_version_config(version,
scaling: str,
instance_tag: str,
instances: int = 10) -> Dict[str, Any]:
"""Creates one version config as part of an API response."""
return {scaling: {instance_tag: instances}, 'id': version}
class RollbackTestCase(unittest.TestCase):
"""End-to-end test of rollback."""
def setUp(self) -> None:
self._appengine_admin, self._appengine_request = (
appengine_test.setup_appengine_admin())
self._gcs_client, self._schema_tag, self._version_map = (
gcs_test.setup_gcs_client('crash'))
self.addCleanup(mock.patch.stopall)
def test_rollback_success(self):
self._schema_tag.download_as_text.return_value = (
'nomulus-2010-1014-RC00')
self._version_map.download_as_text.return_value = textwrap.dedent("""\
nomulus-20201014-RC00,backend,nomulus-backend-v009
nomulus-20201014-RC00,default,nomulus-default-v009
nomulus-20201014-RC00,pubapi,nomulus-pubapi-v009
nomulus-20201014-RC00,tools,nomulus-tools-v009
nomulus-20201014-RC01,backend,nomulus-backend-v011
nomulus-20201014-RC01,default,nomulus-default-v010
nomulus-20201014-RC01,pubapi,nomulus-pubapi-v010
nomulus-20201014-RC01,tools,nomulus-tools-v010
""")
self._appengine_request.execute.side_effect = [
# Response to get_serving_versions:
{
'services': [
_make_serving_version('backend', 'nomulus-backend-v011'),
_make_serving_version('default', 'nomulus-default-v010'),
_make_serving_version('pubapi', 'nomulus-pubapi-v010'),
_make_serving_version('tools', 'nomulus-tools-v010')
]
},
# Responses to get_version_configs. AppEngineAdmin queries the
# services by alphabetical order to facilitate this test.
{
'versions': [
_make_version_config('nomulus-backend-v009',
'basicScaling', 'maxInstances'),
_make_version_config('nomulus-backend-v011',
'basicScaling', 'maxInstances')
]
},
{
'versions': [
_make_version_config('nomulus-default-v009',
'basicScaling', 'maxInstances'),
_make_version_config('nomulus-default-v010',
'basicScaling', 'maxInstances')
]
},
{
'versions': [
_make_version_config('nomulus-pubapi-v009',
'manualScaling', 'instances'),
_make_version_config('nomulus-pubapi-v010',
'manualScaling', 'instances')
]
},
{
'versions': [
_make_version_config('nomulus-tools-v009',
'automaticScaling',
'maxTotalInstances'),
_make_version_config('nomulus-tools-v010',
'automaticScaling',
'maxTotalInstances')
]
}
]
steps = plan.get_rollback_plan(self._gcs_client, self._appengine_admin,
'crash', 'nomulus-20201014-RC00')
self.assertEqual(len(steps), 15)
self.assertRegex(steps[0].info(),
'.*nom_build :integration:sqlIntegrationTest.*')
self.assertRegex(steps[1].info(), '.*gcloud app versions start.*')
self.assertRegex(steps[5].info(),
'.*gcloud app services set-traffic.*')
self.assertRegex(steps[9].info(), '.*gcloud app versions stop.*')
self.assertRegex(steps[13].info(),
'.*echo nomulus-20201014-RC00 | gsutil cat -.*')
self.assertRegex(steps[14].info(), '.*gsutil -m rsync -d .*')
if __name__ == '__main__':
unittest.main()
+178
View File
@@ -0,0 +1,178 @@
# Copyright 2020 The Nomulus Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Script to rollback the Nomulus server on AppEngine."""
import argparse
import dataclasses
import sys
import textwrap
from typing import Any, Optional, Tuple
import appengine
import gcs
import plan
MAIN_HELP = 'Script to roll back the Nomulus server on AppEngine.'
ROLLBACK_HELP = 'Rolls back Nomulus to the target release.'
GET_SERVING_RELEASE_HELP = 'Shows the release tag(s) of the serving versions.'
GET_RECENT_DEPLOYMENTS_HELP = ('Shows recently deployed versions and their '
'release tags.')
ROLLBACK_MODE_HELP = textwrap.dedent("""\
The execution mode.
- dryrun: Prints descriptions of all steps.
- interactive: Prompts for confirmation before executing
each step.
- auto: Executes all steps in one go.
""")
@dataclasses.dataclass(frozen=True)
class Argument:
"""Describes a command line argument.
This class is for use with argparse.ArgumentParser. Except for the
'arg_names' attribute which specifies the argument name and/or flags, all
other attributes must match an accepted parameter in the parser's
add_argument() method.
"""
arg_names: Tuple[str, ...]
help: str
default: Optional[Any] = None
required: bool = True
choices: Optional[Tuple[str, ...]] = None
def get_arg_attrs(self):
return dict((k, v) for k, v in vars(self).items() if k != 'arg_names')
ARGUMENTS = (Argument(('--dev_project', '-d'),
'The GCP project with Nomulus deployment records.'),
Argument(('--project', '-p'),
'The GCP project where the Nomulus server is deployed.'),
Argument(('--env', '-e'),
'The name of the Nomulus server environment.',
choices=('production', 'sandbox', 'crash', 'alpha')))
ROLLBACK_ARGUMENTS = (Argument(('--target_release', '-t'),
'The release to be deployed.'),
Argument(('--run_mode', '-m'),
ROLLBACK_MODE_HELP,
required=False,
default='dryrun',
choices=('dryrun', 'interactive', 'auto')))
def rollback(dev_project: str, project: str, env: str, target_release: str,
run_mode: str) -> None:
"""Rolls back a Nomulus server to the target release.
Args:
dev_project: The GCP project with deployment records.
project: The GCP project of the Nomulus server.
env: The environment name of the Nomulus server.
target_release: The tag of the release to be brought up.
run_mode: How to handle the rollback steps: print-only (dryrun)
one step at a time with user confirmation (interactive),
or all steps in one shot (automatic).
"""
steps = plan.get_rollback_plan(gcs.GcsClient(dev_project),
appengine.AppEngineAdmin(project), env,
target_release)
print('Rollback steps:\n\n')
for step in steps:
print(f'{step.info()}\n')
if run_mode == 'dryrun':
continue
if run_mode == 'interactive':
confirmation = input(
'Do you wish to (c)ontinue, (s)kip, or (a)bort? ')
if confirmation == 'a':
return
if confirmation == 's':
continue
step.execute()
def show_serving_release(dev_project: str, project: str, env: str) -> None:
"""Shows the release tag(s) of the currently serving versions."""
serving_versions = appengine.AppEngineAdmin(project).get_serving_versions()
versions_to_tags = gcs.GcsClient(dev_project).get_releases_by_versions(
env, serving_versions)
print(f'{project}:')
for version, tag in versions_to_tags.items():
print(f'{version.service_id}\t{version.version_id}\t{tag}')
def show_recent_deployments(dev_project: str, project: str, env: str) -> None:
"""Show release and version of recent deployments."""
num_services = len(appengine.SERVICES)
num_records = 3 * num_services
print(f'{project}:')
for version, tag in gcs.GcsClient(dev_project).get_recent_deployments(
env, num_records).items():
print(f'{version.service_id}\t{version.version_id}\t{tag}')
def main() -> int:
parser = argparse.ArgumentParser(prog='nom_rollback',
description=MAIN_HELP)
subparsers = parser.add_subparsers(dest='command',
help='Supported commands')
rollback_parser = subparsers.add_parser(
'rollback',
help=ROLLBACK_HELP,
formatter_class=argparse.RawTextHelpFormatter)
for flag in ARGUMENTS:
rollback_parser.add_argument(*flag.arg_names, **flag.get_arg_attrs())
for flag in ROLLBACK_ARGUMENTS:
rollback_parser.add_argument(*flag.arg_names, **flag.get_arg_attrs())
show_serving_release_parser = subparsers.add_parser(
'show_serving_release', help=GET_SERVING_RELEASE_HELP)
for flag in ARGUMENTS:
show_serving_release_parser.add_argument(*flag.arg_names,
**flag.get_arg_attrs())
show_recent_deployments_parser = subparsers.add_parser(
'show_recent_deployments', help=GET_RECENT_DEPLOYMENTS_HELP)
for flag in ARGUMENTS:
show_recent_deployments_parser.add_argument(*flag.arg_names,
**flag.get_arg_attrs())
args = parser.parse_args()
command = args.command
args = {k: v for k, v in vars(args).items() if k != 'command'}
{
'rollback': rollback,
'show_recent_deployments': show_recent_deployments,
'show_serving_release': show_serving_release
}[command](**args)
return 0
if __name__ == '__main__':
try:
sys.exit(main())
except Exception as ex: # pylint: disable=broad-except
print(ex)
sys.exit(1)
+169
View File
@@ -0,0 +1,169 @@
# Copyright 2020 The Nomulus Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Definition of rollback steps and factory methods to create them."""
import dataclasses
import subprocess
import textwrap
from typing import Tuple
import appengine
import common
@dataclasses.dataclass(frozen=True)
class RollbackStep:
"""One rollback step.
Most steps are implemented using commandline tools, e.g., gcloud and
gsutil, and execute their commands by forking a subprocess. Each step
also has a info method that returns its command with a description.
Two steps are handled differently. The _UpdateDeployTag step gets a piped
shell command, which needs to be handled differently. The
_SetManualScalingNumInstances step uses the AppEngine Admin API client in
this package to set the number of instances. The Nomulus set_num_instances
command is not working right now.
"""
description: str
command: Tuple[str, ...]
def info(self) -> str:
return f'# {self.description}\n' f'{" ".join(self.command)}'
def execute(self) -> None:
"""Executes the step.
Raises:
CannotRollbackError if command fails.
"""
if subprocess.call(self.command) != 0:
raise common.CannotRollbackError(f'Failed: {self.description}')
def check_schema_compatibility(dev_project: str, nom_tag: str,
sql_tag: str) -> RollbackStep:
return RollbackStep(description='Check compatibility with SQL schema.',
command=(f'{common.get_nomulus_root()}/nom_build',
':integration:sqlIntegrationTest',
f'--schema_version={sql_tag}',
f'--nomulus_version={nom_tag}',
'--publish_repo='
f'gcs://{dev_project}-deployed-tags/maven'))
@dataclasses.dataclass(frozen=True)
class _SetManualScalingNumInstances(RollbackStep):
"""Sets the number of instances for a manual scaling version.
The Nomulus set_num_instances command is currently broken. This step uses
the AppEngine REST API to update the version.
"""
appengine_admin: appengine.AppEngineAdmin
version: common.VersionKey
num_instance: int
def execute(self) -> None:
self.appengine_admin.set_manual_scaling_num_instance(
self.version.service_id, self.version.version_id,
self.num_instance)
def set_manual_scaling_instances(appengine_admin: appengine.AppEngineAdmin,
version: common.VersionConfig,
num_instances: int) -> RollbackStep:
cmd_description = textwrap.dedent("""\
Nomulus set_num_instances command is currently broken.
This script uses the AppEngine REST API to update the version.
To set this value without using this tool, you may use the REST API at
https://cloud.google.com/appengine/docs/admin-api/reference/rest/v1beta/apps.services.versions/patch
""")
return _SetManualScalingNumInstances(
f'Set number of instance for manual-scaling version '
f'{version.version_id} in {version.service_id} to {num_instances}.',
(cmd_description, ''), appengine_admin, version, num_instances)
def start_or_stop_version(project: str, action: str,
version: common.VersionKey) -> RollbackStep:
"""Creates a rollback step that starts or stops an AppEngine version.
Args:
project: The GCP project of the AppEngine application.
action: Start or Stop.
version: The version being managed.
"""
return RollbackStep(
f'{action.title()} {version.version_id} in {version.service_id}',
('gcloud', 'app', 'versions', action, version.version_id, '--quiet',
'--service', version.service_id, '--project', project))
def direct_service_traffic_to_version(
project: str, version: common.VersionKey) -> RollbackStep:
return RollbackStep(
f'Direct all traffic to {version.version_id} in {version.service_id}.',
('gcloud', 'app', 'services', 'set-traffic', version.service_id,
'--quiet', f'--splits={version.version_id}=1', '--project', project))
@dataclasses.dataclass(frozen=True)
class _UpdateDeployTag(RollbackStep):
"""Updates the deployment tag on GCS."""
nom_tag: str
destination: str
def execute(self) -> None:
with subprocess.Popen(('gsutil', 'cp', '-', self.destination),
stdin=subprocess.PIPE) as p:
try:
p.communicate(self.nom_tag.encode('utf-8'))
if p.wait() != 0:
raise common.CannotRollbackError(
f'Failed: {self.description}')
except:
p.kill()
raise
def update_deploy_tags(dev_project: str, env: str,
nom_tag: str) -> RollbackStep:
destination = f'gs://{dev_project}-deployed-tags/nomulus.{env}.tag'
return _UpdateDeployTag(
f'Update Nomulus tag in {env}',
(f'echo {nom_tag} | gsutil cp - {destination}', ''), nom_tag,
destination)
def sync_live_release(dev_project: str, nom_tag: str) -> RollbackStep:
"""Syncs the target release artifacts to the live folder.
By convention the gs://{dev_project}-deploy/live folder should contain the
artifacts from the currently serving release.
For Domain Registry team members, this step updates the nomulus tool
installed on corp desktops.
"""
artifacts_folder = f'gs://{dev_project}-deploy/{nom_tag}'
live_folder = f'gs://{dev_project}-deploy/live'
return RollbackStep(
f'Syncing {artifacts_folder} to {live_folder}.',
('gsutil', '-m', 'rsync', '-d', artifacts_folder, live_folder))
Executable
+5
View File
@@ -0,0 +1,5 @@
#!/bin/sh
# Wrapper for rollback_tool.py.
cd $(dirname $0)
python3 ./release/rollback/rollback_tool.py "$@"
exit $?