diff options
author | Martin Polden <mpolden@mpolden.no> | 2018-09-04 10:58:44 +0200 |
---|---|---|
committer | Martin Polden <mpolden@mpolden.no> | 2018-09-04 13:51:10 +0200 |
commit | 4f70d12360597ac3f1c52464d40589c868f398aa (patch) | |
tree | 43d9ee8aae7b702f2d01c227d033c2163a19b889 /controller-server | |
parent | 0d3b0e4d53861b3ca12498303aa091e564625bed (diff) |
Periodically update and store tenant contact information
Diffstat (limited to 'controller-server')
15 files changed, 439 insertions, 105 deletions
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Controller.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Controller.java index ee0a6875796..794b248b27a 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Controller.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Controller.java @@ -12,8 +12,8 @@ import com.yahoo.vespa.curator.Lock; 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.BuildService; -import com.yahoo.vespa.hosted.controller.api.integration.RunDataStore; import com.yahoo.vespa.hosted.controller.api.integration.MetricsService; +import com.yahoo.vespa.hosted.controller.api.integration.RunDataStore; import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzClientFactory; import com.yahoo.vespa.hosted.controller.api.integration.chef.Chef; import com.yahoo.vespa.hosted.controller.api.integration.configserver.ConfigServer; @@ -79,7 +79,6 @@ public class Controller extends AbstractComponent { private final ConfigServer configServer; private final MetricsService metricsService; private final Chef chef; - private final Organization organization; private final AthenzClientFactory athenzClientFactory; /** @@ -117,7 +116,6 @@ public class Controller extends AbstractComponent { this.curator = Objects.requireNonNull(curator, "Curator cannot be null"); this.gitHub = Objects.requireNonNull(gitHub, "GitHub cannot be null"); this.entityService = Objects.requireNonNull(entityService, "EntityService cannot be null"); - this.organization = Objects.requireNonNull(organization, "Organization cannot be null"); this.globalRoutingService = Objects.requireNonNull(globalRoutingService, "GlobalRoutingService cannot be null"); this.zoneRegistry = Objects.requireNonNull(zoneRegistry, "ZoneRegistry cannot be null"); this.configServer = Objects.requireNonNull(configServer, "ConfigServer cannot be null"); @@ -136,7 +134,7 @@ public class Controller extends AbstractComponent { Objects.requireNonNull(routingGenerator, "RoutingGenerator cannot be null"), Objects.requireNonNull(buildService, "BuildService cannot be null"), clock); - tenantController = new TenantController(this, curator, athenzClientFactory); + tenantController = new TenantController(this, curator, athenzClientFactory, organization); // Record the version of this controller curator().writeControllerVersion(this.hostname(), Vtag.currentVersion); @@ -289,10 +287,6 @@ public class Controller extends AbstractComponent { return chef; } - public Organization organization() { - return organization; - } - public CuratorDb curator() { return curator; } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedTenant.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedTenant.java index c8a3b52b9fc..cb3f50d08c7 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedTenant.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedTenant.java @@ -7,9 +7,11 @@ import com.yahoo.vespa.curator.Lock; import com.yahoo.vespa.hosted.controller.api.identifiers.Property; import com.yahoo.vespa.hosted.controller.api.identifiers.PropertyId; import com.yahoo.vespa.hosted.controller.tenant.AthenzTenant; +import com.yahoo.vespa.hosted.controller.tenant.Contact; import java.util.Objects; import java.util.Optional; +import java.util.function.Consumer; /** * A tenant that has been locked for modification. Provides methods for modifying a tenant's fields. @@ -23,35 +25,47 @@ public class LockedTenant { private final AthenzDomain domain; private final Property property; private final Optional<PropertyId> propertyId; + private final Optional<Contact> contact; + /** + * Should never be constructed directly. + * + * Use {@link TenantController#lockIfPresent(TenantName, Consumer)} or + * {@link TenantController#lockOrThrow(TenantName, Consumer)} + */ LockedTenant(AthenzTenant tenant, Lock lock) { - this(lock, tenant.name(), tenant.domain(), tenant.property(), tenant.propertyId()); + this(lock, tenant.name(), tenant.domain(), tenant.property(), tenant.propertyId(), tenant.contact()); } private LockedTenant(Lock lock, TenantName name, AthenzDomain domain, Property property, - Optional<PropertyId> propertyId) { + Optional<PropertyId> propertyId, Optional<Contact> contact) { this.lock = Objects.requireNonNull(lock, "lock must be non-null"); this.name = Objects.requireNonNull(name, "name must be non-null"); 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, "propertId must be non-null"); + this.propertyId = Objects.requireNonNull(propertyId, "propertyId must be non-null"); + this.contact = Objects.requireNonNull(contact, "contact must be non-null"); } /** Returns a read-only copy of this */ public AthenzTenant get() { - return new AthenzTenant(name, domain, property, propertyId); + return new AthenzTenant(name, domain, property, propertyId, contact); } public LockedTenant with(AthenzDomain domain) { - return new LockedTenant(lock, name, domain, property, propertyId); + return new LockedTenant(lock, name, domain, property, propertyId, contact); } public LockedTenant with(Property property) { - return new LockedTenant(lock, name, domain, property, propertyId); + return new LockedTenant(lock, name, domain, property, propertyId, contact); } public LockedTenant with(PropertyId propertyId) { - return new LockedTenant(lock, name, domain, property, Optional.of(propertyId)); + return new LockedTenant(lock, name, domain, property, Optional.of(propertyId), contact); + } + + public LockedTenant with(Contact contact) { + return new LockedTenant(lock, name, domain, property, propertyId, Optional.of(contact)); } @Override diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/TenantController.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/TenantController.java index 16e160d0939..5f456553120 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/TenantController.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/TenantController.java @@ -10,14 +10,18 @@ import com.yahoo.vespa.curator.Lock; import com.yahoo.vespa.hosted.controller.api.identifiers.UserId; import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzClientFactory; import com.yahoo.vespa.hosted.controller.api.integration.athenz.ZmsClient; +import com.yahoo.vespa.hosted.controller.api.integration.organization.Organization; +import com.yahoo.vespa.hosted.controller.api.integration.organization.User; import com.yahoo.vespa.hosted.controller.persistence.CuratorDb; import com.yahoo.vespa.hosted.controller.tenant.AthenzTenant; +import com.yahoo.vespa.hosted.controller.tenant.Contact; import com.yahoo.vespa.hosted.controller.tenant.Tenant; import com.yahoo.vespa.hosted.controller.tenant.UserTenant; import java.util.Comparator; import java.util.HashSet; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.function.Consumer; @@ -34,18 +38,17 @@ public class TenantController { private static final Logger log = Logger.getLogger(TenantController.class.getName()); - /** The controller owning this */ private final Controller controller; - - /** For persistence */ private final CuratorDb curator; - private final AthenzClientFactory athenzClientFactory; + private final Organization organization; + + public TenantController(Controller controller, CuratorDb curator, AthenzClientFactory athenzClientFactory, Organization organization) { + this.controller = Objects.requireNonNull(controller, "controller must be non-null"); + this.curator = Objects.requireNonNull(curator, "curator must be non-null"); + this.athenzClientFactory = Objects.requireNonNull(athenzClientFactory, "athenzClientFactory must be non-null"); + this.organization = Objects.requireNonNull(organization, "organization must be non-null"); - public TenantController(Controller controller, CuratorDb curator, AthenzClientFactory athenzClientFactory) { - this.controller = controller; - this.curator = curator; - this.athenzClientFactory = athenzClientFactory; // Write all tenants to ensure persisted data uses latest serialization format for (Tenant tenant : curator.readTenants()) { try (Lock lock = lock(tenant.name())) { @@ -79,6 +82,24 @@ public class TenantController { } } + /** Find contact information for given tenant */ + // TODO: Move this to ContactInformationMaintainer + public Optional<Contact> findContact(AthenzTenant tenant) { + if (!tenant.propertyId().isPresent()) { + return Optional.empty(); + } + List<List<String>> persons = organization.contactsFor(tenant.propertyId().get()) + .stream() + .map(personList -> personList.stream() + .map(User::displayName) + .collect(Collectors.toList())) + .collect(Collectors.toList()); + return Optional.of(new Contact(organization.contactsUri(tenant.propertyId().get()), + organization.propertyUri(tenant.propertyId().get()), + organization.issueCreationUri(tenant.propertyId().get()), + persons)); + } + /** * Lock a tenant for modification and apply action. Only valid for Athenz tenants as it's the only type that * accepts modification. diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ContactInformationMaintainer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ContactInformationMaintainer.java new file mode 100644 index 00000000000..aaa9c09074b --- /dev/null +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ContactInformationMaintainer.java @@ -0,0 +1,44 @@ +// 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.maintenance; + +import com.yahoo.log.LogLevel; +import com.yahoo.vespa.hosted.controller.Controller; +import com.yahoo.vespa.hosted.controller.tenant.AthenzTenant; +import com.yahoo.vespa.hosted.controller.tenant.Tenant; +import com.yahoo.yolean.Exceptions; + +import java.time.Duration; +import java.util.logging.Logger; + +/** + * Periodically fetch and store contact information for tenants. + * + * @author mpolden + */ +public class ContactInformationMaintainer extends Maintainer { + + private static final Logger log = Logger.getLogger(ContactInformationMaintainer.class.getName()); + + public ContactInformationMaintainer(Controller controller, Duration interval, JobControl jobControl) { + super(controller, interval, jobControl); + } + + @Override + protected void maintain() { + for (Tenant t : controller().tenants().asList()) { + if (!(t instanceof AthenzTenant)) continue; // No contact information for non-Athenz tenants + AthenzTenant tenant = (AthenzTenant) t; + if (!tenant.propertyId().isPresent()) continue; // Can only update contact information if property ID is known + try { + controller().tenants().findContact(tenant).ifPresent(contact -> { + controller().tenants().lockIfPresent(t.name(), lockedTenant -> controller().tenants().store(lockedTenant.with(contact))); + }); + } catch (Exception e) { + log.log(LogLevel.WARNING, "Failed to update contact information for " + tenant + ": " + + Exceptions.toMessageString(e) + ". Retrying in " + + maintenanceInterval()); + } + } + } + +} diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ControllerMaintenance.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ControllerMaintenance.java index 2c65ea0e3cb..8256d9ca182 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ControllerMaintenance.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ControllerMaintenance.java @@ -5,13 +5,11 @@ import com.yahoo.component.AbstractComponent; import com.yahoo.jdisc.Metric; import com.yahoo.vespa.hosted.controller.Controller; import com.yahoo.vespa.hosted.controller.api.integration.chef.Chef; -import com.yahoo.vespa.hosted.controller.api.integration.deployment.TesterCloud; import com.yahoo.vespa.hosted.controller.api.integration.dns.NameService; import com.yahoo.vespa.hosted.controller.api.integration.noderepository.NodeRepositoryClientInterface; import com.yahoo.vespa.hosted.controller.api.integration.organization.DeploymentIssues; import com.yahoo.vespa.hosted.controller.api.integration.organization.OwnershipIssues; import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneId; -import com.yahoo.vespa.hosted.controller.deployment.InternalStepRunner; import com.yahoo.vespa.hosted.controller.maintenance.config.MaintainerConfig; import com.yahoo.vespa.hosted.controller.persistence.CuratorDb; @@ -47,6 +45,7 @@ public class ControllerMaintenance extends AbstractComponent { private final List<OsUpgrader> osUpgraders; private final OsVersionStatusUpdater osVersionStatusUpdater; private final JobRunner jobRunner; + private final ContactInformationMaintainer contactInformationMaintainer; @SuppressWarnings("unused") // instantiated by Dependency Injection public ControllerMaintenance(MaintainerConfig maintainerConfig, Controller controller, CuratorDb curator, @@ -71,6 +70,7 @@ public class ControllerMaintenance extends AbstractComponent { jobRunner = new JobRunner(controller, Duration.ofSeconds(30), jobControl); osUpgraders = osUpgraders(controller, jobControl); osVersionStatusUpdater = new OsVersionStatusUpdater(controller, maintenanceInterval, jobControl); + contactInformationMaintainer = new ContactInformationMaintainer(controller, Duration.ofHours(12), jobControl); } public Upgrader upgrader() { return upgrader; } @@ -96,6 +96,7 @@ public class ControllerMaintenance extends AbstractComponent { osUpgraders.forEach(Maintainer::deconstruct); osVersionStatusUpdater.deconstruct(); jobRunner.deconstruct(); + contactInformationMaintainer.deconstruct(); } /** Create one OS upgrader per cloud found in the zone registry of controller */ diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializer.java index d55dc791462..28400b85306 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializer.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializer.java @@ -3,6 +3,7 @@ package com.yahoo.vespa.hosted.controller.persistence; import com.yahoo.config.provision.TenantName; +import com.yahoo.slime.ArrayTraverser; import com.yahoo.slime.Cursor; import com.yahoo.slime.Inspector; import com.yahoo.slime.Slime; @@ -11,8 +12,12 @@ import com.yahoo.vespa.config.SlimeUtils; import com.yahoo.vespa.hosted.controller.api.identifiers.Property; import com.yahoo.vespa.hosted.controller.api.identifiers.PropertyId; import com.yahoo.vespa.hosted.controller.tenant.AthenzTenant; +import com.yahoo.vespa.hosted.controller.tenant.Contact; import com.yahoo.vespa.hosted.controller.tenant.UserTenant; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; import java.util.Optional; /** @@ -26,6 +31,12 @@ public class TenantSerializer { private static final String athenzDomainField = "athenzDomain"; private static final String propertyField = "property"; private static final String propertyIdField = "propertyId"; + private static final String contactField = "contact"; + private static final String contactUrlField = "contactUrl"; + private static final String propertyUrlField = "propertyUrl"; + private static final String issueTrackerUrlField = "issueTrackerUrl"; + private static final String personsField = "persons"; + private static final String personField = "person"; public Slime toSlime(AthenzTenant tenant) { Slime slime = new Slime(); @@ -34,6 +45,20 @@ public class TenantSerializer { root.setString(athenzDomainField, tenant.domain().getName()); root.setString(propertyField, tenant.property().id()); tenant.propertyId().ifPresent(propertyId -> root.setString(propertyIdField, propertyId.id())); + tenant.contact().ifPresent(contact -> { + Cursor contactObject = root.setObject(contactField); + contactObject.setString(contactUrlField, contact.url().toString()); + contactObject.setString(propertyUrlField, contact.propertyUrl().toString()); + contactObject.setString(issueTrackerUrlField, contact.issueTrackerUrl().toString()); + Cursor personsArray = contactObject.setArray(personsField); + contact.persons().forEach(personList -> { + Cursor personArray = personsArray.addArray(); + personList.forEach(person -> { + Cursor personObject = personArray.addObject(); + personObject.setString(personField, person); + }); + }); + }); return slime; } @@ -50,7 +75,8 @@ public class TenantSerializer { AthenzDomain domain = new AthenzDomain(root.field(athenzDomainField).asString()); Property property = new Property(root.field(propertyField).asString()); Optional<PropertyId> propertyId = SlimeUtils.optionalString(root.field(propertyIdField)).map(PropertyId::new); - return new AthenzTenant(name, domain, property, propertyId); + Optional<Contact> contact = contactFrom(root.field(contactField)); + return new AthenzTenant(name, domain, property, propertyId, contact); } public UserTenant userTenantFrom(Slime slime) { @@ -59,4 +85,24 @@ public class TenantSerializer { return new UserTenant(name); } + private Optional<Contact> contactFrom(Inspector object) { + if (!object.valid()) { + return Optional.empty(); + } + return Optional.of(new Contact(URI.create(object.field(contactUrlField).asString()), + URI.create(object.field(propertyUrlField).asString()), + URI.create(object.field(issueTrackerUrlField).asString()), + personsFrom(object.field(personsField)))); + } + + private List<List<String>> personsFrom(Inspector array) { + List<List<String>> personLists = new ArrayList<>(); + array.traverse((ArrayTraverser) (i, personArray) -> { + List<String> persons = new ArrayList<>(); + personArray.traverse((ArrayTraverser) (j, inspector) -> persons.add(inspector.field("person").asString())); + personLists.add(persons); + }); + return personLists; + } + } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java index 4acede561b9..4c924c60e61 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java @@ -16,6 +16,7 @@ import com.yahoo.container.jdisc.HttpResponse; import com.yahoo.container.jdisc.LoggingRequestHandler; import com.yahoo.io.IOUtils; import com.yahoo.log.LogLevel; +import com.yahoo.restapi.Path; import com.yahoo.slime.Cursor; import com.yahoo.slime.Inspector; import com.yahoo.slime.Slime; @@ -50,7 +51,6 @@ import com.yahoo.vespa.hosted.controller.api.integration.configserver.ConfigServ import com.yahoo.vespa.hosted.controller.api.integration.configserver.Log; import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType; import com.yahoo.vespa.hosted.controller.api.integration.deployment.RunId; -import com.yahoo.vespa.hosted.controller.api.integration.organization.User; import com.yahoo.vespa.hosted.controller.api.integration.routing.RotationStatus; import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneId; import com.yahoo.vespa.hosted.controller.application.ApplicationPackage; @@ -66,12 +66,12 @@ import com.yahoo.vespa.hosted.controller.application.JobStatus; import com.yahoo.vespa.hosted.controller.application.SourceRevision; import com.yahoo.vespa.hosted.controller.restapi.ErrorResponse; import com.yahoo.vespa.hosted.controller.restapi.MessageResponse; -import com.yahoo.restapi.Path; import com.yahoo.vespa.hosted.controller.restapi.ResourceResponse; import com.yahoo.vespa.hosted.controller.restapi.SlimeJsonResponse; import com.yahoo.vespa.hosted.controller.restapi.StringResponse; import com.yahoo.vespa.hosted.controller.restapi.filter.SetBouncerPassthruHeaderFilter; import com.yahoo.vespa.hosted.controller.tenant.AthenzTenant; +import com.yahoo.vespa.hosted.controller.tenant.Contact; import com.yahoo.vespa.hosted.controller.tenant.Tenant; import com.yahoo.vespa.hosted.controller.tenant.UserTenant; import com.yahoo.vespa.hosted.controller.versions.VespaVersion; @@ -905,13 +905,11 @@ public class ApplicationApiHandler extends LoggingRequestHandler { private void toSlime(Cursor object, Tenant tenant, HttpRequest request, boolean listApplications) { object.setString("tenant", tenant.name().value()); object.setString("type", tentantType(tenant)); - Optional<PropertyId> propertyId = Optional.empty(); if (tenant instanceof AthenzTenant) { AthenzTenant athenzTenant = (AthenzTenant) tenant; object.setString("athensDomain", athenzTenant.domain().getName()); object.setString("property", athenzTenant.property().id()); - propertyId = athenzTenant.propertyId(); - propertyId.ifPresent(id -> object.setString("propertyId", id.toString())); + athenzTenant.propertyId().ifPresent(id -> object.setString("propertyId", id.toString())); } Cursor applicationArray = object.setArray("applications"); if (listApplications) { // This cludge is needed because we call this after deleting the tenant. As this call makes another tenant lookup it will fail. TODO is to support lookup on tenant @@ -924,23 +922,23 @@ public class ApplicationApiHandler extends LoggingRequestHandler { } } } - propertyId.ifPresent(id -> { - try { - object.setString("propertyUrl", controller.organization().propertyUri(id).toString()); - object.setString("contactsUrl", controller.organization().contactsUri(id).toString()); - object.setString("issueCreationUrl", controller.organization().issueCreationUri(id).toString()); - Cursor lists = object.setArray("contacts"); - for (List<? extends User> contactList : controller.organization().contactsFor(id)) { - Cursor list = lists.addArray(); - for (User contact : contactList) - list.addString(contact.displayName()); - } - } - catch (RuntimeException e) { - log.log(Level.WARNING, "Error fetching property info for " + tenant + " with propertyId " + id + ": " + - Exceptions.toMessageString(e)); + if (tenant instanceof AthenzTenant) { + AthenzTenant athenzTenant = (AthenzTenant) tenant; + Optional<Contact> contact = athenzTenant.contact(); + if (!contact.isPresent()) { // TODO: Remove this fallback once all contacts have been written once + contact = controller.tenants().findContact(athenzTenant); } - }); + contact.ifPresent(c -> { + object.setString("propertyUrl", c.propertyUrl().toString()); + object.setString("contactsUrl", c.url().toString()); + object.setString("issueCreationUrl", c.issueTrackerUrl().toString()); + Cursor contactsArray = object.setArray("contacts"); + c.persons().forEach(persons -> { + Cursor personArray = contactsArray.addArray(); + persons.forEach(personArray::addString); + }); + }); + } } // A tenant has different content when in a list ... antipattern, but not solvable before application/v5 diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/AthenzTenant.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/AthenzTenant.java index 52d205e4eeb..8cbb4e06aca 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/AthenzTenant.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/AthenzTenant.java @@ -6,6 +6,7 @@ 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 java.util.Objects; import java.util.Optional; /** @@ -18,16 +19,19 @@ public class AthenzTenant extends Tenant { private final AthenzDomain domain; private final Property property; private final Optional<PropertyId> propertyId; + private final Optional<Contact> contact; /** * This should only be used by serialization. * Use {@link #create(TenantName, AthenzDomain, Property, Optional)}. * */ - public AthenzTenant(TenantName name, AthenzDomain domain, Property property, Optional<PropertyId> propertyId) { + public AthenzTenant(TenantName name, AthenzDomain domain, Property property, Optional<PropertyId> propertyId, + Optional<Contact> contact) { super(name); - this.domain = domain; - this.property = property; - this.propertyId = propertyId; + 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"); + this.contact = Objects.requireNonNull(contact, "contact must be non-null"); } /** Property name of this tenant */ @@ -35,11 +39,16 @@ public class AthenzTenant extends Tenant { return property; } - /** Property ID of the tenant, if present */ + /** Property ID of the tenant, if any */ public Optional<PropertyId> propertyId() { return propertyId; } + /** Contact information for this, if any */ + public Optional<Contact> contact() { + return contact; + } + /** Athenz domain of this tenant */ public AthenzDomain domain() { return domain; @@ -58,7 +67,7 @@ public class AthenzTenant extends Tenant { /** Create a new Athenz tenant */ public static AthenzTenant create(TenantName name, AthenzDomain domain, Property property, Optional<PropertyId> propertyId) { - return new AthenzTenant(requireName(requireNoPrefix(name)), domain, property, propertyId); + return new AthenzTenant(requireName(requireNoPrefix(name)), domain, property, propertyId, Optional.empty()); } private static TenantName requireNoPrefix(TenantName name) { diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/Contact.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/Contact.java new file mode 100644 index 00000000000..e13b0f982da --- /dev/null +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/Contact.java @@ -0,0 +1,75 @@ +// 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.google.common.collect.ImmutableList; + +import java.net.URI; +import java.util.List; +import java.util.Objects; + +/** + * Contact information for a tenant. + * + * @author mpolden + */ +public class Contact { + + private final URI url; + private final URI propertyUrl; + private final URI issueTrackerUrl; + private final List<List<String>> persons; + + public Contact(URI url, URI propertyUrl, URI issueTrackerUrl, List<List<String>> persons) { + this.propertyUrl = Objects.requireNonNull(propertyUrl, "propertyUrl must be non-null"); + this.url = Objects.requireNonNull(url, "url must be non-null"); + this.issueTrackerUrl = Objects.requireNonNull(issueTrackerUrl, "issueTrackerUrl must be non-null"); + this.persons = ImmutableList.copyOf(Objects.requireNonNull(persons, "persons must be non-null")); + } + + /** URL to this */ + public URI url() { + return url; + } + + /** URL to information about this property */ + public URI propertyUrl() { + return propertyUrl; + } + + /** URL to this contacts's issue tracker */ + public URI issueTrackerUrl() { + return issueTrackerUrl; + } + + /** Nested list of persons representing this. First level represents that person's rank in the corporate dystopia. */ + public List<List<String>> persons() { + return persons; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Contact contact = (Contact) o; + return Objects.equals(url, contact.url) && + Objects.equals(propertyUrl, contact.propertyUrl) && + Objects.equals(issueTrackerUrl, contact.issueTrackerUrl) && + Objects.equals(persons, contact.persons); + } + + @Override + public int hashCode() { + return Objects.hash(url, propertyUrl, issueTrackerUrl, persons); + } + + @Override + public String toString() { + return "Contact{" + + "url=" + url + + ", propertyUrl=" + propertyUrl + + ", issueTrackerUrl=" + issueTrackerUrl + + ", persons=" + persons + + '}'; + } + +} diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTester.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTester.java index c067bccb4c3..367e4e52e79 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTester.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTester.java @@ -78,6 +78,7 @@ public final class ControllerTester { private final MockBuildService buildService; private final MetricsServiceMock metricsService; private final RoutingGeneratorMock routingGenerator; + private final MockOrganization organization; private Controller controller; @@ -87,7 +88,7 @@ public final class ControllerTester { new ZoneRegistryMock(), new GitHubMock(), curatorDb, rotationsConfig, new MemoryNameService(), new ArtifactRepositoryMock(), new ApplicationStoreMock(), new MemoryEntityService(), new MockBuildService(), - metricsService, new RoutingGeneratorMock()); + metricsService, new RoutingGeneratorMock(), new MockOrganization(clock)); } public ControllerTester(ManualClock clock) { @@ -112,7 +113,8 @@ public final class ControllerTester { MemoryNameService nameService, ArtifactRepositoryMock artifactRepository, ApplicationStoreMock appStoreMock, EntityService entityService, MockBuildService buildService, - MetricsServiceMock metricsService, RoutingGeneratorMock routingGenerator) { + MetricsServiceMock metricsService, RoutingGeneratorMock routingGenerator, + MockOrganization organization) { this.athenzDb = athenzDb; this.clock = clock; this.configServer = configServer; @@ -127,9 +129,10 @@ public final class ControllerTester { this.buildService = buildService; this.metricsService = metricsService; this.routingGenerator = routingGenerator; + this.organization = organization; this.controller = createController(curator, rotationsConfig, configServer, clock, gitHub, zoneRegistry, athenzDb, nameService, artifactRepository, appStoreMock, entityService, buildService, - metricsService, routingGenerator); + metricsService, routingGenerator, organization); // Make root logger use time from manual clock configureDefaultLogHandler(handler -> handler.setFilter( @@ -175,11 +178,15 @@ public final class ControllerTester { public RoutingGeneratorMock routingGenerator() { return routingGenerator; } + public MockOrganization organization() { + return organization; + } + /** Create a new controller instance. Useful to verify that controller state is rebuilt from persistence */ public final void createNewController() { controller = createController(curator, rotationsConfig, configServer, clock, gitHub, zoneRegistry, athenzDb, nameService, artifactRepository, applicationStore, entityService, buildService, metricsService, - routingGenerator); + routingGenerator, organization); } /** Creates the given tenant and application and deploys it */ @@ -197,12 +204,6 @@ public final class ControllerTester { } /** Creates the given tenant and application and deploys it */ - public Application createAndDeploy(String tenantName, String domainName, String applicationName, - String instanceName, Environment environment, long projectId, Long propertyId) { - return createAndDeploy(tenantName, domainName, applicationName, instanceName, toZone(environment), projectId, propertyId); - } - - /** Creates the given tenant and application and deploys it */ public Application createAndDeploy(String tenantName, String domainName, String applicationName, ZoneId zone, long projectId, Long propertyId) { return createAndDeploy(tenantName, domainName, applicationName, "default", zone, projectId, propertyId); } @@ -295,12 +296,12 @@ public final class ControllerTester { ArtifactRepository artifactRepository, ApplicationStore applicationStore, EntityService entityService, BuildService buildService, MetricsServiceMock metricsService, - RoutingGenerator routingGenerator) { + RoutingGenerator routingGenerator, MockOrganization organization) { Controller controller = new Controller(curator, rotationsConfig, gitHub, entityService, - new MockOrganization(clock), + organization, new MemoryGlobalRoutingService(), zoneRegistryMock, configServer, diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ContactInformationMaintainerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ContactInformationMaintainerTest.java new file mode 100644 index 00000000000..e67fa6c2b46 --- /dev/null +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ContactInformationMaintainerTest.java @@ -0,0 +1,75 @@ +// 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.maintenance; + +import com.yahoo.config.provision.TenantName; +import com.yahoo.vespa.hosted.controller.ControllerTester; +import com.yahoo.vespa.hosted.controller.api.identifiers.PropertyId; +import com.yahoo.vespa.hosted.controller.api.integration.organization.User; +import com.yahoo.vespa.hosted.controller.tenant.AthenzTenant; +import com.yahoo.vespa.hosted.controller.tenant.Contact; +import org.junit.Before; +import org.junit.Test; + +import java.net.URI; +import java.time.Duration; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * @author mpolden + */ +public class ContactInformationMaintainerTest { + + private ControllerTester tester; + private ContactInformationMaintainer maintainer; + + @Before + public void before() { + tester = new ControllerTester(); + maintainer = new ContactInformationMaintainer(tester.controller(), Duration.ofDays(1), new JobControl(tester.controller().curator())); + } + + @Test + public void updates_contact_information() { + long propertyId = 1; + TenantName name = tester.createTenant("tenant1", "domain1", propertyId); + Supplier<AthenzTenant> tenant = () -> tester.controller().tenants().requireAthenzTenant(name); + assertFalse("No contact information initially", tenant.get().contact().isPresent()); + + Contact contact = testContact(); + registerContact(propertyId, contact); + maintainer.run(); + + assertTrue("Contact information added", tenant.get().contact().isPresent()); + assertEquals(contact, tenant.get().contact().get()); + } + + private void registerContact(long propertyId, Contact contact) { + PropertyId p = new PropertyId(String.valueOf(propertyId)); + tester.organization().addProperty(p) + .setContactsUrl(p, contact.url()) + .setIssueUrl(p, contact.issueTrackerUrl()) + .setPropertyUrl(p, contact.propertyUrl()) + .setContactsFor(p, contact.persons().stream().map(persons -> persons.stream() + .map(User::from) + .collect(Collectors.toList())) + .collect(Collectors.toList())); + } + + private static Contact testContact() { + URI contactUrl = URI.create("http://contact1.test"); + URI issueTrackerUrl = URI.create("http://issue-tracker1.test"); + URI propertyUrl = URI.create("http://property1.test"); + List<List<String>> persons = Arrays.asList(Collections.singletonList("alice"), + Collections.singletonList("bob")); + return new Contact(contactUrl, propertyUrl, issueTrackerUrl, persons); + } + +} diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializerTest.java index fd909482072..38b09024cdf 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializerTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializerTest.java @@ -5,9 +5,13 @@ 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.tenant.AthenzTenant; +import com.yahoo.vespa.hosted.controller.tenant.Contact; import com.yahoo.vespa.hosted.controller.tenant.UserTenant; import org.junit.Test; +import java.net.URI; +import java.util.Arrays; +import java.util.Collections; import java.util.Optional; import static org.junit.Assert.assertEquals; @@ -47,6 +51,25 @@ public class TenantSerializerTest { } @Test + public void athenz_tenant_with_contact() { + AthenzTenant tenant = new AthenzTenant(TenantName.from("athenz-tenant"), + new AthenzDomain("domain1"), + new Property("property1"), + Optional.of(new PropertyId("1")), + Optional.of(new Contact( + URI.create("http://contact1.test"), + URI.create("http://property1.test"), + URI.create("http://issue-tracker-1.test"), + Arrays.asList( + Collections.singletonList("person1"), + Collections.singletonList("person2") + ) + ))); + AthenzTenant serialized = serializer.athenzTenantFrom(serializer.toSlime(tenant)); + assertEquals(tenant.contact(), serialized.contact()); + } + + @Test public void user_tenant() { UserTenant tenant = UserTenant.create("by-foo"); UserTenant serialized = serializer.userTenantFrom(serializer.toSlime(tenant)); diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java index 017479ecc90..13092451d4b 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java @@ -46,6 +46,8 @@ import com.yahoo.vespa.hosted.controller.athenz.mock.AthenzDbMock; import com.yahoo.vespa.hosted.controller.deployment.ApplicationPackageBuilder; import com.yahoo.vespa.hosted.controller.deployment.BuildJob; import com.yahoo.vespa.hosted.controller.integration.ConfigServerMock; +import com.yahoo.vespa.hosted.controller.maintenance.ContactInformationMaintainer; +import com.yahoo.vespa.hosted.controller.maintenance.JobControl; import com.yahoo.vespa.hosted.controller.restapi.ContainerControllerTester; import com.yahoo.vespa.hosted.controller.restapi.ContainerTester; import com.yahoo.vespa.hosted.controller.restapi.ControllerContainerTest; @@ -53,6 +55,7 @@ import com.yahoo.vespa.hosted.controller.tenant.AthenzTenant; import org.apache.http.HttpEntity; import org.apache.http.entity.ContentType; import org.apache.http.entity.mime.MultipartEntityBuilder; +import org.junit.Before; import org.junit.Test; import java.io.ByteArrayOutputStream; @@ -61,6 +64,7 @@ import java.io.IOException; import java.io.UncheckedIOException; import java.net.URI; import java.nio.charset.StandardCharsets; +import java.time.Duration; import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; @@ -108,10 +112,18 @@ public class ApplicationApiTest extends ControllerContainerTest { private static final ZoneId TEST_ZONE = ZoneId.from(Environment.test, RegionName.from("us-east-1")); private static final ZoneId STAGING_ZONE = ZoneId.from(Environment.staging, RegionName.from("us-east-3")); + + private ContainerControllerTester controllerTester; + private ContainerTester tester; + + @Before + public void before() { + controllerTester = new ContainerControllerTester(container, responseFiles); + tester = controllerTester.containerTester(); + } + @Test - public void testApplicationApi() throws Exception { - ContainerControllerTester controllerTester = new ContainerControllerTester(container, responseFiles); - ContainerTester tester = controllerTester.containerTester(); + public void testApplicationApi() { tester.computeVersionStatus(); createAthenzDomainWithAdmin(ATHENZ_TENANT_DOMAIN, USER_ID); // (Necessary but not provided in this API) @@ -151,7 +163,8 @@ public class ApplicationApiTest extends ControllerContainerTest { // Add another Athens domain, so we can try to create more tenants createAthenzDomainWithAdmin(ATHENZ_TENANT_DOMAIN_2, USER_ID); // New domain to test tenant w/property ID // Add property info for that property id, as well, in the mock organization. - addPropertyData((MockOrganization) controllerTester.controller().organization(), "1234"); + registerContact(1234); + // POST (add) a tenant with property ID tester.assertResponse(request("/application/v4/tenant/tenant2", POST) .userIdentity(USER_ID) @@ -164,9 +177,10 @@ public class ApplicationApiTest extends ControllerContainerTest { .nToken(N_TOKEN) .data("{\"athensDomain\":\"domain2\", \"property\":\"property2\", \"propertyId\":\"1234\"}"), new File("tenant-without-applications-with-id.json")); - // GET a tenant with property ID + // GET a tenant with property ID and contact information + updateContactInformation(); tester.assertResponse(request("/application/v4/tenant/tenant2", GET).userIdentity(USER_ID), - new File("tenant-without-applications-with-id.json")); + new File("tenant-with-contact-info.json")); // POST (create) an application tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", POST) @@ -465,8 +479,6 @@ public class ApplicationApiTest extends ControllerContainerTest { @Test public void testDeployDirectly() { // Setup - ContainerControllerTester controllerTester = new ContainerControllerTester(container, responseFiles); - ContainerTester tester = controllerTester.containerTester(); tester.computeVersionStatus(); createAthenzDomainWithAdmin(ATHENZ_TENANT_DOMAIN, USER_ID); @@ -500,8 +512,6 @@ public class ApplicationApiTest extends ControllerContainerTest { @Test public void testDeployDirectlyUsingOneCallForDeploy() { // Setup - ContainerControllerTester controllerTester = new ContainerControllerTester(container, responseFiles); - ContainerTester tester = controllerTester.containerTester(); tester.computeVersionStatus(); UserId userId = new UserId("new_user"); createAthenzDomainWithAdmin(ATHENZ_TENANT_DOMAIN, userId); @@ -523,10 +533,8 @@ public class ApplicationApiTest extends ControllerContainerTest { } @Test - public void testSortsDeploymentsAndJobs() throws Exception { + public void testSortsDeploymentsAndJobs() { // Setup - ContainerControllerTester controllerTester = new ContainerControllerTester(container, responseFiles); - ContainerTester tester = controllerTester.containerTester(); tester.computeVersionStatus(); createAthenzDomainWithAdmin(ATHENZ_TENANT_DOMAIN, USER_ID); @@ -602,7 +610,6 @@ public class ApplicationApiTest extends ControllerContainerTest { @Test public void testErrorResponses() throws Exception { - ContainerTester tester = new ContainerTester(container, responseFiles); tester.computeVersionStatus(); createAthenzDomainWithAdmin(ATHENZ_TENANT_DOMAIN, USER_ID); @@ -749,7 +756,7 @@ public class ApplicationApiTest extends ControllerContainerTest { // Create legancy tenant name containing underscores tester.controller().tenants().create(new AthenzTenant(TenantName.from("my_tenant"), ATHENZ_TENANT_DOMAIN, - new Property("property1"), Optional.empty()), + new Property("property1"), Optional.empty(), Optional.empty()), N_TOKEN); // POST (add) a Athenz tenant with dashes duplicates existing one with underscores tester.assertResponse(request("/application/v4/tenant/my-tenant", POST) @@ -761,8 +768,7 @@ public class ApplicationApiTest extends ControllerContainerTest { } @Test - public void testAuthorization() throws Exception { - ContainerTester tester = new ContainerTester(container, responseFiles); + public void testAuthorization() { UserId authorizedUser = USER_ID; UserId unauthorizedUser = new UserId("othertenant"); @@ -855,9 +861,7 @@ public class ApplicationApiTest extends ControllerContainerTest { } @Test - public void deployment_fails_on_illegal_domain_in_deployment_spec() throws IOException { - ContainerControllerTester controllerTester = new ContainerControllerTester(container, responseFiles); - ContainerTester tester = controllerTester.containerTester(); + public void deployment_fails_on_illegal_domain_in_deployment_spec() { ApplicationPackage applicationPackage = new ApplicationPackageBuilder() .upgradePolicy("default") .athenzIdentity(com.yahoo.config.provision.AthenzDomain.from("invalid.domain"), com.yahoo.config.provision.AthenzService.from("service")) @@ -881,8 +885,6 @@ public class ApplicationApiTest extends ControllerContainerTest { @Test public void deployment_succeeds_when_correct_domain_is_used() { - ContainerControllerTester controllerTester = new ContainerControllerTester(container, responseFiles); - ContainerTester tester = controllerTester.containerTester(); ApplicationPackage applicationPackage = new ApplicationPackageBuilder() .upgradePolicy("default") .athenzIdentity(com.yahoo.config.provision.AthenzDomain.from("domain1"), com.yahoo.config.provision.AthenzService.from("service")) @@ -912,11 +914,10 @@ public class ApplicationApiTest extends ControllerContainerTest { @Test public void testJobStatusReporting() { - ContainerControllerTester tester = new ContainerControllerTester(container, responseFiles); addUserToHostedOperatorRole(HostedAthenzIdentities.from(HOSTED_VESPA_OPERATOR)); - tester.containerTester().computeVersionStatus(); + tester.computeVersionStatus(); long projectId = 1; - Application app = tester.createApplication(); + Application app = controllerTester.createApplication(); ApplicationPackage applicationPackage = new ApplicationPackageBuilder() .environment(Environment.prod) .region("corp-us-east-1") @@ -924,11 +925,11 @@ public class ApplicationApiTest extends ControllerContainerTest { Version vespaVersion = new Version("6.1"); // system version from mock config server client - BuildJob job = new BuildJob(report -> notifyCompletion(report, tester), tester.artifactRepository()) + BuildJob job = new BuildJob(report -> notifyCompletion(report, controllerTester), controllerTester.artifactRepository()) .application(app) .projectId(projectId); job.type(JobType.component).uploadArtifact(applicationPackage).submit(); - tester.deploy(app, applicationPackage, TEST_ZONE); + controllerTester.deploy(app, applicationPackage, TEST_ZONE); job.type(JobType.systemTest).submit(); // Notifying about unknown job fails @@ -936,7 +937,7 @@ public class ApplicationApiTest extends ControllerContainerTest { .data(asJson(job.type(JobType.productionUsEast3).report())) .userIdentity(HOSTED_VESPA_OPERATOR) .get(); - tester.containerTester().assertResponse(request, new File("jobreport-unexpected-completion.json"), 400); + tester.assertResponse(request, new File("jobreport-unexpected-completion.json"), 400); // ... and assert it was recorded JobStatus recordedStatus = @@ -960,25 +961,24 @@ public class ApplicationApiTest extends ControllerContainerTest { @Test public void testJobStatusReportingOutOfCapacity() { - ContainerControllerTester tester = new ContainerControllerTester(container, responseFiles); - tester.containerTester().computeVersionStatus(); + controllerTester.containerTester().computeVersionStatus(); long projectId = 1; - Application app = tester.createApplication(); + Application app = controllerTester.createApplication(); ApplicationPackage applicationPackage = new ApplicationPackageBuilder() .environment(Environment.prod) .region("corp-us-east-1") .build(); // Report job failing with out of capacity - BuildJob job = new BuildJob(report -> notifyCompletion(report, tester), tester.artifactRepository()) + BuildJob job = new BuildJob(report -> notifyCompletion(report, controllerTester), controllerTester.artifactRepository()) .application(app) .projectId(projectId); job.type(JobType.component).uploadArtifact(applicationPackage).submit(); - tester.deploy(app, applicationPackage, TEST_ZONE); + controllerTester.deploy(app, applicationPackage, TEST_ZONE); job.type(JobType.systemTest).submit(); - tester.deploy(app, applicationPackage, STAGING_ZONE); + controllerTester.deploy(app, applicationPackage, STAGING_ZONE); job.type(JobType.stagingTest).error(DeploymentJobs.JobError.outOfCapacity).submit(); // Appropriate error is recorded @@ -1134,7 +1134,7 @@ public class ApplicationApiTest extends ControllerContainerTest { private void startAndTestChange(ContainerControllerTester controllerTester, ApplicationId application, long projectId, ApplicationPackage applicationPackage, - HttpEntity deployData, long buildNumber) throws IOException { + HttpEntity deployData, long buildNumber) { ContainerTester tester = controllerTester.containerTester(); // Trigger application change @@ -1208,11 +1208,22 @@ public class ApplicationApiTest extends ControllerContainerTest { } } - private void addPropertyData(MockOrganization organization, String propertyIdValue) { - PropertyId propertyId = new PropertyId(propertyIdValue); - organization.addProperty(propertyId); - organization.setContactsFor(propertyId, Arrays.asList(Collections.singletonList(User.from("alice")), - Collections.singletonList(User.from("bob")))); + private MockOrganization organization() { + return (MockOrganization) tester.container().components().getComponent(MockOrganization.class.getName()); + } + + private void updateContactInformation() { + new ContactInformationMaintainer(tester.controller(), Duration.ofDays(1), new JobControl(tester.controller().curator())).run(); + } + + private void registerContact(long propertyId) { + PropertyId p = new PropertyId(String.valueOf(propertyId)); + organization().addProperty(p) + .setIssueUrl(p, URI.create("www.issues.tld/" + p.id())) + .setContactsUrl(p, URI.create("www.contacts.tld/" + p.id())) + .setPropertyUrl(p, URI.create("www.properties.tld/" + p.id())) + .setContactsFor(p, Arrays.asList(Collections.singletonList(User.from("alice")), + Collections.singletonList(User.from("bob")))); } } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant-with-contact-info.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant-with-contact-info.json new file mode 100644 index 00000000000..0ba0a01c5d0 --- /dev/null +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant-with-contact-info.json @@ -0,0 +1,19 @@ +{ + "tenant": "tenant2", + "type": "ATHENS", + "athensDomain": "domain2", + "property": "property2", + "propertyId": "1234", + "applications": [], + "propertyUrl": "www.properties.tld/1234", + "contactsUrl": "www.contacts.tld/1234", + "issueCreationUrl": "www.issues.tld/1234", + "contacts": [ + [ + "alice" + ], + [ + "bob" + ] + ] +} diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/responses/maintenance.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/responses/maintenance.json index 2b847010482..6a71e524ae4 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/responses/maintenance.json +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/responses/maintenance.json @@ -10,6 +10,9 @@ "name": "ClusterUtilizationMaintainer" }, { + "name": "ContactInformationMaintainer" + }, + { "name": "DefaultOsUpgrader" }, { |