diff --git a/core/src/main/java/google/registry/model/domain/DomainBase.java b/core/src/main/java/google/registry/model/domain/DomainBase.java index c0efa17bf..652d75a96 100644 --- a/core/src/main/java/google/registry/model/domain/DomainBase.java +++ b/core/src/main/java/google/registry/model/domain/DomainBase.java @@ -76,8 +76,10 @@ import javax.annotation.Nullable; import javax.persistence.AttributeOverride; import javax.persistence.AttributeOverrides; import javax.persistence.Column; +import javax.persistence.Convert; import javax.persistence.ElementCollection; import javax.persistence.Embedded; +import javax.persistence.JoinTable; import javax.persistence.Transient; import org.joda.time.DateTime; import org.joda.time.Interval; @@ -141,7 +143,11 @@ public class DomainBase extends EppResource */ @Index @ElementCollection @Transient Set> nsHosts; - @Ignore @Transient Set> nsHostVKeys; + @Ignore + @ElementCollection + @JoinTable(name = "DomainHost") + @Convert(converter = HostResource.VKeyHostResourceConverter.class) + Set> nsHostVKeys; /** * The union of the contacts visible via {@link #getContacts} and {@link #getRegistrant}. diff --git a/core/src/main/java/google/registry/model/host/HostResource.java b/core/src/main/java/google/registry/model/host/HostResource.java index 5674e2a91..8ed476990 100644 --- a/core/src/main/java/google/registry/model/host/HostResource.java +++ b/core/src/main/java/google/registry/model/host/HostResource.java @@ -34,22 +34,25 @@ import google.registry.model.annotations.ReportedOn; import google.registry.model.domain.DomainBase; import google.registry.model.transfer.TransferData; import google.registry.persistence.VKey; +import google.registry.persistence.converter.VKeyConverter; import java.net.InetAddress; import java.util.Optional; import java.util.Set; import javax.annotation.Nullable; +import javax.persistence.ElementCollection; import org.joda.time.DateTime; /** * A persistable Host resource including mutable and non-mutable fields. * - *

A host's {@link TransferData} is stored on the superordinate domain. Non-subordinate hosts + *

A host's {@link TransferData} is stored on the superordinate domain. Non-subordinate hosts * don't carry a full set of TransferData; all they have is lastTransferTime. * * @see RFC 5732 */ @ReportedOn @Entity +@javax.persistence.Entity @ExternalMessagingName("host") public class HostResource extends EppResource implements ForeignKeyedEppResource { @@ -64,8 +67,7 @@ public class HostResource extends EppResource implements ForeignKeyedEppResource String fullyQualifiedHostName; /** IP Addresses for this host. Can be null if this is an external host. */ - @Index - Set inetAddresses; + @Index @ElementCollection Set inetAddresses; /** The superordinate domain of this host, or null if this is an external host. */ @Index @@ -210,4 +212,11 @@ public class HostResource extends EppResource implements ForeignKeyedEppResource return this; } } + + public static class VKeyHostResourceConverter extends VKeyConverter { + @Override + protected Class getAttributeClass() { + return HostResource.class; + } + } } diff --git a/core/src/main/resources/META-INF/persistence.xml b/core/src/main/resources/META-INF/persistence.xml index d73b3ed8e..0e2bdd5ea 100644 --- a/core/src/main/resources/META-INF/persistence.xml +++ b/core/src/main/resources/META-INF/persistence.xml @@ -20,6 +20,7 @@ * Use Hibernate's ServiceRegistry for bootstrapping (not JPA-compliant) --> google.registry.model.domain.DomainBase + google.registry.model.host.HostResource google.registry.model.registrar.Registrar google.registry.model.registrar.RegistrarContact google.registry.schema.domain.RegistryLock @@ -45,6 +46,7 @@ google.registry.persistence.converter.StringListConverter google.registry.persistence.converter.StringSetConverter google.registry.persistence.converter.UpdateAutoTimestampConverter + google.registry.persistence.converter.VKeyConverter google.registry.persistence.converter.ZonedDateTimeConverter diff --git a/core/src/test/java/google/registry/model/domain/DomainBaseSqlTest.java b/core/src/test/java/google/registry/model/domain/DomainBaseSqlTest.java index 20582e5a5..e4c5b6ebb 100644 --- a/core/src/test/java/google/registry/model/domain/DomainBaseSqlTest.java +++ b/core/src/test/java/google/registry/model/domain/DomainBaseSqlTest.java @@ -18,6 +18,7 @@ import static com.google.common.truth.Truth.assertThat; import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm; import static google.registry.util.DateTimeUtils.START_OF_TIME; import static org.joda.time.DateTimeZone.UTC; +import static org.junit.Assert.assertThrows; import com.google.common.collect.ImmutableSet; import com.googlecode.objectify.Key; @@ -27,11 +28,15 @@ import google.registry.model.domain.launch.LaunchNotice; import google.registry.model.domain.secdns.DelegationSignerData; import google.registry.model.eppcommon.AuthInfo.PasswordAuth; import google.registry.model.eppcommon.StatusValue; +import google.registry.model.host.HostResource; +import google.registry.persistence.VKey; 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.sql.SQLException; import javax.persistence.EntityManager; +import javax.persistence.RollbackException; import org.joda.time.DateTime; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Order; @@ -54,12 +59,16 @@ public class DomainBaseSqlTest { DomainBase domain; Key contactKey; Key contact2Key; + VKey host1VKey; + HostResource host; @BeforeEach public void setUp() { contactKey = Key.create(ContactResource.class, "contact_id1"); contact2Key = Key.create(ContactResource.class, "contact_id2"); + host1VKey = VKey.createSql(HostResource.class, "host1"); + domain = new DomainBase.Builder() .setFullyQualifiedDomainName("example.com") @@ -68,6 +77,7 @@ public class DomainBaseSqlTest { .setLastEppUpdateTime(fakeClock.nowUtc()) .setLastEppUpdateClientId("AnotherRegistrar") .setLastTransferTime(fakeClock.nowUtc()) + .setNameservers(host1VKey) .setStatusValues( ImmutableSet.of( StatusValue.CLIENT_DELETE_PROHIBITED, @@ -87,6 +97,12 @@ public class DomainBaseSqlTest { LaunchNotice.create("tcnid", "validatorId", START_OF_TIME, START_OF_TIME)) .setSmdId("smdid") .build(); + + host = + new HostResource.Builder() + .setRepoId("host1") + .setFullyQualifiedHostName("ns1.example.com") + .build(); } @Test @@ -97,6 +113,9 @@ public class DomainBaseSqlTest { // Persist the domain. EntityManager em = jpaTm().getEntityManager(); em.persist(domain); + + // Persist the host. + em.persist(host); }); jpaTm() @@ -125,4 +144,31 @@ public class DomainBaseSqlTest { assertThat(result).isEqualTo(org); }); } + + @Test + public void testForeignKeyConstraints() { + Exception e = + assertThrows( + RollbackException.class, + () -> { + jpaTm() + .transact( + () -> { + // Persist the domain without the associated host object. + EntityManager em = jpaTm().getEntityManager(); + em.persist(domain); + }); + }); + assertThat(e) + .hasCauseThat() // ConstraintViolationException + .hasCauseThat() // ConstraintViolationException + .hasCauseThat() + .isInstanceOf(SQLException.class); + assertThat(e) + .hasCauseThat() // ConstraintViolationException + .hasCauseThat() // ConstraintViolationException + .hasCauseThat() + .hasMessageThat() + .contains("\"DomainHost\" violates foreign key constraint \"fk_domainhost_host"); + } } diff --git a/db/src/main/resources/sql/flyway/V22__update_ns_hosts.sql b/db/src/main/resources/sql/flyway/V22__update_ns_hosts.sql new file mode 100644 index 000000000..69427ad31 --- /dev/null +++ b/db/src/main/resources/sql/flyway/V22__update_ns_hosts.sql @@ -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. + +create table "DomainHost" ( + domain_repo_id text not null, + ns_host_v_keys text +); + +create table "HostResource" ( + repo_id text not null, + creation_client_id text, + creation_time timestamptz, + current_sponsor_client_id text, + deletion_time timestamptz, + last_epp_update_client_id text, + last_epp_update_time timestamptz, + statuses text[], + fully_qualified_host_name text, + last_superordinate_change timestamptz, + last_transfer_time timestamptz, + superordinate_domain bytea, + primary key (repo_id) +); + +create table "HostResource_inetAddresses" ( + host_resource_repo_id text not null, + inet_addresses bytea +); + +alter table if exists "DomainHost" + add constraint FKfmi7bdink53swivs390m2btxg + foreign key (domain_repo_id) + references "Domain"; + +alter table if exists "DomainHost" + add constraint FK_DomainHost_host_valid + foreign key (ns_host_v_keys) + references "HostResource"; + +alter table if exists "HostResource_inetAddresses" + add constraint FK6unwhfkcu3oq6q347fxvpagv + foreign key (host_resource_repo_id) + references "HostResource"; diff --git a/db/src/main/resources/sql/schema/db-schema.sql.generated b/db/src/main/resources/sql/schema/db-schema.sql.generated index ededa2754..76a26d548 100644 --- a/db/src/main/resources/sql/schema/db-schema.sql.generated +++ b/db/src/main/resources/sql/schema/db-schema.sql.generated @@ -67,6 +67,11 @@ primary key (repo_id) ); + create table "DomainHost" ( + domain_repo_id text not null, + ns_host_v_keys text + ); + create table "GracePeriod" ( id bigserial not null, billing_event_one_time bytea, @@ -77,6 +82,27 @@ primary key (id) ); + create table "HostResource" ( + repo_id text not null, + creation_client_id text, + creation_time timestamptz, + current_sponsor_client_id text, + deletion_time timestamptz, + last_epp_update_client_id text, + last_epp_update_time timestamptz, + statuses text[], + fully_qualified_host_name text, + last_superordinate_change timestamptz, + last_transfer_time timestamptz, + superordinate_domain bytea, + primary key (repo_id) + ); + + create table "HostResource_inetAddresses" ( + host_resource_repo_id text not null, + inet_addresses bytea + ); + create table "Lock" ( resource_name text not null, tld text not null, @@ -222,6 +248,16 @@ create index reservedlist_name_idx on "ReservedList" (name); foreign key (revision_id) references "ClaimsList"; + alter table if exists "DomainHost" + add constraint FKeq1guccbre1yk3oosgp2io554 + foreign key (domain_repo_id) + references "Domain"; + + alter table if exists "HostResource_inetAddresses" + add constraint FK6unwhfkcu3oq6q347fxvpagv + foreign key (host_resource_repo_id) + references "HostResource"; + alter table if exists "PremiumEntry" add constraint FKo0gw90lpo1tuee56l0nb6y6g5 foreign key (revision_id) diff --git a/db/src/main/resources/sql/schema/nomulus.golden.sql b/db/src/main/resources/sql/schema/nomulus.golden.sql index 6f3b19399..47fd564b5 100644 --- a/db/src/main/resources/sql/schema/nomulus.golden.sql +++ b/db/src/main/resources/sql/schema/nomulus.golden.sql @@ -116,6 +116,46 @@ CREATE TABLE public."Domain" ( ); +-- +-- Name: DomainHost; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public."DomainHost" ( + domain_repo_id text NOT NULL, + ns_host_v_keys text +); + + +-- +-- Name: HostResource; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public."HostResource" ( + repo_id text NOT NULL, + creation_client_id text, + creation_time timestamp with time zone, + current_sponsor_client_id text, + deletion_time timestamp with time zone, + last_epp_update_client_id text, + last_epp_update_time timestamp with time zone, + statuses text[], + fully_qualified_host_name text, + last_superordinate_change timestamp with time zone, + last_transfer_time timestamp with time zone, + superordinate_domain bytea +); + + +-- +-- Name: HostResource_inetAddresses; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public."HostResource_inetAddresses" ( + host_resource_repo_id text NOT NULL, + inet_addresses bytea +); + + -- -- Name: Lock; Type: TABLE; Schema: public; Owner: - -- @@ -390,6 +430,14 @@ ALTER TABLE ONLY public."Domain" ADD CONSTRAINT "Domain_pkey" PRIMARY KEY (repo_id); +-- +-- Name: HostResource HostResource_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public."HostResource" + ADD CONSTRAINT "HostResource_pkey" PRIMARY KEY (repo_id); + + -- -- Name: Lock Lock_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -562,6 +610,30 @@ ALTER TABLE ONLY public."ClaimsEntry" ADD CONSTRAINT fk6sc6at5hedffc0nhdcab6ivuq FOREIGN KEY (revision_id) REFERENCES public."ClaimsList"(revision_id); +-- +-- Name: HostResource_inetAddresses fk6unwhfkcu3oq6q347fxvpagv; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public."HostResource_inetAddresses" + ADD CONSTRAINT fk6unwhfkcu3oq6q347fxvpagv FOREIGN KEY (host_resource_repo_id) REFERENCES public."HostResource"(repo_id); + + +-- +-- Name: DomainHost fk_domainhost_host_valid; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public."DomainHost" + ADD CONSTRAINT fk_domainhost_host_valid FOREIGN KEY (ns_host_v_keys) REFERENCES public."HostResource"(repo_id); + + +-- +-- Name: DomainHost fkfmi7bdink53swivs390m2btxg; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public."DomainHost" + ADD CONSTRAINT fkfmi7bdink53swivs390m2btxg FOREIGN KEY (domain_repo_id) REFERENCES public."Domain"(repo_id); + + -- -- Name: ReservedEntry fkgq03rk0bt1hb915dnyvd3vnfc; Type: FK CONSTRAINT; Schema: public; Owner: - --