Skip to content

Commit

Permalink
Periodically update and store tenant contact information
Browse files Browse the repository at this point in the history
  • Loading branch information
mpolden committed Sep 4, 2018
1 parent 8a92412 commit a84a27f
Show file tree
Hide file tree
Showing 16 changed files with 467 additions and 117 deletions.
Expand Up @@ -2,6 +2,7 @@
package com.yahoo.vespa.hosted.controller.api.integration.organization;

import com.google.inject.Inject;
import com.yahoo.component.AbstractComponent;
import com.yahoo.vespa.hosted.controller.api.identifiers.PropertyId;

import java.net.URI;
Expand All @@ -18,7 +19,7 @@
/**
* @author jvenstad
*/
public class MockOrganization implements Organization {
public class MockOrganization extends AbstractComponent implements Organization {

private final Clock clock;
private final AtomicLong counter = new AtomicLong();
Expand Down Expand Up @@ -89,45 +90,58 @@ public List<? extends List<? extends User>> contactsFor(PropertyId propertyId) {

@Override
public URI issueCreationUri(PropertyId propertyId) {
return URI.create("www.issues.tld/" + propertyId.id());
return properties.getOrDefault(propertyId, new PropertyInfo()).issueUrl;
}

@Override
public URI contactsUri(PropertyId propertyId) {
return URI.create("www.contacts.tld/" + propertyId.id());
return properties.getOrDefault(propertyId, new PropertyInfo()).contactsUrl;
}

@Override
public URI propertyUri(PropertyId propertyId) {
return URI.create("www.properties.tld/" + propertyId.id());
return properties.getOrDefault(propertyId, new PropertyInfo()).propertyUrl;
}

public Map<IssueId, MockIssue> issues() {
return Collections.unmodifiableMap(issues);
}

public void close(IssueId issueId) {
public MockOrganization close(IssueId issueId) {
issues.get(issueId).open = false;
touch(issueId);
return this;
}

public void setDefaultAssigneeFor(PropertyId propertyId, User defaultAssignee) {
properties.get(propertyId).defaultAssignee = defaultAssignee;
public MockOrganization setContactsFor(PropertyId propertyId, List<List<User>> contacts) {
properties.get(propertyId).contacts = contacts;
return this;
}

public void setContactsFor(PropertyId propertyId, List<List<User>> contacts) {
properties.get(propertyId).contacts = contacts;
public MockOrganization setPropertyUrl(PropertyId propertyId, URI url) {
properties.get(propertyId).propertyUrl = url;
return this;
}

public MockOrganization setContactsUrl(PropertyId propertyId, URI url) {
properties.get(propertyId).contactsUrl = url;
return this;
}

public void addProperty(PropertyId propertyId) {
public MockOrganization setIssueUrl(PropertyId propertyId, URI url) {
properties.get(propertyId).issueUrl = url;
return this;
}

public MockOrganization addProperty(PropertyId propertyId) {
properties.put(propertyId, new PropertyInfo());
return this;
}

private void touch(IssueId issueId) {
issues.get(issueId).updated = clock.instant();
}


public class MockIssue {

private Issue issue;
Expand All @@ -148,11 +162,13 @@ private MockIssue(Issue issue) {

}


private class PropertyInfo {

private User defaultAssignee;
private List<List<User>> contacts = Collections.emptyList();
private URI issueUrl;
private URI contactsUrl;
private URI propertyUrl;

}

Expand Down
Expand Up @@ -12,8 +12,8 @@
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;
Expand Down Expand Up @@ -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;

/**
Expand Down Expand Up @@ -117,7 +116,6 @@ public Controller(CuratorDb curator, RotationsConfig rotationsConfig,
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");
Expand All @@ -136,7 +134,7 @@ public Controller(CuratorDb curator, RotationsConfig rotationsConfig,
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);
Expand Down Expand Up @@ -289,10 +287,6 @@ public Chef chefClient() {
return chef;
}

public Organization organization() {
return organization;
}

public CuratorDb curator() {
return curator;
}
Expand Down
Expand Up @@ -7,9 +7,11 @@
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.
Expand All @@ -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
Expand Down
Expand Up @@ -10,14 +10,18 @@
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;
Expand All @@ -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())) {
Expand Down Expand Up @@ -79,6 +82,24 @@ public List<Tenant> asList(UserId user) {
}
}

/** 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.
Expand Down
@@ -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());
}
}
}

}
Expand Up @@ -5,13 +5,11 @@
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;

Expand Down Expand Up @@ -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,
Expand All @@ -71,6 +70,7 @@ public ControllerMaintenance(MaintainerConfig maintainerConfig, Controller contr
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; }
Expand All @@ -96,6 +96,7 @@ public void deconstruct() {
osUpgraders.forEach(Maintainer::deconstruct);
osVersionStatusUpdater.deconstruct();
jobRunner.deconstruct();
contactInformationMaintainer.deconstruct();
}

/** Create one OS upgrader per cloud found in the zone registry of controller */
Expand Down

0 comments on commit a84a27f

Please sign in to comment.