aboutsummaryrefslogtreecommitdiffstats
path: root/controller-api
diff options
context:
space:
mode:
authorBjørn Christian Seime <bjorncs@verizonmedia.com>2021-09-24 14:27:56 +0200
committerBjørn Christian Seime <bjorncs@verizonmedia.com>2021-09-24 14:27:56 +0200
commitda99f37106766020c73ea6efd18008354aceea7d (patch)
tree1035e758252752eaa9ea1ed8bbf11a919274b0fd /controller-api
parent1a598e45369d1fdcfd46756bd782306adf5a74d0 (diff)
Move 'c.y.v.hosted.controller' to controller-api
Diffstat (limited to 'controller-api')
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/AthenzTenant.java73
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/CloudTenant.java87
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/DeletedTenant.java39
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/LastLoginInfo.java55
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/Tenant.java88
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TenantInfo.java127
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TenantInfoAddress.java95
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TenantInfoBillingContact.java74
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/package-info.java8
9 files changed, 646 insertions, 0 deletions
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/AthenzTenant.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/AthenzTenant.java
new file mode 100644
index 00000000000..7fa46031c98
--- /dev/null
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/AthenzTenant.java
@@ -0,0 +1,73 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.tenant;
+
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.vespa.athenz.api.AthenzDomain;
+import com.yahoo.vespa.hosted.controller.api.identifiers.Property;
+import com.yahoo.vespa.hosted.controller.api.identifiers.PropertyId;
+import com.yahoo.vespa.hosted.controller.api.integration.organization.Contact;
+
+import java.time.Instant;
+import java.util.Objects;
+import java.util.Optional;
+
+/**
+ * Represents an Athenz tenant in hosted Vespa.
+ *
+ * @author mpolden
+ */
+public class AthenzTenant extends Tenant {
+
+ private final AthenzDomain domain;
+ private final Property property;
+ private final Optional<PropertyId> propertyId;
+
+ /**
+ * This should only be used by serialization.
+ * Use {@link #create(TenantName, AthenzDomain, Property, Optional, Instant)}.
+ * */
+ public AthenzTenant(TenantName name, AthenzDomain domain, Property property, Optional<PropertyId> propertyId,
+ Optional<Contact> contact, Instant createdAt, LastLoginInfo lastLoginInfo) {
+ super(name, createdAt, lastLoginInfo, contact);
+ this.domain = Objects.requireNonNull(domain, "domain must be non-null");
+ this.property = Objects.requireNonNull(property, "property must be non-null");
+ this.propertyId = Objects.requireNonNull(propertyId, "propertyId must be non-null");
+ }
+
+ /** Property name of this tenant */
+ public Property property() {
+ return property;
+ }
+
+ /** Property ID of the tenant, if any */
+ public Optional<PropertyId> propertyId() {
+ return propertyId;
+ }
+
+ /** Athenz domain of this tenant */
+ public AthenzDomain domain() {
+ return domain;
+ }
+
+ /** Returns true if tenant is in given domain */
+ public boolean in(AthenzDomain domain) {
+ return this.domain.equals(domain);
+ }
+
+ @Override
+ public String toString() {
+ return "athenz tenant '" + name() + "'";
+ }
+
+ /** Create a new Athenz tenant */
+ public static AthenzTenant create(TenantName name, AthenzDomain domain, Property property,
+ Optional<PropertyId> propertyId, Instant createdAt) {
+ return new AthenzTenant(requireName(name), domain, property, propertyId, Optional.empty(), createdAt, LastLoginInfo.EMPTY);
+ }
+
+ @Override
+ public Type type() {
+ return Type.athenz;
+ }
+
+}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/CloudTenant.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/CloudTenant.java
new file mode 100644
index 00000000000..1060b118beb
--- /dev/null
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/CloudTenant.java
@@ -0,0 +1,87 @@
+// Copyright 2020 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.tenant;
+
+import com.google.common.collect.BiMap;
+import com.google.common.collect.ImmutableBiMap;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.text.Text;
+import com.yahoo.vespa.hosted.controller.api.integration.secrets.TenantSecretStore;
+
+import java.security.Principal;
+import java.security.PublicKey;
+import java.time.Instant;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.regex.Pattern;
+
+/**
+ * A paying tenant in a Vespa cloud service.
+ *
+ * @author jonmv
+ */
+public class CloudTenant extends Tenant {
+
+ private static final Pattern VALID_ARCHIVE_ACCESS_ROLE_PATTERN = Pattern.compile("arn:aws:iam::\\d{12}:.+");
+
+ private final Optional<Principal> creator;
+ private final BiMap<PublicKey, Principal> developerKeys;
+ private final TenantInfo info;
+ private final List<TenantSecretStore> tenantSecretStores;
+ private final Optional<String> archiveAccessRole;
+
+ /** Public for the serialization layer — do not use! */
+ public CloudTenant(TenantName name, Instant createdAt, LastLoginInfo lastLoginInfo, Optional<Principal> creator,
+ BiMap<PublicKey, Principal> developerKeys, TenantInfo info,
+ List<TenantSecretStore> tenantSecretStores, Optional<String> archiveAccessRole) {
+ super(name, createdAt, lastLoginInfo, Optional.empty());
+ this.creator = creator;
+ this.developerKeys = developerKeys;
+ this.info = Objects.requireNonNull(info);
+ this.tenantSecretStores = tenantSecretStores;
+ this.archiveAccessRole = archiveAccessRole;
+ if (!archiveAccessRole.map(role -> VALID_ARCHIVE_ACCESS_ROLE_PATTERN.matcher(role).matches()).orElse(true))
+ throw new IllegalArgumentException(Text.format("Invalid archive access role '%s': Must match expected pattern: '%s'",
+ archiveAccessRole.get(), VALID_ARCHIVE_ACCESS_ROLE_PATTERN.pattern()));
+ if (archiveAccessRole.map(role -> role.length() > 100).orElse(false))
+ throw new IllegalArgumentException("Invalid archive access role too long, must be 100 or less characters");
+ }
+
+ /** Creates a tenant with the given name, provided it passes validation. */
+ public static CloudTenant create(TenantName tenantName, Instant createdAt, Principal creator) {
+ return new CloudTenant(requireName(tenantName),
+ createdAt,
+ LastLoginInfo.EMPTY,
+ Optional.ofNullable(creator),
+ ImmutableBiMap.of(), TenantInfo.EMPTY, List.of(), Optional.empty());
+ }
+
+ /** The user that created the tenant */
+ public Optional<Principal> creator() {
+ return creator;
+ }
+
+ /** Legal name, addresses etc */
+ public TenantInfo info() {
+ return info;
+ }
+
+ /** An iam role which is allowed to access the S3 (log, dump) archive) */
+ public Optional<String> archiveAccessRole() {
+ return archiveAccessRole;
+ }
+
+ /** Returns the set of developer keys and their corresponding developers for this tenant. */
+ public BiMap<PublicKey, Principal> developerKeys() { return developerKeys; }
+
+ /** List of configured secret stores */
+ public List<TenantSecretStore> tenantSecretStores() {
+ return tenantSecretStores;
+ }
+
+ @Override
+ public Type type() {
+ return Type.cloud;
+ }
+
+}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/DeletedTenant.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/DeletedTenant.java
new file mode 100644
index 00000000000..cf6d73cb8f8
--- /dev/null
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/DeletedTenant.java
@@ -0,0 +1,39 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.tenant;
+
+import com.yahoo.config.provision.TenantName;
+
+import java.time.Instant;
+import java.util.Objects;
+import java.util.Optional;
+
+/**
+ * Represents a tenant that has been deleted. Exists to prevent creation of a new tenant with the same name.
+ *
+ * @author freva
+ */
+public class DeletedTenant extends Tenant {
+
+ private final Instant deletedAt;
+
+ public DeletedTenant(TenantName name, Instant createdAt, Instant deletedAt) {
+ super(name, createdAt, LastLoginInfo.EMPTY, Optional.empty());
+ this.deletedAt = Objects.requireNonNull(deletedAt, "deletedAt must be non-null");
+ }
+
+ /** Instant when the tenant was deleted */
+ public Instant deletedAt() {
+ return deletedAt;
+ }
+
+ @Override
+ public String toString() {
+ return "deleted tenant '" + name() + "'";
+ }
+
+ @Override
+ public Type type() {
+ return Type.deleted;
+ }
+
+}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/LastLoginInfo.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/LastLoginInfo.java
new file mode 100644
index 00000000000..15f2f97e7d1
--- /dev/null
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/LastLoginInfo.java
@@ -0,0 +1,55 @@
+// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.tenant;
+
+import java.time.Instant;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * @author freva
+ */
+public class LastLoginInfo {
+
+ public static final LastLoginInfo EMPTY = new LastLoginInfo(Map.of());
+
+ private final Map<UserLevel, Instant> lastLoginByUserLevel;
+
+ public LastLoginInfo(Map<UserLevel, Instant> lastLoginByUserLevel) {
+ this.lastLoginByUserLevel = Map.copyOf(lastLoginByUserLevel);
+ }
+
+ public Optional<Instant> get(UserLevel userLevel) {
+ return Optional.ofNullable(lastLoginByUserLevel.get(userLevel));
+ }
+
+ /**
+ * Returns new instance with updated last login time if the given {@code loginAt} timestamp is after the current
+ * for the given {@code userLevel}, otherwise returns this
+ */
+ public LastLoginInfo withLastLoginIfLater(UserLevel userLevel, Instant loginAt) {
+ Instant lastLogin = lastLoginByUserLevel.getOrDefault(userLevel, Instant.EPOCH);
+ if (loginAt.isAfter(lastLogin)) {
+ Map<UserLevel, Instant> lastLoginByUserLevel = new HashMap<>(this.lastLoginByUserLevel);
+ lastLoginByUserLevel.put(userLevel, loginAt);
+ return new LastLoginInfo(lastLoginByUserLevel);
+ }
+ return this;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ LastLoginInfo lastLoginInfo = (LastLoginInfo) o;
+ return lastLoginByUserLevel.equals(lastLoginInfo.lastLoginByUserLevel);
+ }
+
+ @Override
+ public int hashCode() {
+ return lastLoginByUserLevel.hashCode();
+ }
+
+ public enum UserLevel { user, developer, administrator };
+}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/Tenant.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/Tenant.java
new file mode 100644
index 00000000000..80982d70107
--- /dev/null
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/Tenant.java
@@ -0,0 +1,88 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.tenant;
+
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.vespa.hosted.controller.api.integration.organization.Contact;
+
+import java.time.Instant;
+import java.util.Objects;
+import java.util.Optional;
+
+/**
+ * A tenant in hosted Vespa.
+ *
+ * @author mpolden
+ */
+public abstract class Tenant {
+
+ private final TenantName name;
+ private final Instant createdAt;
+ private final LastLoginInfo lastLoginInfo;
+ private final Optional<Contact> contact;
+
+ Tenant(TenantName name, Instant createdAt, LastLoginInfo lastLoginInfo, Optional<Contact> contact) {
+ this.name = name;
+ this.createdAt = createdAt;
+ this.lastLoginInfo = lastLoginInfo;
+ this.contact = contact;
+ }
+
+ /** Name of this tenant */
+ public TenantName name() {
+ return name;
+ }
+
+ /** Instant when the tenant was created */
+ public Instant createdAt() {
+ return createdAt;
+ }
+
+ /** Returns login information for this tenant */
+ public LastLoginInfo lastLoginInfo() {
+ return lastLoginInfo;
+ }
+
+ /** Contact information for this tenant */
+ public Optional<Contact> contact() {
+ return contact;
+ }
+
+ public abstract Type type();
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Tenant tenant = (Tenant) o;
+ return Objects.equals(name, tenant.name);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(name);
+ }
+
+ public static TenantName requireName(TenantName name) {
+ if ( ! name.value().matches("^(?=.{1,20}$)[a-z](-?[a-z0-9]+)*$")) {
+ throw new IllegalArgumentException("New tenant or application names must start with a letter, may " +
+ "contain no more than 20 characters, and may only contain lowercase " +
+ "letters, digits or dashes, but no double-dashes.");
+ }
+ return name;
+ }
+
+
+ public enum Type {
+
+ /** Tenant authenticated through Athenz. */
+ athenz,
+
+ /** Tenant authenticated through some cloud identity provider. */
+ cloud,
+
+ /** Tenant has been deleted. */
+ deleted,
+
+ }
+
+}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TenantInfo.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TenantInfo.java
new file mode 100644
index 00000000000..81c08e1083b
--- /dev/null
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TenantInfo.java
@@ -0,0 +1,127 @@
+// Copyright 2020 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.tenant;
+
+import java.util.Objects;
+
+/**
+ * Tenant information beyond technical tenant id and user authorizations.
+ *
+ * This info is used to capture generic support information and invoiced billing information.
+ *
+ * All fields are non null but strings can be empty
+ *
+ * @author smorgrav
+ */
+public class TenantInfo {
+ private final String name;
+ private final String email;
+ private final String website;
+ private final String contactName;
+ private final String contactEmail;
+ private final String invoiceEmail;
+ private final TenantInfoAddress address;
+ private final TenantInfoBillingContact billingContact;
+
+ TenantInfo(String name, String email, String website, String contactName, String contactEmail,
+ String invoiceEmail, TenantInfoAddress address, TenantInfoBillingContact billingContact) {
+ this.name = Objects.requireNonNull(name);
+ this.email = Objects.requireNonNull(email);
+ this.website = Objects.requireNonNull(website);
+ this.contactName = Objects.requireNonNull(contactName);
+ this.contactEmail = Objects.requireNonNull(contactEmail);
+ this.invoiceEmail = Objects.requireNonNull(invoiceEmail);
+ this.address = Objects.requireNonNull(address);
+ this.billingContact = Objects.requireNonNull(billingContact);
+ }
+
+ public static final TenantInfo EMPTY = new TenantInfo("","","", "", "", "",
+ TenantInfoAddress.EMPTY, TenantInfoBillingContact.EMPTY);
+
+ public String name() {
+ return name;
+ }
+
+ public String email() {
+ return email;
+ }
+
+ public String website() {
+ return website;
+ }
+
+ public String contactName() {
+ return contactName;
+ }
+
+ public String contactEmail() {
+ return contactEmail;
+ }
+
+ public String invoiceEmail() {
+ return invoiceEmail;
+ }
+
+ public TenantInfoAddress address() {
+ return address;
+ }
+
+ public TenantInfoBillingContact billingContact() {
+ return billingContact;
+ }
+
+ public TenantInfo withName(String newName) {
+ return new TenantInfo(newName, email, website, contactName, contactEmail, invoiceEmail, address, billingContact);
+ }
+
+ public TenantInfo withEmail(String newEmail) {
+ return new TenantInfo(name, newEmail, website, contactName, contactEmail, invoiceEmail, address, billingContact);
+ }
+
+ public TenantInfo withWebsite(String newWebsite) {
+ return new TenantInfo(name, email, newWebsite, contactName, contactEmail, invoiceEmail, address, billingContact);
+ }
+
+ public TenantInfo withContactName(String newContactName) {
+ return new TenantInfo(name, email, website, newContactName, contactEmail, invoiceEmail, address, billingContact);
+ }
+
+ public TenantInfo withContactEmail(String newContactEmail) {
+ return new TenantInfo(name, email, website, contactName, newContactEmail, invoiceEmail, address, billingContact);
+ }
+
+ public TenantInfo withInvoiceEmail(String newInvoiceEmail) {
+ return new TenantInfo(name, email, website, contactName, contactEmail, newInvoiceEmail, address, billingContact);
+ }
+
+ public TenantInfo withAddress(TenantInfoAddress newAddress) {
+ return new TenantInfo(name, email, website, contactName, contactEmail, invoiceEmail, newAddress, billingContact);
+ }
+
+ public TenantInfo withBillingContact(TenantInfoBillingContact newBillingContact) {
+ return new TenantInfo(name, email, website, contactName, contactEmail, invoiceEmail, address, newBillingContact);
+ }
+
+ public boolean isEmpty() {
+ return this.equals(EMPTY);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ TenantInfo that = (TenantInfo) o;
+ return name.equals(that.name) &&
+ email.equals(that.email) &&
+ website.equals(that.website) &&
+ contactName.equals(that.contactName) &&
+ contactEmail.equals(that.contactEmail) &&
+ invoiceEmail.equals(that.invoiceEmail) &&
+ address.equals(that.address) &&
+ billingContact.equals(that.billingContact);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(name, email, website, contactName, contactEmail, invoiceEmail, address, billingContact);
+ }
+}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TenantInfoAddress.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TenantInfoAddress.java
new file mode 100644
index 00000000000..a12f351abd6
--- /dev/null
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TenantInfoAddress.java
@@ -0,0 +1,95 @@
+// Copyright 2020 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.tenant;
+
+import java.util.Objects;
+
+/**
+ * Address formats are quite diverse across the world both in therms of what fields are used, named and
+ * the order of them.
+ *
+ * To be generic a little future proof the address fields here are a mix of free text (address lines) and fixed fields.
+ * The address lines can be street address, P.O box, c/o name, apartment, suite, unit, building floor etc etc.
+ *
+ * All fields are mandatory but can be an empty string (ie. not null)
+ *
+ * @author smorgrav
+ */
+public class TenantInfoAddress {
+
+ private final String addressLines;
+ private final String postalCodeOrZip;
+ private final String city;
+ private final String stateRegionProvince;
+ private final String country;
+
+ TenantInfoAddress(String addressLines, String postalCodeOrZip, String city, String country, String stateRegionProvince) {
+ this.addressLines = Objects.requireNonNull(addressLines);;
+ this.city = Objects.requireNonNull(city);
+ this.postalCodeOrZip = Objects.requireNonNull(postalCodeOrZip);
+ this.country = Objects.requireNonNull(country);
+ this.stateRegionProvince = Objects.requireNonNull(stateRegionProvince);
+ }
+
+ public static final TenantInfoAddress EMPTY = new TenantInfoAddress("","","", "", "");
+
+ public String addressLines() {
+ return addressLines;
+ }
+
+ public String postalCodeOrZip() {
+ return postalCodeOrZip;
+ }
+
+ public String city() {
+ return city;
+ }
+
+ public String country() {
+ return country;
+ }
+
+ public String stateRegionProvince() {
+ return stateRegionProvince;
+ }
+
+ public TenantInfoAddress withAddressLines(String newAddressLines) {
+ return new TenantInfoAddress(newAddressLines, postalCodeOrZip, city, country, stateRegionProvince);
+ }
+
+ public TenantInfoAddress withPostalCodeOrZip(String newPostalCodeOrZip) {
+ return new TenantInfoAddress(addressLines, newPostalCodeOrZip, city, country, stateRegionProvince);
+ }
+
+ public TenantInfoAddress withCity(String newCity) {
+ return new TenantInfoAddress(addressLines, postalCodeOrZip, newCity, country, stateRegionProvince);
+ }
+
+ public TenantInfoAddress withCountry(String newCountry) {
+ return new TenantInfoAddress(addressLines, postalCodeOrZip, city, newCountry, stateRegionProvince);
+ }
+
+ public TenantInfoAddress withStateRegionProvince(String newStateRegionProvince) {
+ return new TenantInfoAddress(addressLines, postalCodeOrZip, city, country, newStateRegionProvince);
+ }
+
+ public boolean isEmpty() {
+ return this.equals(EMPTY);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ TenantInfoAddress that = (TenantInfoAddress) o;
+ return addressLines.equals(that.addressLines) &&
+ postalCodeOrZip.equals(that.postalCodeOrZip) &&
+ city.equals(that.city) &&
+ stateRegionProvince.equals(that.stateRegionProvince) &&
+ country.equals(that.country);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(addressLines, postalCodeOrZip, city, stateRegionProvince, country);
+ }
+}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TenantInfoBillingContact.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TenantInfoBillingContact.java
new file mode 100644
index 00000000000..a00dd626f0a
--- /dev/null
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TenantInfoBillingContact.java
@@ -0,0 +1,74 @@
+// Copyright 2020 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.tenant;
+
+import java.util.Objects;
+
+/**
+ * @author smorgrav
+ */
+public class TenantInfoBillingContact {
+ private final String name;
+ private final String email;
+ private final String phone;
+ private final TenantInfoAddress address;
+
+ TenantInfoBillingContact(String name, String email, String phone, TenantInfoAddress address) {
+ this.name = Objects.requireNonNull(name);
+ this.email = Objects.requireNonNull(email);
+ this.phone = Objects.requireNonNull(phone);
+ this.address = Objects.requireNonNull(address);
+ }
+
+ public static final TenantInfoBillingContact EMPTY =
+ new TenantInfoBillingContact("","", "", TenantInfoAddress.EMPTY);
+
+ public String name() {
+ return name;
+ }
+
+ public String email() { return email; }
+
+ public String phone() {
+ return phone;
+ }
+
+ public TenantInfoAddress address() {
+ return address;
+ }
+
+ public TenantInfoBillingContact withName(String newName) {
+ return new TenantInfoBillingContact(newName, email, phone, address);
+ }
+
+ public TenantInfoBillingContact withEmail(String newEmail) {
+ return new TenantInfoBillingContact(name, newEmail, phone, address);
+ }
+
+ public TenantInfoBillingContact withPhone(String newPhone) {
+ return new TenantInfoBillingContact(name, email, newPhone, address);
+ }
+
+ public TenantInfoBillingContact withAddress(TenantInfoAddress newAddress) {
+ return new TenantInfoBillingContact(name, email, phone, newAddress);
+ }
+
+ public boolean isEmpty() {
+ return this.equals(EMPTY);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ TenantInfoBillingContact that = (TenantInfoBillingContact) o;
+ return name.equals(that.name) &&
+ email.equals(that.email) &&
+ phone.equals(that.phone) &&
+ address.equals(that.address);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(name, email, phone, address);
+ }
+}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/package-info.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/package-info.java
new file mode 100644
index 00000000000..9218bfcd850
--- /dev/null
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/package-info.java
@@ -0,0 +1,8 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+/**
+ * @author mpolden
+ */
+@ExportPackage
+package com.yahoo.vespa.hosted.controller.tenant;
+
+import com.yahoo.osgi.annotation.ExportPackage;