summaryrefslogtreecommitdiffstats
path: root/controller-server
diff options
context:
space:
mode:
authorMartin Polden <mpolden@mpolden.no>2018-09-04 10:58:44 +0200
committerMartin Polden <mpolden@mpolden.no>2018-09-04 13:51:10 +0200
commit4f70d12360597ac3f1c52464d40589c868f398aa (patch)
tree43d9ee8aae7b702f2d01c227d033c2163a19b889 /controller-server
parent0d3b0e4d53861b3ca12498303aa091e564625bed (diff)
Periodically update and store tenant contact information
Diffstat (limited to 'controller-server')
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Controller.java10
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedTenant.java28
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/TenantController.java37
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ContactInformationMaintainer.java44
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ControllerMaintenance.java5
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializer.java48
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java40
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/AthenzTenant.java21
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/Contact.java75
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTester.java25
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ContactInformationMaintainerTest.java75
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializerTest.java23
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java91
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant-with-contact-info.json19
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/responses/maintenance.json3
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"
},
{