diff options
106 files changed, 2478 insertions, 1088 deletions
diff --git a/athenz-identity-provider-service/pom.xml b/athenz-identity-provider-service/pom.xml index 86d4defa861..982cb89f2bf 100644 --- a/athenz-identity-provider-service/pom.xml +++ b/athenz-identity-provider-service/pom.xml @@ -131,6 +131,14 @@ <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> + <configuration> + <compilerArgs> + <arg>-Xlint:all</arg> + <arg>-Xlint:-deprecation</arg> + <arg>-Xlint:-serial</arg> + <arg>-Werror</arg> + </compilerArgs> + </configuration> </plugin> </plugins> </build> diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/AthenzSslKeyStoreConfigurator.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/AthenzSslKeyStoreConfigurator.java index f1fc938d3ea..2a517e06ae2 100644 --- a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/AthenzSslKeyStoreConfigurator.java +++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/AthenzSslKeyStoreConfigurator.java @@ -23,11 +23,11 @@ import java.security.GeneralSecurityException; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.PrivateKey; -import java.security.SecureRandom; import java.security.cert.X509Certificate; import java.time.Duration; import java.time.Instant; import java.util.Optional; +import java.util.UUID; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; @@ -45,7 +45,6 @@ import static com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.Utils.g @SuppressWarnings("unused") // Component injected into Jetty connector factory public class AthenzSslKeyStoreConfigurator extends AbstractComponent implements SslKeyStoreConfigurator { private static final Logger log = Logger.getLogger(AthenzSslKeyStoreConfigurator.class.getName()); - private static final SecureRandom secureRandom = new SecureRandom(); private static final String CERTIFICATE_ALIAS = "athenz"; private static final Duration EXPIRATION_MARGIN = Duration.ofHours(6); @@ -172,12 +171,7 @@ public class AthenzSslKeyStoreConfigurator extends AbstractComponent implements } private static char[] generateKeystorePassword() { - int length = 128; - char[] pwd = new char[length]; - for (int i = 0; i < length; i++) { - pwd[i] = (char) secureRandom.nextInt(); - } - return pwd; + return UUID.randomUUID().toString().toCharArray(); } private class AthenzCertificateUpdater implements Runnable { diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/identitydocument/IdentityDocumentGenerator.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/identitydocument/IdentityDocumentGenerator.java index 728406c297f..59126fd023f 100644 --- a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/identitydocument/IdentityDocumentGenerator.java +++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/identitydocument/IdentityDocumentGenerator.java @@ -7,6 +7,7 @@ import com.yahoo.net.HostName; import com.yahoo.vespa.athenz.api.AthenzService; import com.yahoo.vespa.athenz.identityprovider.api.EntityBindingsMapper; import com.yahoo.vespa.athenz.identityprovider.api.IdentityDocument; +import com.yahoo.vespa.athenz.identityprovider.api.IdentityType; import com.yahoo.vespa.athenz.identityprovider.api.SignedIdentityDocument; import com.yahoo.vespa.athenz.identityprovider.api.VespaUniqueInstanceId; import com.yahoo.vespa.hosted.athenz.instanceproviderservice.KeyProvider; @@ -27,7 +28,10 @@ import java.util.Objects; import java.util.Set; /** + * Generates a signed identity document for a given hostname and type + * * @author mortent + * @author bjorncs */ public class IdentityDocumentGenerator { @@ -47,10 +51,10 @@ public class IdentityDocumentGenerator { this.keyProvider = keyProvider; } - public SignedIdentityDocument generateSignedIdentityDocument(String hostname) { + public SignedIdentityDocument generateSignedIdentityDocument(String hostname, IdentityType identityType) { Node node = nodeRepository.getNode(hostname).orElseThrow(() -> new RuntimeException("Unable to find node " + hostname)); try { - IdentityDocument identityDocument = generateIdDocument(node); + IdentityDocument identityDocument = generateIdDocument(node, identityType); String identityDocumentString = Utils.getMapper().writeValueAsString(EntityBindingsMapper.toIdentityDocumentEntity(identityDocument)); String encodedIdentityDocument = @@ -70,13 +74,18 @@ public class IdentityDocumentGenerator { toZoneDnsSuffix(zone, zoneConfig.certDnsSuffix()), new AthenzService(zoneConfig.domain(), zoneConfig.serviceName()), URI.create(zoneConfig.ztsUrl()), - SignedIdentityDocument.DEFAULT_DOCUMENT_VERSION); + SignedIdentityDocument.DEFAULT_DOCUMENT_VERSION, + identityDocument.configServerHostname(), + identityDocument.instanceHostname(), + identityDocument.createdAt(), + identityDocument.ipAddresses(), + identityType); } catch (Exception e) { throw new RuntimeException("Exception generating identity document: " + e.getMessage(), e); } } - private IdentityDocument generateIdDocument(Node node) { + private IdentityDocument generateIdDocument(Node node, IdentityType identityType) { Allocation allocation = node.allocation().orElseThrow(() -> new RuntimeException("No allocation for node " + node.hostname())); VespaUniqueInstanceId providerUniqueId = new VespaUniqueInstanceId( allocation.membership().index(), @@ -85,17 +94,10 @@ public class IdentityDocumentGenerator { allocation.owner().application().value(), allocation.owner().tenant().value(), zone.region().value(), - zone.environment().value()); + zone.environment().value(), + identityType); - // TODO: Hack to allow access from docker containers to non-ipv6 services. - // Remove when yca-bridge is no longer needed Set<String> ips = new HashSet<>(node.ipAddresses()); - if(node.parentHostname().isPresent()) { - String parentHostName = node.parentHostname().get(); - nodeRepository.getNode(parentHostName) - .map(Node::ipAddresses) - .ifPresent(ips::addAll); - } return new IdentityDocument( providerUniqueId, HostName.getLocalhost(), diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/identitydocument/IdentityDocumentResource.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/identitydocument/IdentityDocumentResource.java index 93668006e26..219e12c7223 100644 --- a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/identitydocument/IdentityDocumentResource.java +++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/identitydocument/IdentityDocumentResource.java @@ -6,6 +6,7 @@ import com.yahoo.container.jaxrs.annotation.Component; import com.yahoo.jdisc.http.servlet.ServletRequest; import com.yahoo.log.LogLevel; import com.yahoo.vespa.athenz.identityprovider.api.EntityBindingsMapper; +import com.yahoo.vespa.athenz.identityprovider.api.IdentityType; import com.yahoo.vespa.athenz.identityprovider.api.bindings.IdentityDocumentApi; import com.yahoo.vespa.athenz.identityprovider.api.bindings.SignedIdentityDocumentEntity; import com.yahoo.vespa.hosted.provision.restapi.v2.filter.NodePrincipal; @@ -18,7 +19,6 @@ import javax.ws.rs.InternalServerErrorException; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; -import javax.ws.rs.QueryParam; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import java.util.logging.Logger; @@ -41,15 +41,7 @@ public class IdentityDocumentResource implements IdentityDocumentApi { this.request = request; } - /** - * @deprecated Use {@link #getNodeIdentityDocument(String)} and {@link #getTenantIdentityDocument(String)} instead. - */ - @GET - @Produces(MediaType.APPLICATION_JSON) - @Deprecated - @Override - // TODO Make this method private when the rest api is not longer in use - public SignedIdentityDocumentEntity getIdentityDocument(@QueryParam("hostname") String hostname) { + private SignedIdentityDocumentEntity getIdentityDocument(String hostname, IdentityType identityType) { if (hostname == null) { throw new BadRequestException("The 'hostname' query parameter is missing"); } @@ -67,7 +59,7 @@ public class IdentityDocumentResource implements IdentityDocumentApi { throw new ForbiddenException(); } try { - return EntityBindingsMapper.toSignedIdentityDocumentEntity(identityDocumentGenerator.generateSignedIdentityDocument(hostname)); + return EntityBindingsMapper.toSignedIdentityDocumentEntity(identityDocumentGenerator.generateSignedIdentityDocument(hostname, identityType)); } catch (Exception e) { String message = String.format("Unable to generate identity doument for '%s': %s", hostname, e.getMessage()); log.log(LogLevel.ERROR, message, e); @@ -80,7 +72,7 @@ public class IdentityDocumentResource implements IdentityDocumentApi { @Path("/node/{host}") @Override public SignedIdentityDocumentEntity getNodeIdentityDocument(@PathParam("host") String host) { - return getIdentityDocument(host); + return getIdentityDocument(host, IdentityType.NODE); } @GET @@ -88,7 +80,7 @@ public class IdentityDocumentResource implements IdentityDocumentApi { @Path("/tenant/{host}") @Override public SignedIdentityDocumentEntity getTenantIdentityDocument(@PathParam("host") String host) { - return getIdentityDocument(host); + return getIdentityDocument(host, IdentityType.TENANT); } } diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/instanceconfirmation/InstanceValidator.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/instanceconfirmation/InstanceValidator.java index e457df37946..0201c46b253 100644 --- a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/instanceconfirmation/InstanceValidator.java +++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/instanceconfirmation/InstanceValidator.java @@ -82,6 +82,7 @@ public class InstanceValidator { } // If/when we dont care about logging exactly whats wrong, this can be simplified + // TODO Use identity type to determine if this check should be performed boolean isSameIdentityAsInServicesXml(ApplicationId applicationId, String domain, String service) { Optional<ApplicationInfo> applicationInfo = superModelProvider.getSuperModel().getApplicationInfo(applicationId); diff --git a/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/identitydocument/IdentityDocumentGeneratorTest.java b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/identitydocument/IdentityDocumentGeneratorTest.java index d7b061ca2f1..078ef1b7e39 100644 --- a/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/identitydocument/IdentityDocumentGeneratorTest.java +++ b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/identitydocument/IdentityDocumentGeneratorTest.java @@ -15,6 +15,7 @@ import com.yahoo.config.provision.SystemName; import com.yahoo.config.provision.TenantName; import com.yahoo.config.provision.Zone; import com.yahoo.vespa.athenz.identityprovider.api.EntityBindingsMapper; +import com.yahoo.vespa.athenz.identityprovider.api.IdentityType; import com.yahoo.vespa.athenz.identityprovider.api.SignedIdentityDocument; import com.yahoo.vespa.athenz.identityprovider.api.VespaUniqueInstanceId; import com.yahoo.vespa.athenz.identityprovider.api.bindings.SignedIdentityDocumentEntity; @@ -81,7 +82,7 @@ public class IdentityDocumentGeneratorTest { AthenzProviderServiceConfig config = getAthenzProviderConfig("domain", "service", dnsSuffix, ZONE); IdentityDocumentGenerator identityDocumentGenerator = new IdentityDocumentGenerator(config, nodeRepository, ZONE, keyProvider); - SignedIdentityDocument signedIdentityDocument = identityDocumentGenerator.generateSignedIdentityDocument(containerHostname); + SignedIdentityDocument signedIdentityDocument = identityDocumentGenerator.generateSignedIdentityDocument(containerHostname, IdentityType.TENANT); // Verify attributes assertEquals(containerHostname, signedIdentityDocument.identityDocument().instanceHostname()); @@ -92,11 +93,11 @@ public class IdentityDocumentGeneratorTest { assertEquals(expectedZoneDnsSuffix, signedIdentityDocument.dnsSuffix()); VespaUniqueInstanceId expectedProviderUniqueId = - new VespaUniqueInstanceId(0, "default", "default", "application", "tenant", region, environment); + new VespaUniqueInstanceId(0, "default", "default", "application", "tenant", region, environment, IdentityType.TENANT); assertEquals(expectedProviderUniqueId, signedIdentityDocument.providerUniqueId()); - // Validate that both parent and container ips are present - assertThat(signedIdentityDocument.identityDocument().ipAddresses(), Matchers.containsInAnyOrder("127.0.0.1", "::1")); + // Validate that container ips are present + assertThat(signedIdentityDocument.identityDocument().ipAddresses(), Matchers.containsInAnyOrder("::1")); SignedIdentityDocumentEntity signedIdentityDocumentEntity = EntityBindingsMapper.toSignedIdentityDocumentEntity(signedIdentityDocument); diff --git a/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/instanceconfirmation/InstanceValidatorTest.java b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/instanceconfirmation/InstanceValidatorTest.java index 54786c86cd3..54411b424eb 100644 --- a/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/instanceconfirmation/InstanceValidatorTest.java +++ b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/instanceconfirmation/InstanceValidatorTest.java @@ -143,7 +143,12 @@ public class InstanceValidatorTest { "dnssuffix", "service", URI.create("http://localhost/zts"), - 1)); + 1, + identityDocument.configServerHostname, + identityDocument.instanceHostname, + identityDocument.createdAt, + identityDocument.ipAddresses, + null)); // TODO Remove support for legacy representation without type } catch (Exception e) { throw new RuntimeException(e); } diff --git a/config-model-api/src/main/java/com/yahoo/config/application/api/ValidationOverrides.java b/config-model-api/src/main/java/com/yahoo/config/application/api/ValidationOverrides.java index 11f9add6b25..441ef273a6f 100644 --- a/config-model-api/src/main/java/com/yahoo/config/application/api/ValidationOverrides.java +++ b/config-model-api/src/main/java/com/yahoo/config/application/api/ValidationOverrides.java @@ -66,6 +66,11 @@ public class ValidationOverrides { return false; } + public static String toAllowMessage(ValidationId id) { + return "To allow this add <allow until='yyyy-mm-dd'>" + id + "</allow> to validation-overrides.xml" + + ", see https://docs.vespa.ai/documentation/reference/validation-overrides.html"; + } + /** Returns the XML form of this, or null if it was not created by fromXml, nor is empty */ public String xmlForm() { return xmlForm; } @@ -155,7 +160,9 @@ public class ValidationOverrides { /** Returns "validationId: message" */ @Override - public String getMessage() { return validationId + ": " + super.getMessage(); } + public String getMessage() { + return validationId + ": " + super.getMessage() + ". " + toAllowMessage(validationId); + } } diff --git a/config-model/src/main/java/com/yahoo/vespa/model/admin/monitoring/VespaMetricSet.java b/config-model/src/main/java/com/yahoo/vespa/model/admin/monitoring/VespaMetricSet.java index 6467199d9f9..fc46ed18dde 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/admin/monitoring/VespaMetricSet.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/admin/monitoring/VespaMetricSet.java @@ -110,6 +110,13 @@ public class VespaMetricSet { metrics.add(new Metric("jdisc.memory_mappings.max")); metrics.add(new Metric("jdisc.open_file_descriptors.max")); + metrics.add(new Metric("jdisc.gc.count.average")); + metrics.add(new Metric("jdisc.gc.count.max")); + metrics.add(new Metric("jdisc.gc.count.last")); + metrics.add(new Metric("jdisc.gc.ms.average")); + metrics.add(new Metric("jdisc.gc.ms.max")); + metrics.add(new Metric("jdisc.gc.ms.last")); + metrics.add(new Metric("jdisc.deactivated_containers.total.last")); metrics.add(new Metric("jdisc.deactivated_containers.with_retained_refs.last")); diff --git a/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/ClusterSizeReductionValidatorTest.java b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/ClusterSizeReductionValidatorTest.java index 765acf9e27b..4c3583ba0ae 100644 --- a/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/ClusterSizeReductionValidatorTest.java +++ b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/ClusterSizeReductionValidatorTest.java @@ -1,6 +1,8 @@ // Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.model.application.validation.change; +import com.yahoo.config.application.api.ValidationId; +import com.yahoo.config.application.api.ValidationOverrides; import com.yahoo.config.model.api.ConfigChangeAction; import com.yahoo.config.model.api.ConfigChangeRefeedAction; import com.yahoo.vespa.model.VespaModel; @@ -33,7 +35,8 @@ public class ClusterSizeReductionValidatorTest { fail("Expected exception due to cluster size reduction"); } catch (IllegalArgumentException expected) { - assertEquals("cluster-size-reduction: Size reduction in 'default' is too large. Current size: 30, new size: 14. New size must be at least 50% of the current size", + assertEquals("cluster-size-reduction: Size reduction in 'default' is too large. Current size: 30, new size: 14. New size must be at least 50% of the current size. " + + ValidationOverrides.toAllowMessage(ValidationId.clusterSizeReduction), Exceptions.toMessageString(expected)); } } diff --git a/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/ContentClusterRemovalValidatorTest.java b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/ContentClusterRemovalValidatorTest.java index 25ad6dbc620..ee58ca67b02 100644 --- a/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/ContentClusterRemovalValidatorTest.java +++ b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/ContentClusterRemovalValidatorTest.java @@ -1,6 +1,8 @@ // Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.model.application.validation.change; +import com.yahoo.config.application.api.ValidationId; +import com.yahoo.config.application.api.ValidationOverrides; import com.yahoo.vespa.model.VespaModel; import com.yahoo.vespa.model.application.validation.ValidationTester; import com.yahoo.yolean.Exceptions; @@ -24,7 +26,8 @@ public class ContentClusterRemovalValidatorTest { fail("Expected exception due to content cluster id change"); } catch (IllegalArgumentException expected) { - assertEquals("content-cluster-removal: Content cluster 'contentClusterId' is removed. This will cause loss of all data in this cluster", + assertEquals("content-cluster-removal: Content cluster 'contentClusterId' is removed. This will cause loss of all data in this cluster. " + + ValidationOverrides.toAllowMessage(ValidationId.contentClusterRemoval), Exceptions.toMessageString(expected)); } } diff --git a/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/ContentTypeRemovalValidatorTest.java b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/ContentTypeRemovalValidatorTest.java index a52c6d7c7a2..ca45520711e 100644 --- a/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/ContentTypeRemovalValidatorTest.java +++ b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/ContentTypeRemovalValidatorTest.java @@ -1,6 +1,8 @@ // Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.model.application.validation.change; +import com.yahoo.config.application.api.ValidationId; +import com.yahoo.config.application.api.ValidationOverrides; import com.yahoo.vespa.model.VespaModel; import com.yahoo.vespa.model.application.validation.ValidationTester; import com.yahoo.yolean.Exceptions; @@ -28,7 +30,8 @@ public class ContentTypeRemovalValidatorTest { } catch (IllegalArgumentException expected) { assertEquals("content-type-removal: Type 'music' is removed in content cluster 'test'. " + - "This will cause loss of all data of this type", + "This will cause loss of all data of this type. " + + ValidationOverrides.toAllowMessage(ValidationId.contentTypeRemoval), Exceptions.toMessageString(expected)); } } diff --git a/config/src/main/java/com/yahoo/config/subscription/impl/JRTConfigRequester.java b/config/src/main/java/com/yahoo/config/subscription/impl/JRTConfigRequester.java index 88af414e28d..243c9e932a8 100644 --- a/config/src/main/java/com/yahoo/config/subscription/impl/JRTConfigRequester.java +++ b/config/src/main/java/com/yahoo/config/subscription/impl/JRTConfigRequester.java @@ -30,10 +30,10 @@ import com.yahoo.vespa.config.protocol.Trace; * as context, and puts the requests objects on a queue on the subscription, * for handling by the user thread. * - * @author vegardh - * @since 5.1 + * @author Vegard Havdal */ public class JRTConfigRequester implements RequestWaiter { + private static final Logger log = Logger.getLogger(JRTConfigRequester.class.getName()); public static final ConfigSourceSet defaultSourceSet = ConfigSourceSet.createDefault(); private static final int TRACELEVEL = 6; diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/filedistribution/FileDistributionImpl.java b/configserver/src/main/java/com/yahoo/vespa/config/server/filedistribution/FileDistributionImpl.java index 2db89c2e8ed..36d76bbfc79 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/filedistribution/FileDistributionImpl.java +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/filedistribution/FileDistributionImpl.java @@ -10,7 +10,6 @@ import com.yahoo.jrt.Spec; import com.yahoo.jrt.StringArray; import com.yahoo.jrt.Supervisor; import com.yahoo.jrt.Target; -import com.yahoo.jrt.Transport; import com.yahoo.log.LogLevel; import com.yahoo.vespa.defaults.Defaults; @@ -24,11 +23,12 @@ import java.util.logging.Logger; public class FileDistributionImpl implements FileDistribution { private final static Logger log = Logger.getLogger(FileDistributionImpl.class.getName()); - private final Supervisor supervisor = new Supervisor(new Transport()); + private final Supervisor supervisor; private final File fileReferencesDir; - public FileDistributionImpl(ConfigserverConfig configserverConfig) { + public FileDistributionImpl(ConfigserverConfig configserverConfig, Supervisor supervisor) { this.fileReferencesDir = new File(Defaults.getDefaults().underVespaHome(configserverConfig.fileReferencesDir())); + this.supervisor = supervisor; } @Override diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/maintenance/ConfigServerMaintenance.java b/configserver/src/main/java/com/yahoo/vespa/config/server/maintenance/ConfigServerMaintenance.java index c6a390caf86..2a53f9ee45c 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/maintenance/ConfigServerMaintenance.java +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/maintenance/ConfigServerMaintenance.java @@ -51,7 +51,7 @@ public class ConfigServerMaintenance extends AbstractComponent { this.defaultInterval = Duration.ofMinutes(configserverConfig.maintainerIntervalMinutes()); // TODO: Want job control or feature flag to control when to run this, for now use a very // long interval to avoid running the maintainer - this.tenantsMaintainerInterval = isCd || isTest + this.tenantsMaintainerInterval = isCd || isTest || configserverConfig.region().equals("us-central-1") ? defaultInterval : Duration.ofMinutes(configserverConfig.tenantsMaintainerIntervalMinutes()); } diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/maintenance/FileDistributionMaintainer.java b/configserver/src/main/java/com/yahoo/vespa/config/server/maintenance/FileDistributionMaintainer.java index 2664a0bde8c..1d16283d938 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/maintenance/FileDistributionMaintainer.java +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/maintenance/FileDistributionMaintainer.java @@ -31,9 +31,10 @@ public class FileDistributionMaintainer extends Maintainer { @Override protected void maintain() { - // TODO: For now only deletes files in CD system + // TODO: Delete files in all zones boolean deleteFiles = (SystemName.from(configserverConfig.system()) == SystemName.cd) - || Environment.from(configserverConfig.environment()).isTest(); + || Environment.from(configserverConfig.environment()).isTest() + || configserverConfig.region().equals("us-central-1"); applicationRepository.deleteUnusedFiledistributionReferences(fileReferencesDir, deleteFiles); } } diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/FileDistributionFactory.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/FileDistributionFactory.java index 8394494adca..15bc3c1fb46 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/session/FileDistributionFactory.java +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/FileDistributionFactory.java @@ -3,6 +3,8 @@ package com.yahoo.vespa.config.server.session; import com.google.inject.Inject; import com.yahoo.cloud.config.ConfigserverConfig; +import com.yahoo.jrt.Supervisor; +import com.yahoo.jrt.Transport; import com.yahoo.vespa.config.server.filedistribution.FileDistributionImpl; import com.yahoo.vespa.config.server.filedistribution.FileDistributionProvider; @@ -17,6 +19,7 @@ import java.io.File; public class FileDistributionFactory { private final ConfigserverConfig configserverConfig; + private final Supervisor supervisor = new Supervisor(new Transport()); @Inject public FileDistributionFactory(ConfigserverConfig configserverConfig) { @@ -24,7 +27,7 @@ public class FileDistributionFactory { } public FileDistributionProvider createProvider(File applicationPackage) { - return new FileDistributionProvider(applicationPackage, new FileDistributionImpl(configserverConfig)); + return new FileDistributionProvider(applicationPackage, new FileDistributionImpl(configserverConfig, supervisor)); } } diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/ConfigServerBootstrapTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/ConfigServerBootstrapTest.java index c0c3683bebb..992d46d3115 100644 --- a/configserver/src/test/java/com/yahoo/vespa/config/server/ConfigServerBootstrapTest.java +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/ConfigServerBootstrapTest.java @@ -47,9 +47,9 @@ public class ConfigServerBootstrapTest { VipStatus vipStatus = new VipStatus(); ConfigServerBootstrap bootstrap = new ConfigServerBootstrap(tester.applicationRepository(), rpcServer, versionState, createStateMonitor(), vipStatus); assertFalse(vipStatus.isInRotation()); - waitUntil(() -> bootstrap.status() == StateMonitor.Status.up, "failed waiting for status 'up'"); waitUntil(rpcServer::isRunning, "failed waiting for Rpc server running"); - assertTrue(vipStatus.isInRotation()); + waitUntil(() -> bootstrap.status() == StateMonitor.Status.up, "failed waiting for status 'up'"); + waitUntil(vipStatus::isInRotation, "failed waiting for server to be in rotation"); bootstrap.deconstruct(); assertEquals(StateMonitor.Status.down, bootstrap.status()); diff --git a/container-dependency-versions/pom.xml b/container-dependency-versions/pom.xml index b4af6800768..f546a4e36d2 100644 --- a/container-dependency-versions/pom.xml +++ b/container-dependency-versions/pom.xml @@ -459,7 +459,7 @@ <properties> <bouncycastle.version>1.58</bouncycastle.version> - <felix.version>5.0.1</felix.version> + <felix.version>5.4.0</felix.version> <findbugs.version>1.3.9</findbugs.version> <guava.version>18.0</guava.version> <guice.version>3.0</guice.version> diff --git a/container-disc/src/main/java/com/yahoo/container/jdisc/metric/GarbageCollectionMetrics.java b/container-disc/src/main/java/com/yahoo/container/jdisc/metric/GarbageCollectionMetrics.java new file mode 100644 index 00000000000..04fd8572ad4 --- /dev/null +++ b/container-disc/src/main/java/com/yahoo/container/jdisc/metric/GarbageCollectionMetrics.java @@ -0,0 +1,94 @@ +package com.yahoo.container.jdisc.metric; + +import com.yahoo.jdisc.Metric; + +import java.lang.management.GarbageCollectorMXBean; +import java.lang.management.ManagementFactory; +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.Map; + +/** + * @author ollivir + */ +public class GarbageCollectionMetrics { + private static final String GC_COUNT = "jdisc.gc.count"; + private static final String GC_TIME = "jdisc.gc.ms"; + private static final String DIMENSION_KEY = "gcName"; + + public static final Duration REPORTING_INTERVAL = Duration.ofSeconds(62); + + static class GcStats { + private final Instant when; + private final long count; + private final Duration totalRuntime; + + private GcStats(Instant when, long count, Duration totalRuntime) { + this.when = when; + this.count = count; + this.totalRuntime = totalRuntime; + } + } + + private Map<String, LinkedList<GcStats>> gcStatistics; + + private final Clock clock; + + public GarbageCollectionMetrics(Clock clock) { + this.clock = clock; + this.gcStatistics = new HashMap<>(); + collectGcStatistics(clock.instant()); + } + + private void collectGcStatistics(Instant now) { + for (GarbageCollectorMXBean gcBean : ManagementFactory.getGarbageCollectorMXBeans()) { + String gcName = gcBean.getName().replace(" ", ""); + GcStats stats = new GcStats(now, gcBean.getCollectionCount(), Duration.ofMillis(gcBean.getCollectionTime())); + + LinkedList<GcStats> window = gcStatistics.computeIfAbsent(gcName, anyName -> new LinkedList<>()); + window.addLast(stats); + } + } + + private void cleanStatistics(Instant now) { + Instant oldestToKeep = now.minus(REPORTING_INTERVAL); + + for(Iterator<Map.Entry<String, LinkedList<GcStats>>> it = gcStatistics.entrySet().iterator(); it.hasNext(); ) { + Map.Entry<String, LinkedList<GcStats>> entry = it.next(); + LinkedList<GcStats> history = entry.getValue(); + while(history.isEmpty() == false && oldestToKeep.isAfter(history.getFirst().when)) { + history.removeFirst(); + } + if(history.isEmpty()) { + it.remove(); + } + } + } + + public void emitMetrics(Metric metric) { + Instant now = clock.instant(); + + collectGcStatistics(now); + cleanStatistics(now); + + for (Map.Entry<String, LinkedList<GcStats>> item : gcStatistics.entrySet()) { + GcStats reference = item.getValue().getFirst(); + GcStats latest = item.getValue().getLast(); + Map<String, String> contextData = new HashMap<>(); + contextData.put(DIMENSION_KEY, item.getKey()); + Metric.Context gcContext = metric.createContext(contextData); + + metric.set(GC_COUNT, latest.count - reference.count, gcContext); + metric.set(GC_TIME, latest.totalRuntime.minus(reference.totalRuntime).toMillis(), gcContext); + } + } + + // partial exposure for testing + Map<String, LinkedList<GcStats>> getGcStatistics() { + return gcStatistics; + } +} diff --git a/container-disc/src/main/java/com/yahoo/container/jdisc/metric/MetricUpdater.java b/container-disc/src/main/java/com/yahoo/container/jdisc/metric/MetricUpdater.java index 22b049c9ab7..c2ef789e8fc 100644 --- a/container-disc/src/main/java/com/yahoo/container/jdisc/metric/MetricUpdater.java +++ b/container-disc/src/main/java/com/yahoo/container/jdisc/metric/MetricUpdater.java @@ -10,6 +10,7 @@ import java.nio.file.DirectoryStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.time.Clock; import java.time.Duration; import java.util.Timer; import java.util.TimerTask; @@ -89,10 +90,12 @@ public class MetricUpdater extends AbstractComponent { private final Runtime runtime = Runtime.getRuntime(); private final Metric metric; private final ContainerWatchdogMetrics containerWatchdogMetrics; + private final GarbageCollectionMetrics garbageCollectionMetrics; public UpdaterTask(Metric metric, ContainerWatchdogMetrics containerWatchdogMetrics) { this.metric = metric; this.containerWatchdogMetrics = containerWatchdogMetrics; + this.garbageCollectionMetrics = new GarbageCollectionMetrics(Clock.systemUTC()); } @SuppressWarnings("deprecation") @@ -109,9 +112,10 @@ public class MetricUpdater extends AbstractComponent { metric.set(TOTAL_MEMORY_BYTES, totalMemory, null); metric.set(MEMORY_MAPPINGS_COUNT, count_mappings(), null); metric.set(OPEN_FILE_DESCRIPTORS, count_open_files(), null); + containerWatchdogMetrics.emitMetrics(metric); + garbageCollectionMetrics.emitMetrics(metric); } - } private static class TimerScheduler implements Scheduler { diff --git a/container-disc/src/test/java/com/yahoo/container/jdisc/metric/GarbageCollectionMetricsTest.java b/container-disc/src/test/java/com/yahoo/container/jdisc/metric/GarbageCollectionMetricsTest.java new file mode 100644 index 00000000000..61d8763b852 --- /dev/null +++ b/container-disc/src/test/java/com/yahoo/container/jdisc/metric/GarbageCollectionMetricsTest.java @@ -0,0 +1,57 @@ +package com.yahoo.container.jdisc.metric; + +import com.yahoo.jdisc.Metric; +import com.yahoo.test.ManualClock; +import org.junit.Test; + +import java.lang.management.ManagementFactory; +import java.time.Duration; +import java.util.LinkedList; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * @author ollivir + */ +public class GarbageCollectionMetricsTest { + @Test + public void gc_metrics_are_collected_in_a_sliding_window() { + ManualClock clock = new ManualClock(); + Metric metric = mock(Metric.class); + int garbageCollectors = ManagementFactory.getGarbageCollectorMXBeans().size(); + + Duration interval = GarbageCollectionMetrics.REPORTING_INTERVAL; + GarbageCollectionMetrics garbageCollectionMetrics = new GarbageCollectionMetrics(clock); + assertThat(garbageCollectionMetrics.getGcStatistics().keySet().size(), is(garbageCollectors)); + + clock.advance(interval.minus(Duration.ofMillis(10))); + garbageCollectionMetrics.emitMetrics(metric); + assertWindowLengths(garbageCollectionMetrics, 2); + + clock.advance(Duration.ofMillis(10)); + garbageCollectionMetrics.emitMetrics(metric); + assertWindowLengths(garbageCollectionMetrics, 3); + + clock.advance(Duration.ofMillis(10)); + garbageCollectionMetrics.emitMetrics(metric); + assertWindowLengths(garbageCollectionMetrics, 3); + + clock.advance(interval); + garbageCollectionMetrics.emitMetrics(metric); + assertWindowLengths(garbageCollectionMetrics, 2); + + verify(metric, times(garbageCollectors * 4 * 2)).set(anyString(), any(), any()); + } + + private static void assertWindowLengths(GarbageCollectionMetrics gcm, int count) { + for(LinkedList<GarbageCollectionMetrics.GcStats> window: gcm.getGcStatistics().values()) { + assertThat(window.size(), is(count)); + } + } +} diff --git a/container-disc/src/test/java/com/yahoo/container/jdisc/metric/MetricUpdaterTest.java b/container-disc/src/test/java/com/yahoo/container/jdisc/metric/MetricUpdaterTest.java index f10af7593a4..e9e04eab3b4 100644 --- a/container-disc/src/test/java/com/yahoo/container/jdisc/metric/MetricUpdaterTest.java +++ b/container-disc/src/test/java/com/yahoo/container/jdisc/metric/MetricUpdaterTest.java @@ -5,6 +5,7 @@ import com.yahoo.jdisc.Metric; import com.yahoo.jdisc.statistics.ContainerWatchdogMetrics; import org.junit.Test; +import java.lang.management.ManagementFactory; import java.time.Duration; import static org.mockito.Matchers.any; @@ -20,11 +21,13 @@ public class MetricUpdaterTest { @Test public void metrics_are_updated_in_scheduler_cycle() throws InterruptedException { + int gcCount = ManagementFactory.getGarbageCollectorMXBeans().size(); + Metric metric = mock(Metric.class); ContainerWatchdogMetrics containerWatchdogMetrics = mock(ContainerWatchdogMetrics.class); new MetricUpdater(new MockScheduler(), metric, containerWatchdogMetrics); verify(containerWatchdogMetrics, times(1)).emitMetrics(any()); - verify(metric, times(8)).set(anyString(), any(), any()); + verify(metric, times(8 + 2 * gcCount)).set(anyString(), any(), any()); } private static class MockScheduler implements MetricUpdater.Scheduler { diff --git a/container-jersey2/pom.xml b/container-jersey2/pom.xml index 76ff21dc028..c5ed7d872bf 100644 --- a/container-jersey2/pom.xml +++ b/container-jersey2/pom.xml @@ -53,10 +53,6 @@ <groupId>org.ow2.asm</groupId> <artifactId>asm</artifactId> </dependency> - <dependency> - <groupId>org.scala-lang</groupId> - <artifactId>scala-library</artifactId> - </dependency> </dependencies> <build> <plugins> diff --git a/container-jersey2/src/main/java/com/yahoo/container/servlet/jersey/ComponentGraphProvider.java b/container-jersey2/src/main/java/com/yahoo/container/servlet/jersey/ComponentGraphProvider.java new file mode 100644 index 00000000000..7ff9646cb27 --- /dev/null +++ b/container-jersey2/src/main/java/com/yahoo/container/servlet/jersey/ComponentGraphProvider.java @@ -0,0 +1,73 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.servlet.jersey; + +import com.yahoo.container.di.config.ResolveDependencyException; +import com.yahoo.container.di.config.RestApiContext; +import com.yahoo.container.jaxrs.annotation.Component; +import org.glassfish.hk2.api.Injectee; +import org.glassfish.hk2.api.InjectionResolver; +import org.glassfish.hk2.api.ServiceHandle; + +import javax.inject.Singleton; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Resolves jdisc container components for jersey 2 components. + * + * @author Tony Vaagenes + * @author ollivir + */ +@Singleton // jersey2 requirement: InjectionResolvers must be in the Singleton scope +public class ComponentGraphProvider implements InjectionResolver<Component> { + private Collection<RestApiContext.Injectable> injectables; + + public ComponentGraphProvider(Collection<RestApiContext.Injectable> injectables) { + this.injectables = injectables; + } + + @Override + public Object resolve(Injectee injectee, ServiceHandle<?> root) { + Class<?> wantedClass; + Type type = injectee.getRequiredType(); + if (type instanceof Class) { + wantedClass = (Class<?>) type; + } else { + throw new UnsupportedOperationException("Only classes are supported, got " + type); + } + + List<RestApiContext.Injectable> componentsWithMatchingType = new ArrayList<>(); + for (RestApiContext.Injectable injectable : injectables) { + if (wantedClass.isInstance(injectable.instance)) { + componentsWithMatchingType.add(injectable); + } + } + + if (componentsWithMatchingType.size() == 1) { + return componentsWithMatchingType.get(0).instance; + } else { + String injectionDescription = "class '" + wantedClass + "' to inject into Jersey resource/provider '" + + injectee.getInjecteeClass() + "')"; + if (componentsWithMatchingType.size() > 1) { + String ids = componentsWithMatchingType.stream().map(c -> c.id.toString()).collect(Collectors.joining(",")); + throw new ResolveDependencyException("Multiple components found of " + injectionDescription + ": " + ids); + } else { + throw new ResolveDependencyException("Could not find a component of " + injectionDescription + "."); + } + } + } + + @Override + public boolean isMethodParameterIndicator() { + return true; + } + + @Override + public boolean isConstructorParameterIndicator() { + return true; + } +} diff --git a/container-jersey2/src/main/java/com/yahoo/container/servlet/jersey/JerseyApplication.java b/container-jersey2/src/main/java/com/yahoo/container/servlet/jersey/JerseyApplication.java new file mode 100644 index 00000000000..4c4e43bc8d5 --- /dev/null +++ b/container-jersey2/src/main/java/com/yahoo/container/servlet/jersey/JerseyApplication.java @@ -0,0 +1,25 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.servlet.jersey; + +import javax.ws.rs.core.Application; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +/** + * @author Tony Vaagenes + * @author ollivir + */ +public class JerseyApplication extends Application { + private Set<Class<?>> classes; + + public JerseyApplication(Collection<Class<?>> resourcesAndProviderClasses) { + this.classes = new HashSet<>(resourcesAndProviderClasses); + } + + @Override + public Set<Class<?>> getClasses() { + return classes; + } +} diff --git a/container-jersey2/src/main/java/com/yahoo/container/servlet/jersey/JerseyServletProvider.java b/container-jersey2/src/main/java/com/yahoo/container/servlet/jersey/JerseyServletProvider.java new file mode 100644 index 00000000000..1dbe410ba54 --- /dev/null +++ b/container-jersey2/src/main/java/com/yahoo/container/servlet/jersey/JerseyServletProvider.java @@ -0,0 +1,118 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.servlet.jersey; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider; +import com.yahoo.container.di.componentgraph.Provider; +import com.yahoo.container.di.config.RestApiContext; +import com.yahoo.container.di.config.RestApiContext.BundleInfo; +import com.yahoo.container.jaxrs.annotation.Component; +import org.eclipse.jetty.servlet.ServletHolder; +import org.glassfish.hk2.api.InjectionResolver; +import org.glassfish.hk2.api.TypeLiteral; +import org.glassfish.hk2.utilities.Binder; +import org.glassfish.hk2.utilities.binding.AbstractBinder; +import org.glassfish.jersey.media.multipart.MultiPartFeature; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.servlet.ServletContainer; +import org.objectweb.asm.ClassReader; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +import static com.yahoo.container.servlet.jersey.util.ResourceConfigUtil.registerComponent; + +/** + * @author Tony Vaagenes + * @author ollivir + */ +public class JerseyServletProvider implements Provider<ServletHolder> { + private final ServletHolder jerseyServletHolder; + + public JerseyServletProvider(RestApiContext restApiContext) { + this.jerseyServletHolder = new ServletHolder(new ServletContainer(resourceConfig(restApiContext))); + } + + private ResourceConfig resourceConfig(RestApiContext restApiContext) { + final ResourceConfig resourceConfig = ResourceConfig + .forApplication(new JerseyApplication(resourcesAndProviders(restApiContext.getBundles()))); + + registerComponent(resourceConfig, componentInjectorBinder(restApiContext)); + registerComponent(resourceConfig, jacksonDatatypeJdk8Provider()); + resourceConfig.register(MultiPartFeature.class); + + return resourceConfig; + } + + private static Collection<Class<?>> resourcesAndProviders(Collection<BundleInfo> bundles) { + final List<Class<?>> ret = new ArrayList<>(); + + for (BundleInfo bundle : bundles) { + for (String classEntry : bundle.getClassEntries()) { + Optional<String> className = detectResourceOrProvider(bundle.classLoader, classEntry); + className.ifPresent(cname -> ret.add(loadClass(bundle.symbolicName, bundle.classLoader, cname))); + } + } + return ret; + } + + private static Optional<String> detectResourceOrProvider(ClassLoader bundleClassLoader, String classEntry) { + try (InputStream inputStream = getResourceAsStream(bundleClassLoader, classEntry)) { + ResourceOrProviderClassVisitor visitor = ResourceOrProviderClassVisitor.visit(new ClassReader(inputStream)); + return visitor.getJerseyClassName(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private static InputStream getResourceAsStream(ClassLoader bundleClassLoader, String classEntry) { + InputStream is = bundleClassLoader.getResourceAsStream(classEntry); + if (is == null) { + throw new RuntimeException("No entry " + classEntry + " in bundle " + bundleClassLoader); + } else { + return is; + } + } + + private static Class<?> loadClass(String bundleSymbolicName, ClassLoader classLoader, String className) { + try { + return classLoader.loadClass(className); + } catch (Exception e) { + throw new RuntimeException("Failed loading class " + className + " from bundle " + bundleSymbolicName, e); + } + } + + private static Binder componentInjectorBinder(RestApiContext restApiContext) { + final ComponentGraphProvider componentGraphProvider = new ComponentGraphProvider(restApiContext.getInjectableComponents()); + final TypeLiteral<InjectionResolver<Component>> componentAnnotationType = new TypeLiteral<InjectionResolver<Component>>() { + }; + + return new AbstractBinder() { + @Override + public void configure() { + bind(componentGraphProvider).to(componentAnnotationType); + } + }; + } + + private static JacksonJaxbJsonProvider jacksonDatatypeJdk8Provider() { + JacksonJaxbJsonProvider provider = new JacksonJaxbJsonProvider(); + provider.setMapper(new ObjectMapper().registerModule(new Jdk8Module()).registerModule(new JavaTimeModule())); + return provider; + } + + @Override + public ServletHolder get() { + return jerseyServletHolder; + } + + @Override + public void deconstruct() { + } +} diff --git a/container-jersey2/src/main/java/com/yahoo/container/servlet/jersey/ResourceOrProviderClassVisitor.java b/container-jersey2/src/main/java/com/yahoo/container/servlet/jersey/ResourceOrProviderClassVisitor.java new file mode 100644 index 00000000000..7cb47ac6118 --- /dev/null +++ b/container-jersey2/src/main/java/com/yahoo/container/servlet/jersey/ResourceOrProviderClassVisitor.java @@ -0,0 +1,103 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.servlet.jersey; + +import org.objectweb.asm.AnnotationVisitor; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; + +import javax.ws.rs.Path; +import javax.ws.rs.ext.Provider; + +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; + +/** + * @author Tony Vaagenes + * @author ollivir + */ +public class ResourceOrProviderClassVisitor extends ClassVisitor { + private String className = null; + private boolean isPublic = false; + private boolean isAbstract = false; + + private boolean isInnerClass = false; + private boolean isStatic = false; + + private boolean isAnnotated = false; + + public ResourceOrProviderClassVisitor() { + super(Opcodes.ASM6); + } + + public Optional<String> getJerseyClassName() { + if (isJerseyClass()) { + return Optional.of(getClassName()); + } else { + return Optional.empty(); + } + } + + public boolean isJerseyClass() { + return isAnnotated && isPublic && !isAbstract && (!isInnerClass || isStatic); + } + + public String getClassName() { + assert (className != null); + return org.objectweb.asm.Type.getObjectType(className).getClassName(); + } + + @Override + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + isPublic = isPublic(access); + className = name; + isAbstract = isAbstract(access); + } + + @Override + public void visitInnerClass(String name, String outerName, String innerName, int access) { + assert (className != null); + + if (name.equals(className)) { + isInnerClass = true; + isStatic = isStatic(access); + } + } + + @Override + public AnnotationVisitor visitAnnotation(String desc, boolean visible) { + isAnnotated |= annotationClassDescriptors.contains(desc); + return null; + } + + private static Set<String> annotationClassDescriptors = new HashSet<>(); + + static { + annotationClassDescriptors.add(Type.getDescriptor(Path.class)); + annotationClassDescriptors.add(Type.getDescriptor(Provider.class)); + } + + private static boolean isPublic(int access) { + return isSet(Opcodes.ACC_PUBLIC, access); + } + + private static boolean isStatic(int access) { + return isSet(Opcodes.ACC_STATIC, access); + } + + private static boolean isAbstract(int access) { + return isSet(Opcodes.ACC_ABSTRACT, access); + } + + private static boolean isSet(int bits, int access) { + return (access & bits) == bits; + } + + public static ResourceOrProviderClassVisitor visit(ClassReader classReader) { + ResourceOrProviderClassVisitor visitor = new ResourceOrProviderClassVisitor(); + classReader.accept(visitor, ClassReader.SKIP_DEBUG | ClassReader.SKIP_CODE | ClassReader.SKIP_FRAMES); + return visitor; + } +} diff --git a/container-jersey2/src/main/scala/com/yahoo/container/servlet/jersey/ComponentGraphProvider.scala b/container-jersey2/src/main/scala/com/yahoo/container/servlet/jersey/ComponentGraphProvider.scala deleted file mode 100644 index cabde3680a4..00000000000 --- a/container-jersey2/src/main/scala/com/yahoo/container/servlet/jersey/ComponentGraphProvider.scala +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.container.servlet.jersey - -import javax.inject.Singleton - -import com.yahoo.container.di.config.{ResolveDependencyException, RestApiContext} -import com.yahoo.container.jaxrs.annotation.Component -import org.glassfish.hk2.api.{ServiceHandle, Injectee, InjectionResolver} - -/** - * Resolves jdisc container components for jersey 2 components. - * Similar to Gjoran's ComponentGraphProvider for jersey 1. - * @author tonytv - */ -@Singleton //jersey2 requirement: InjectionResolvers must be in the Singleton scope -class ComponentGraphProvider(injectables: Traversable[RestApiContext.Injectable]) extends InjectionResolver[Component] { - override def resolve(injectee: Injectee, root: ServiceHandle[_]): AnyRef = { - val wantedClass = injectee.getRequiredType match { - case c: Class[_] => c - case unsupported => throw new UnsupportedOperationException("Only classes are supported, got " + unsupported) - } - - val componentsWithMatchingType = injectables.filter{ injectable => - wantedClass.isInstance(injectable.instance) } - - val injectionDescription = - s"class '$wantedClass' to inject into Jersey resource/provider '${injectee.getInjecteeClass}')" - - if (componentsWithMatchingType.size > 1) - throw new ResolveDependencyException(s"Multiple components found of $injectionDescription: " + - componentsWithMatchingType.map(_.id).mkString(",")) - - componentsWithMatchingType.headOption.map(_.instance).getOrElse { - throw new ResolveDependencyException(s"Could not find a component of $injectionDescription.") - } - } - - override def isMethodParameterIndicator: Boolean = true - override def isConstructorParameterIndicator: Boolean = true -} diff --git a/container-jersey2/src/main/scala/com/yahoo/container/servlet/jersey/JerseyApplication.scala b/container-jersey2/src/main/scala/com/yahoo/container/servlet/jersey/JerseyApplication.scala deleted file mode 100644 index eea41003984..00000000000 --- a/container-jersey2/src/main/scala/com/yahoo/container/servlet/jersey/JerseyApplication.scala +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.container.servlet.jersey - -import javax.ws.rs.core.Application - -import scala.collection.JavaConverters._ - -/** - * @author tonytv - */ -class JerseyApplication(resourcesAndProviderClasses: Set[Class[_]]) extends Application { - private val classes: java.util.Set[Class[_]] = resourcesAndProviderClasses.asJava - - override def getClasses = classes - override def getSingletons = super.getSingletons -} diff --git a/container-jersey2/src/main/scala/com/yahoo/container/servlet/jersey/JerseyServletProvider.scala b/container-jersey2/src/main/scala/com/yahoo/container/servlet/jersey/JerseyServletProvider.scala deleted file mode 100644 index f0eff54dc16..00000000000 --- a/container-jersey2/src/main/scala/com/yahoo/container/servlet/jersey/JerseyServletProvider.scala +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.container.servlet.jersey - -import java.io.{IOException, InputStream} - -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.datatype.jdk8.Jdk8Module -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule -import com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider -import com.yahoo.container.di.componentgraph.Provider -import com.yahoo.container.di.config.RestApiContext -import com.yahoo.container.di.config.RestApiContext.BundleInfo -import com.yahoo.container.jaxrs.annotation.Component -import com.yahoo.container.servlet.jersey.util.ResourceConfigUtil.registerComponent -import org.eclipse.jetty.servlet.ServletHolder -import org.glassfish.hk2.api.{InjectionResolver, TypeLiteral} -import org.glassfish.hk2.utilities.Binder -import org.glassfish.hk2.utilities.binding.AbstractBinder -import org.glassfish.jersey.media.multipart.MultiPartFeature -import org.glassfish.jersey.server.ResourceConfig -import org.glassfish.jersey.servlet.ServletContainer -import org.objectweb.asm.ClassReader - -import scala.collection.JavaConverters._ -import scala.util.control.Exception - - -/** - * @author tonytv - */ -class JerseyServletProvider(restApiContext: RestApiContext) extends Provider[ServletHolder] { - private val jerseyServletHolder = new ServletHolder(new ServletContainer(resourceConfig(restApiContext))) - - private def resourceConfig(restApiContext: RestApiContext) = { - val resourceConfig = ResourceConfig.forApplication( - new JerseyApplication(resourcesAndProviders(restApiContext.getBundles.asScala))) - - registerComponent(resourceConfig, componentInjectorBinder(restApiContext)) - registerComponent(resourceConfig, jacksonDatatypeJdk8Provider) - resourceConfig.register(classOf[MultiPartFeature]) - - resourceConfig - } - - def resourcesAndProviders(bundles: Traversable[BundleInfo]) = - (for { - bundle <- bundles.view - classEntry <- bundle.getClassEntries.asScala - className <- detectResourceOrProvider(bundle.classLoader, classEntry) - } yield loadClass(bundle.symbolicName, bundle.classLoader, className)).toSet - - - def detectResourceOrProvider(bundleClassLoader: ClassLoader, classEntry: String): Option[String] = { - using(getResourceAsStream(bundleClassLoader, classEntry)) { inputStream => - val visitor = ResourceOrProviderClassVisitor.visit(new ClassReader(inputStream)) - visitor.getJerseyClassName - } - } - - private def getResourceAsStream(bundleClassLoader: ClassLoader, classEntry: String) = { - bundleClassLoader.getResourceAsStream(classEntry) match { - case null => throw new RuntimeException(s"No entry $classEntry in bundle $bundleClassLoader") - case stream => stream - } - - } - - def using[T <: InputStream, R](stream: T)(f: T => R): R = { - try { - f(stream) - } finally { - Exception.ignoring(classOf[IOException]) { - stream.close() - } - } - } - - def loadClass(bundleSymbolicName: String, classLoader: ClassLoader, className: String) = { - try { - classLoader.loadClass(className) - } catch { - case e: Exception => throw new RuntimeException(s"Failed loading class $className from bundle $bundleSymbolicName", e) - } - } - - def componentInjectorBinder(restApiContext: RestApiContext): Binder = { - val componentGraphProvider = new ComponentGraphProvider(restApiContext.getInjectableComponents.asScala) - val componentAnnotationType = new TypeLiteral[InjectionResolver[Component]] {} - - new AbstractBinder { - override def configure() { - bind(componentGraphProvider).to(componentAnnotationType) - } - } - } - - def jacksonDatatypeJdk8Provider: JacksonJaxbJsonProvider = { - val provider = new JacksonJaxbJsonProvider() - provider.setMapper( - new ObjectMapper() - .registerModule(new Jdk8Module) - .registerModule(new JavaTimeModule)) - provider - } - - override def get() = jerseyServletHolder - override def deconstruct() {} -} - diff --git a/container-jersey2/src/main/scala/com/yahoo/container/servlet/jersey/ResourceOrProviderClassVisitor.scala b/container-jersey2/src/main/scala/com/yahoo/container/servlet/jersey/ResourceOrProviderClassVisitor.scala deleted file mode 100644 index 52674026c25..00000000000 --- a/container-jersey2/src/main/scala/com/yahoo/container/servlet/jersey/ResourceOrProviderClassVisitor.scala +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.container.servlet.jersey - -import javax.ws.rs.Path -import javax.ws.rs.ext.Provider - -import org.objectweb.asm.{ClassVisitor, Opcodes, Type, AnnotationVisitor, ClassReader} - - -/** - * @author tonytv - */ -class ResourceOrProviderClassVisitor private () extends ClassVisitor(Opcodes.ASM6) { - private var className: String = null - private var isPublic: Boolean = false - private var isAbstract = false - - private var isInnerClass: Boolean = false - private var isStatic: Boolean = false - - private var isAnnotated: Boolean = false - - def getJerseyClassName: Option[String] = { - if (isJerseyClass) Some(getClassName) - else None - } - - def isJerseyClass: Boolean = { - isAnnotated && isPublic && !isAbstract && - (!isInnerClass || isStatic) - } - - def getClassName = { - assert (className != null) - Type.getObjectType(className).getClassName - } - - override def visit(version: Int, access: Int, name: String, signature: String, superName: String, interfaces: Array[String]) { - isPublic = ResourceOrProviderClassVisitor.isPublic(access) - className = name - isAbstract = ResourceOrProviderClassVisitor.isAbstract(access) - } - - override def visitInnerClass(name: String, outerName: String, innerName: String, access: Int) { - assert (className != null) - - if (name == className) { - isInnerClass = true - isStatic = ResourceOrProviderClassVisitor.isStatic(access) - } - } - - override def visitAnnotation(desc: String, visible: Boolean): AnnotationVisitor = { - isAnnotated |= ResourceOrProviderClassVisitor.annotationClassDescriptors(desc) - null - } -} - - -object ResourceOrProviderClassVisitor { - val annotationClassDescriptors = Set(classOf[Path], classOf[Provider]) map Type.getDescriptor - - def isPublic = isSet(Opcodes.ACC_PUBLIC) _ - def isStatic = isSet(Opcodes.ACC_STATIC) _ - def isAbstract = isSet(Opcodes.ACC_ABSTRACT) _ - - private def isSet(bits: Int)(access: Int): Boolean = (access & bits) == bits - - def visit(classReader: ClassReader): ResourceOrProviderClassVisitor = { - val visitor = new ResourceOrProviderClassVisitor - classReader.accept(visitor, ClassReader.SKIP_DEBUG | ClassReader.SKIP_CODE | ClassReader.SKIP_FRAMES) - visitor - } -} diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/Organization.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/Organization.java index 00c0d87554a..776002f31cb 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/Organization.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/Organization.java @@ -3,7 +3,6 @@ package com.yahoo.vespa.hosted.controller.api.integration.organization; import com.yahoo.vespa.hosted.controller.api.identifiers.PropertyId; -import java.io.UncheckedIOException; import java.net.URI; import java.time.Duration; import java.util.List; @@ -87,8 +86,9 @@ public interface Organization { * * @param issueId ID of the issue to escalate. * @param propertyId PropertyId of the tenant owning the application for which the issue was filed. + * @return User that was assigned issue as a result of the escalation, if any */ - default boolean escalate(IssueId issueId, PropertyId propertyId) { + default Optional<User> escalate(IssueId issueId, PropertyId propertyId) { List<? extends List<? extends User>> contacts = contactsFor(propertyId); Optional<User> assignee = assigneeOf(issueId); @@ -101,9 +101,9 @@ public interface Organization { for (int level = assigneeLevel + 1; level < contacts.size(); level++) for (User target : contacts.get(level)) if (reassign(issueId, target)) - return true; + return Optional.of(target); - return false; + return Optional.empty(); } /** diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java index 8b0dc35e16b..f0e278c3e6d 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java @@ -5,6 +5,7 @@ import com.google.common.collect.ImmutableList; import com.yahoo.component.Version; import com.yahoo.config.application.api.DeploymentSpec; import com.yahoo.config.application.api.ValidationId; +import com.yahoo.config.application.api.ValidationOverrides; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.TenantName; @@ -109,7 +110,7 @@ public class ApplicationController { this.artifactRepository = artifactRepository; this.rotationRepository = new RotationRepository(rotationsConfig, this, curator); - this.deploymentTrigger = new DeploymentTrigger(controller, curator, buildService, clock); + this.deploymentTrigger = new DeploymentTrigger(controller, buildService, clock); for (Application application : curator.readApplications()) { lockIfPresent(application.id(), this::store); @@ -256,7 +257,7 @@ public class ApplicationController { LockedApplication application = new LockedApplication(new Application(id), lock); store(application); log.info("Created " + application); - return application; + return application.get(); } } @@ -285,7 +286,7 @@ public class ApplicationController { } else { JobType jobType = JobType.from(controller.system(), zone) .orElseThrow(() -> new IllegalArgumentException("No job found for zone " + zone)); - Optional<JobStatus> job = Optional.ofNullable(application.deploymentJobs().jobStatus().get(jobType)); + Optional<JobStatus> job = Optional.ofNullable(application.get().deploymentJobs().jobStatus().get(jobType)); if ( ! job.isPresent() || ! job.get().lastTriggered().isPresent() || job.get().lastCompleted().isPresent() && job.get().lastCompleted().get().at().isAfter(job.get().lastTriggered().get().at())) @@ -297,8 +298,8 @@ public class ApplicationController { applicationVersion = preferOldestVersion ? triggered.sourceApplication().orElse(triggered.application()) : triggered.application(); - applicationPackage = new ApplicationPackage(artifactRepository.getApplicationPackage(application.id(), applicationVersion.id())); - validateRun(application, zone, platformVersion, applicationVersion); + applicationPackage = new ApplicationPackage(artifactRepository.getApplicationPackage(application.get().id(), applicationVersion.id())); + validateRun(application.get(), zone, platformVersion, applicationVersion); } validate(applicationPackage.deploymentSpec()); @@ -323,7 +324,7 @@ public class ApplicationController { application = withRotation(application, zone); Set<String> rotationNames = new HashSet<>(); Set<String> cnames = new HashSet<>(); - application.rotation().ifPresent(applicationRotation -> { + application.get().rotation().ifPresent(applicationRotation -> { rotationNames.add(applicationRotation.id().asString()); cnames.add(applicationRotation.dnsName()); cnames.add(applicationRotation.secureDnsName()); @@ -366,15 +367,15 @@ public class ApplicationController { /** Makes sure the application has a global rotation, if eligible. */ private LockedApplication withRotation(LockedApplication application, ZoneId zone) { - if (zone.environment() == Environment.prod && application.deploymentSpec().globalServiceId().isPresent()) { + if (zone.environment() == Environment.prod && application.get().deploymentSpec().globalServiceId().isPresent()) { try (RotationLock rotationLock = rotationRepository.lock()) { - Rotation rotation = rotationRepository.getOrAssignRotation(application, rotationLock); + Rotation rotation = rotationRepository.getOrAssignRotation(application.get(), rotationLock); application = application.with(rotation.id()); store(application); // store assigned rotation even if deployment fails - registerRotationInDns(rotation, application.rotation().get().dnsName()); - registerRotationInDns(rotation, application.rotation().get().secureDnsName()); - registerRotationInDns(rotation, application.rotation().get().oathDnsName()); + registerRotationInDns(rotation, application.get().rotation().get().dnsName()); + registerRotationInDns(rotation, application.get().rotation().get().secureDnsName()); + registerRotationInDns(rotation, application.get().rotation().get().oathDnsName()); } } return application; @@ -394,22 +395,23 @@ public class ApplicationController { } private LockedApplication deleteRemovedDeployments(LockedApplication application) { - List<Deployment> deploymentsToRemove = application.productionDeployments().values().stream() - .filter(deployment -> ! application.deploymentSpec().includes(deployment.zone().environment(), - Optional.of(deployment.zone().region()))) + List<Deployment> deploymentsToRemove = application.get().productionDeployments().values().stream() + .filter(deployment -> ! application.get().deploymentSpec().includes(deployment.zone().environment(), + Optional.of(deployment.zone().region()))) .collect(Collectors.toList()); if (deploymentsToRemove.isEmpty()) return application; - if ( ! application.validationOverrides().allows(ValidationId.deploymentRemoval, clock.instant())) - throw new IllegalArgumentException(ValidationId.deploymentRemoval.value() + ": " + application + + if ( ! application.get().validationOverrides().allows(ValidationId.deploymentRemoval, clock.instant())) + throw new IllegalArgumentException(ValidationId.deploymentRemoval.value() + ": " + application.get() + " is deployed in " + deploymentsToRemove.stream() .map(deployment -> deployment.zone().region().value()) .collect(Collectors.joining(", ")) + ", but does not include " + (deploymentsToRemove.size() > 1 ? "these zones" : "this zone") + - " in deployment.xml"); + " in deployment.xml. " + + ValidationOverrides.toAllowMessage(ValidationId.deploymentRemoval)); LockedApplication applicationWithRemoval = application; for (Deployment deployment : deploymentsToRemove) @@ -418,10 +420,11 @@ public class ApplicationController { } private LockedApplication deleteUnreferencedDeploymentJobs(LockedApplication application) { - for (JobType job : application.deploymentJobs().jobStatus().keySet()) { + for (JobType job : application.get().deploymentJobs().jobStatus().keySet()) { Optional<ZoneId> zone = job.zone(controller.system()); - if ( ! job.isProduction() || (zone.isPresent() && application.deploymentSpec().includes(zone.get().environment(), zone.map(ZoneId::region)))) + if ( ! job.isProduction() || (zone.isPresent() && application.get().deploymentSpec().includes( + zone.get().environment(), zone.map(ZoneId::region)))) continue; application = application.withoutDeploymentJob(job); } @@ -493,7 +496,7 @@ public class ApplicationController { // TODO: Make this one transaction when database is moved to ZooKeeper instances.forEach(id -> lockOrThrow(id, application -> { - if ( ! application.deployments().isEmpty()) + if ( ! application.get().deployments().isEmpty()) throw new IllegalArgumentException("Could not delete '" + application + "': It has active deployments"); Tenant tenant = controller.tenants().tenant(id.tenant()).get(); @@ -518,7 +521,7 @@ public class ApplicationController { * @param application a locked application to store */ public void store(LockedApplication application) { - curator.writeApplication(application); + curator.writeApplication(application.get()); } /** @@ -572,7 +575,7 @@ public class ApplicationController { */ private LockedApplication deactivate(LockedApplication application, ZoneId zone) { try { - configServer.deactivate(new DeploymentId(application.id(), zone)); + configServer.deactivate(new DeploymentId(application.get().id(), zone)); } catch (NoInstanceException ignored) { // ok; already gone diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedApplication.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedApplication.java index 795b12f8af9..3207d4b8399 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedApplication.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedApplication.java @@ -26,17 +26,29 @@ import com.yahoo.vespa.hosted.controller.rotation.RotationId; import java.time.Instant; import java.util.LinkedHashMap; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.OptionalLong; /** - * A combination of an application instance and a lock for that application. Provides methods for updating application - * fields. + * An application that has been locked for modification. Provides methods for modifying an application's fields. * * @author mpolden * @author jvenstad */ -public class LockedApplication extends Application { +public class LockedApplication { + + private final Lock lock; + private final ApplicationId id; + private final DeploymentSpec deploymentSpec; + private final ValidationOverrides validationOverrides; + private final Map<ZoneId, Deployment> deployments; + private final DeploymentJobs deploymentJobs; + private final Change change; + private final Change outstandingChange; + private final Optional<IssueId> ownershipIssueId; + private final ApplicationMetrics metrics; + private final Optional<RotationId> rotation; /** * Used to create a locked application @@ -44,38 +56,69 @@ public class LockedApplication extends Application { * @param application The application to lock. * @param lock The lock for the application. */ - LockedApplication(Application application, @SuppressWarnings("unused") Lock lock) { - this(new Builder(application)); - } - - private LockedApplication(Builder builder) { - super(builder.applicationId, builder.deploymentSpec, builder.validationOverrides, - builder.deployments, builder.deploymentJobs, builder.deploying, - builder.outstandingChange, builder.ownershipIssueId, builder.metrics, builder.rotation); + LockedApplication(Application application, Lock lock) { + this(Objects.requireNonNull(lock, "lock cannot be null"), application.id(), + application.deploymentSpec(), application.validationOverrides(), + application.deployments(), + application.deploymentJobs(), application.change(), application.outstandingChange(), + application.ownershipIssueId(), application.metrics(), + application.rotation().map(ApplicationRotation::id)); + } + + private LockedApplication(Lock lock, ApplicationId id, + DeploymentSpec deploymentSpec, ValidationOverrides validationOverrides, + Map<ZoneId, Deployment> deployments, DeploymentJobs deploymentJobs, Change change, + Change outstandingChange, Optional<IssueId> ownershipIssueId, ApplicationMetrics metrics, + Optional<RotationId> rotation) { + this.lock = lock; + this.id = id; + this.deploymentSpec = deploymentSpec; + this.validationOverrides = validationOverrides; + this.deployments = deployments; + this.deploymentJobs = deploymentJobs; + this.change = change; + this.outstandingChange = outstandingChange; + this.ownershipIssueId = ownershipIssueId; + this.metrics = metrics; + this.rotation = rotation; + } + + /** Returns a read-only copy of this */ + public Application get() { + return new Application(id, deploymentSpec, validationOverrides, deployments, deploymentJobs, change, + outstandingChange, ownershipIssueId, metrics, rotation); } public LockedApplication withProjectId(OptionalLong projectId) { - return new LockedApplication(new Builder(this).with(deploymentJobs().withProjectId(projectId))); + return new LockedApplication(lock, id, deploymentSpec, validationOverrides, deployments, + deploymentJobs.withProjectId(projectId), change, outstandingChange, + ownershipIssueId, metrics, rotation); } public LockedApplication withDeploymentIssueId(IssueId issueId) { - return new LockedApplication(new Builder(this).with(deploymentJobs().with(issueId))); + return new LockedApplication(lock, id, deploymentSpec, validationOverrides, deployments, + deploymentJobs.with(issueId), change, outstandingChange, + ownershipIssueId, metrics, rotation); } - public LockedApplication withJobCompletion(long projectId, JobType jobType, JobStatus.JobRun completion, Optional<DeploymentJobs.JobError> jobError) { - return new LockedApplication(new Builder(this).with(deploymentJobs().withCompletion(projectId, jobType, completion, jobError)) - ); + public LockedApplication withJobCompletion(long projectId, JobType jobType, JobStatus.JobRun completion, + Optional<DeploymentJobs.JobError> jobError) { + return new LockedApplication(lock, id, deploymentSpec, validationOverrides, deployments, + deploymentJobs.withCompletion(projectId, jobType, completion, jobError), + change, outstandingChange, ownershipIssueId, metrics, rotation); } public LockedApplication withJobTriggering(JobType jobType, JobStatus.JobRun job) { - return new LockedApplication(new Builder(this).with(deploymentJobs().withTriggering(jobType, job))); + return new LockedApplication(lock, id, deploymentSpec, validationOverrides, deployments, + deploymentJobs.withTriggering(jobType, job), change, outstandingChange, + ownershipIssueId, metrics, rotation); } public LockedApplication withNewDeployment(ZoneId zone, ApplicationVersion applicationVersion, Version version, Instant instant) { // Use info from previous deployment if available, otherwise create a new one. - Deployment previousDeployment = deployments().getOrDefault(zone, new Deployment(zone, applicationVersion, - version, instant)); + Deployment previousDeployment = deployments.getOrDefault(zone, new Deployment(zone, applicationVersion, + version, instant)); Deployment newDeployment = new Deployment(zone, applicationVersion, version, instant, previousDeployment.clusterUtils(), previousDeployment.clusterInfo(), @@ -85,146 +128,100 @@ public class LockedApplication extends Application { } public LockedApplication withClusterUtilization(ZoneId zone, Map<ClusterSpec.Id, ClusterUtilization> clusterUtilization) { - Deployment deployment = deployments().get(zone); + Deployment deployment = deployments.get(zone); if (deployment == null) return this; // No longer deployed in this zone. return with(deployment.withClusterUtils(clusterUtilization)); } public LockedApplication withClusterInfo(ZoneId zone, Map<ClusterSpec.Id, ClusterInfo> clusterInfo) { - Deployment deployment = deployments().get(zone); + Deployment deployment = deployments.get(zone); if (deployment == null) return this; // No longer deployed in this zone. return with(deployment.withClusterInfo(clusterInfo)); } public LockedApplication recordActivityAt(Instant instant, ZoneId zone) { - Deployment deployment = deployments().get(zone); + Deployment deployment = deployments.get(zone); if (deployment == null) return this; return with(deployment.recordActivityAt(instant)); } public LockedApplication with(ZoneId zone, DeploymentMetrics deploymentMetrics) { - Deployment deployment = deployments().get(zone); + Deployment deployment = deployments.get(zone); if (deployment == null) return this; // No longer deployed in this zone. return with(deployment.withMetrics(deploymentMetrics)); } public LockedApplication withoutDeploymentIn(ZoneId zone) { - Map<ZoneId, Deployment> deployments = new LinkedHashMap<>(deployments()); + Map<ZoneId, Deployment> deployments = new LinkedHashMap<>(this.deployments); deployments.remove(zone); - return new LockedApplication(new Builder(this).with(deployments)); + return with(deployments); } public LockedApplication withoutDeploymentJob(DeploymentJobs.JobType jobType) { - return new LockedApplication(new Builder(this).with(deploymentJobs().without(jobType))); + return new LockedApplication(lock, id, deploymentSpec, validationOverrides, deployments, + deploymentJobs.without(jobType), change, outstandingChange, + ownershipIssueId, metrics, rotation); } public LockedApplication with(DeploymentSpec deploymentSpec) { - return new LockedApplication(new Builder(this).with(deploymentSpec)); + return new LockedApplication(lock, id, deploymentSpec, validationOverrides, deployments, + deploymentJobs, change, outstandingChange, + ownershipIssueId, metrics, rotation); } public LockedApplication with(ValidationOverrides validationOverrides) { - return new LockedApplication(new Builder(this).with(validationOverrides)); + return new LockedApplication(lock, id, deploymentSpec, validationOverrides, deployments, + deploymentJobs, change, outstandingChange, + ownershipIssueId, metrics, rotation); } public LockedApplication withChange(Change change) { - return new LockedApplication(new Builder(this).withChange(change)); + return new LockedApplication(lock, id, deploymentSpec, validationOverrides, deployments, + deploymentJobs, change, outstandingChange, + ownershipIssueId, metrics, rotation); } public LockedApplication withOutstandingChange(Change outstandingChange) { - return new LockedApplication(new Builder(this).withOutstandingChange(outstandingChange)); + return new LockedApplication(lock, id, deploymentSpec, validationOverrides, deployments, + deploymentJobs, change, outstandingChange, + ownershipIssueId, metrics, rotation); } public LockedApplication withOwnershipIssueId(IssueId issueId) { - return new LockedApplication(new Builder(this).withOwnershipIssueId(Optional.ofNullable(issueId))); + return new LockedApplication(lock, id, deploymentSpec, validationOverrides, deployments, + deploymentJobs, change, outstandingChange, + Optional.ofNullable(issueId), metrics, rotation); } public LockedApplication with(MetricsService.ApplicationMetrics metrics) { - return new LockedApplication(new Builder(this).with(metrics)); + return new LockedApplication(lock, id, deploymentSpec, validationOverrides, deployments, + deploymentJobs, change, outstandingChange, + ownershipIssueId, metrics, rotation); } public LockedApplication with(RotationId rotation) { - return new LockedApplication(new Builder(this).with(rotation)); + return new LockedApplication(lock, id, deploymentSpec, validationOverrides, deployments, + deploymentJobs, change, outstandingChange, + ownershipIssueId, metrics, Optional.of(rotation)); } /** Don't expose non-leaf sub-objects. */ private LockedApplication with(Deployment deployment) { - Map<ZoneId, Deployment> deployments = new LinkedHashMap<>(deployments()); + Map<ZoneId, Deployment> deployments = new LinkedHashMap<>(this.deployments); deployments.put(deployment.zone(), deployment); - return new LockedApplication(new Builder(this).with(deployments)); - } - - private static class Builder { - - private final ApplicationId applicationId; - private DeploymentSpec deploymentSpec; - private ValidationOverrides validationOverrides; - private Map<ZoneId, Deployment> deployments; - private DeploymentJobs deploymentJobs; - private Change deploying; - private Change outstandingChange; - private Optional<IssueId> ownershipIssueId; - private ApplicationMetrics metrics; - private Optional<RotationId> rotation; - - private Builder(Application application) { - this.applicationId = application.id(); - this.deploymentSpec = application.deploymentSpec(); - this.validationOverrides = application.validationOverrides(); - this.deployments = application.deployments(); - this.deploymentJobs = application.deploymentJobs(); - this.deploying = application.change(); - this.outstandingChange = application.outstandingChange(); - this.ownershipIssueId = application.ownershipIssueId(); - this.metrics = application.metrics(); - this.rotation = application.rotation().map(ApplicationRotation::id); - } - - private Builder with(DeploymentSpec deploymentSpec) { - this.deploymentSpec = deploymentSpec; - return this; - } - - private Builder with(ValidationOverrides validationOverrides) { - this.validationOverrides = validationOverrides; - return this; - } - - private Builder with(Map<ZoneId, Deployment> deployments) { - this.deployments = deployments; - return this; - } - - private Builder with(DeploymentJobs deploymentJobs) { - this.deploymentJobs = deploymentJobs; - return this; - } - - private Builder withChange(Change deploying) { - this.deploying = deploying; - return this; - } - - private Builder withOutstandingChange(Change outstandingChange) { - this.outstandingChange = outstandingChange; - return this; - } - - private Builder withOwnershipIssueId(Optional<IssueId> ownershipIssueId) { - this.ownershipIssueId = ownershipIssueId; - return this; - } - - private Builder with(ApplicationMetrics metrics) { - this.metrics = metrics; - return this; - } - - private Builder with(RotationId rotation) { - this.rotation = Optional.of(rotation); - return this; - } + return with(deployments); + } + + private LockedApplication with(Map<ZoneId, Deployment> deployments) { + return new LockedApplication(lock, id, deploymentSpec, validationOverrides, deployments, + deploymentJobs, change, outstandingChange, + ownershipIssueId, metrics, rotation); + } + @Override + public String toString() { + return "application '" + id + "'"; } } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/ApplicationPackage.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/ApplicationPackage.java index 6df8e901653..40e2e4a92d1 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/ApplicationPackage.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/ApplicationPackage.java @@ -34,9 +34,8 @@ public class ApplicationPackage { * it must not be further changed by the caller. */ public ApplicationPackage(byte[] zippedContent) { - Objects.requireNonNull(zippedContent, "The application package content cannot be null"); + this.zippedContent = Objects.requireNonNull(zippedContent, "The application package content cannot be null"); this.contentHash = DigestUtils.shaHex(zippedContent); - this.zippedContent = zippedContent; this.deploymentSpec = extractFile("deployment.xml", zippedContent).map(DeploymentSpec::fromXml).orElse(DeploymentSpec.empty); this.validationOverrides = extractFile("validation-overrides.xml", zippedContent).map(ValidationOverrides::fromXml).orElse(ValidationOverrides.empty); } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/concurrent/Locks.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/concurrent/Locks.java deleted file mode 100644 index 6168812203a..00000000000 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/concurrent/Locks.java +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.controller.concurrent; - -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.locks.ReentrantLock; - -/** - * Holds a map of locks indexed on keys of a given type. - * This is suitable in cases where exclusive access should be granted to any one of a set of keyed objects and - * there is a finite collection of keyed objects. - * - * The returned locks are reentrant (i.e the owning thread may call lock multiple times) and auto-closable. - * - * Typical use is - * <code> - * try (Lock lock = locks.lock(id)) { - * exclusive use of the object with key id - * } - * </code> - * - * @author bratseth - */ -public class Locks<TYPE> { - - private final Map<TYPE, ReentrantLock> locks = new ConcurrentHashMap<>(); - - private final long timeoutMs; - - public Locks(int timeout, TimeUnit timeoutUnit) { - timeoutMs = timeoutUnit.toMillis(timeout); - } - - /** - * Locks key. This will block until the key is acquired. - * Users of this <b>must</b> close any lock acquired. - * - * @param key the key to lock - * @return the acquired lock - * @throws TimeoutException if the lock could not be acquired within the timeout - */ - public Lock lock(TYPE key) { - try { - ReentrantLock lock = locks.computeIfAbsent(key, k -> new ReentrantLock(true)); - boolean acquired = lock.tryLock(timeoutMs, TimeUnit.MILLISECONDS); - if ( ! acquired) - throw new TimeoutException("Timed out waiting for the lock to " + key); - return new Lock(lock); - } catch (InterruptedException e) { - throw new RuntimeException("Interrupted while waiting for lock of " + key); - } - } - -} diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentOrder.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentOrder.java index 405c8d17263..1c535a5a331 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentOrder.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentOrder.java @@ -3,7 +3,6 @@ package com.yahoo.vespa.hosted.controller.deployment; import com.yahoo.config.application.api.DeploymentSpec; import com.yahoo.config.provision.SystemName; -import com.yahoo.vespa.hosted.controller.Controller; import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneId; import com.yahoo.vespa.hosted.controller.application.Deployment; import com.yahoo.vespa.hosted.controller.application.DeploymentJobs; @@ -30,8 +29,7 @@ public class DeploymentOrder { private final Supplier<SystemName> system; public DeploymentOrder(Supplier<SystemName> system) { - Objects.requireNonNull(system, "system may not be null"); - this.system = system; + this.system = Objects.requireNonNull(system, "system may not be null"); } /** Returns jobs for given deployment spec, in the order they are declared */ @@ -46,25 +44,25 @@ public class DeploymentOrder { public List<JobStatus> sortBy(DeploymentSpec deploymentSpec, Collection<JobStatus> jobStatus) { List<DeploymentJobs.JobType> sortedJobs = jobsFrom(deploymentSpec); return jobStatus.stream() - .sorted(comparingInt(job -> sortedJobs.indexOf(job.type()))) - .collect(collectingAndThen(toList(), Collections::unmodifiableList)); + .sorted(comparingInt(job -> sortedJobs.indexOf(job.type()))) + .collect(collectingAndThen(toList(), Collections::unmodifiableList)); } /** Returns deployments sorted according to declared zones */ public List<Deployment> sortBy(List<DeploymentSpec.DeclaredZone> zones, Collection<Deployment> deployments) { List<ZoneId> productionZones = zones.stream() - .filter(z -> z.region().isPresent()) - .map(z -> ZoneId.from(z.environment(), z.region().get())) - .collect(toList()); + .filter(z -> z.region().isPresent()) + .map(z -> ZoneId.from(z.environment(), z.region().get())) + .collect(toList()); return deployments.stream() - .sorted(comparingInt(deployment -> productionZones.indexOf(deployment.zone()))) - .collect(collectingAndThen(toList(), Collections::unmodifiableList)); + .sorted(comparingInt(deployment -> productionZones.indexOf(deployment.zone()))) + .collect(collectingAndThen(toList(), Collections::unmodifiableList)); } /** Resolve job from deployment step */ public JobType toJob(DeploymentSpec.DeclaredZone zone) { return JobType.from(system.get(), zone.environment(), zone.region().orElse(null)) - .orElseThrow(() -> new IllegalArgumentException("Invalid zone " + zone)); + .orElseThrow(() -> new IllegalArgumentException("Invalid zone " + zone)); } } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTrigger.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTrigger.java index e902206ad8b..63a6ac234ff 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTrigger.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTrigger.java @@ -20,7 +20,6 @@ import com.yahoo.vespa.hosted.controller.application.DeploymentJobs.JobReport; import com.yahoo.vespa.hosted.controller.application.DeploymentJobs.JobType; import com.yahoo.vespa.hosted.controller.application.JobStatus; import com.yahoo.vespa.hosted.controller.application.JobStatus.JobRun; -import com.yahoo.vespa.hosted.controller.persistence.CuratorDb; import java.time.Clock; import java.time.Duration; @@ -78,14 +77,11 @@ public class DeploymentTrigger { private final DeploymentOrder order; private final BuildService buildService; - public DeploymentTrigger(Controller controller, CuratorDb curator, BuildService buildService, Clock clock) { - Objects.requireNonNull(controller, "controller cannot be null"); - Objects.requireNonNull(curator, "curator cannot be null"); - Objects.requireNonNull(clock, "clock cannot be null"); - this.controller = controller; - this.clock = clock; + public DeploymentTrigger(Controller controller, BuildService buildService, Clock clock) { + this.controller = Objects.requireNonNull(controller, "controller cannot be null"); + this.buildService = Objects.requireNonNull(buildService, "buildService cannot be null"); + this.clock = Objects.requireNonNull(clock, "clock cannot be null"); this.order = new DeploymentOrder(controller::system); - this.buildService = buildService; } public DeploymentOrder deploymentOrder() { @@ -116,15 +112,15 @@ public class DeploymentTrigger { triggering = JobRun.triggering(controller.systemVersion(), applicationVersion, Optional .empty(), Optional.empty(), "Application commit", clock.instant()); if (report.success()) { - if (acceptNewApplicationVersion(application)) - application = application.withChange(application.change().with(applicationVersion)) + if (acceptNewApplicationVersion(application.get())) + application = application.withChange(application.get().change().with(applicationVersion)) .withOutstandingChange(Change.empty()); else application = application.withOutstandingChange(Change.of(applicationVersion)); } } else { - triggering = application.deploymentJobs().statusOf(report.jobType()).flatMap(JobStatus::lastTriggered) + triggering = application.get().deploymentJobs().statusOf(report.jobType()).flatMap(JobStatus::lastTriggered) .orElseThrow(() -> new IllegalStateException("Notified of completion of " + report.jobType().jobName() + " for " + report.applicationId() + ", but that has neither been triggered nor deployed")); } @@ -132,7 +128,7 @@ public class DeploymentTrigger { report.jobType(), triggering.completion(report.buildNumber(), clock.instant()), report.jobError()); - application = application.withChange(remainingChange(application)); + application = application.withChange(remainingChange(application.get())); applications().store(application); }); } @@ -216,9 +212,9 @@ public class DeploymentTrigger { */ public void triggerChange(ApplicationId applicationId, Change change) { applications().lockOrThrow(applicationId, application -> { - if (application.changeAt(controller.clock().instant()).isPresent() && ! application.deploymentJobs().hasFailures()) + if (application.get().changeAt(controller.clock().instant()).isPresent() && ! application.get().deploymentJobs().hasFailures()) throw new IllegalArgumentException("Could not start " + change + " on " + application + ": " + - application.change() + " is already in progress"); + application.get().change() + " is already in progress"); application = application.withChange(change); if (change.application().isPresent()) application = application.withOutstandingChange(Change.empty()); @@ -230,7 +226,7 @@ public class DeploymentTrigger { /** Cancels a platform upgrade of the given application, and an application upgrade as well if {@code keepApplicationChange}. */ public void cancelChange(ApplicationId applicationId, boolean keepApplicationChange) { applications().lockOrThrow(applicationId, application -> { - applications().store(application.withChange(application.change().application() + applications().store(application.withChange(application.get().change().application() .filter(__ -> keepApplicationChange) .map(Change::of) .orElse(Change.empty()))); diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/Upgrader.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/Upgrader.java index bd8b8fc8747..22cbe942932 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/Upgrader.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/Upgrader.java @@ -7,7 +7,6 @@ import com.yahoo.vespa.curator.Lock; import com.yahoo.vespa.hosted.controller.Application; import com.yahoo.vespa.hosted.controller.Controller; import com.yahoo.vespa.hosted.controller.application.ApplicationList; -import com.yahoo.vespa.hosted.controller.application.Change; import com.yahoo.vespa.hosted.controller.persistence.CuratorDb; import com.yahoo.vespa.hosted.controller.versions.VespaVersion; import com.yahoo.vespa.hosted.controller.versions.VespaVersion.Confidence; @@ -18,6 +17,7 @@ import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.logging.Level; import java.util.logging.Logger; @@ -36,7 +36,7 @@ public class Upgrader extends Maintainer { public Upgrader(Controller controller, Duration interval, JobControl jobControl, CuratorDb curator) { super(controller, interval, jobControl); - this.curator = curator; + this.curator = Objects.requireNonNull(curator, "curator cannot be null"); } /** 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 d58c3ed7dae..6b35e09e049 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 @@ -678,7 +678,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler { ApplicationId id = ApplicationId.from(tenantName, applicationName, "default"); controller.applications().lockOrThrow(id, application -> { - controller.applications().deploymentTrigger().triggerChange(application.id(), Change.of(version)); + controller.applications().deploymentTrigger().triggerChange(application.get().id(), Change.of(version)); }); return new MessageResponse("Triggered deployment of application '" + id + "' on version " + version); } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTest.java index 0de153fc3f9..c24c8693688 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTest.java @@ -3,6 +3,7 @@ package com.yahoo.vespa.hosted.controller; import com.yahoo.component.Version; import com.yahoo.config.application.api.ValidationId; +import com.yahoo.config.application.api.ValidationOverrides; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.ApplicationName; import com.yahoo.config.provision.Environment; @@ -179,7 +180,9 @@ public class ControllerTest { fail("Expected exception due to illegal production deployment removal"); } catch (IllegalArgumentException e) { - assertEquals("deployment-removal: application 'tenant1.app1' is deployed in corp-us-east-1, but does not include this zone in deployment.xml", e.getMessage()); + assertEquals("deployment-removal: application 'tenant1.app1' is deployed in corp-us-east-1, but does not include this zone in deployment.xml. " + + ValidationOverrides.toAllowMessage(ValidationId.deploymentRemoval), + e.getMessage()); } assertNotNull("Zone was not removed", applications.require(app1.id()).deployments().get(productionCorpUsEast1.zone(main).get())); diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializerTest.java index 26cb68647e4..5c5827fa167 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializerTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializerTest.java @@ -156,26 +156,26 @@ public class ApplicationSerializerTest { assertEquals(original.deployments().get(zone2).metrics().writeLatencyMillis(), serialized.deployments().get(zone2).metrics().writeLatencyMillis(), Double.MIN_VALUE); { // test more deployment serialization cases - Application original2 = writable(original).withChange(Change.of(ApplicationVersion.from(new SourceRevision("repo1", "branch1", "commit1"), 42))); + Application original2 = writable(original).withChange(Change.of(ApplicationVersion.from(new SourceRevision("repo1", "branch1", "commit1"), 42))).get(); Application serialized2 = applicationSerializer.fromSlime(applicationSerializer.toSlime(original2)); assertEquals(original2.change(), serialized2.change()); assertEquals(serialized2.change().application().get().source(), original2.change().application().get().source()); - Application original3 = writable(original).withChange(Change.of(ApplicationVersion.from(new SourceRevision("a", "b", "c"), 42))); + Application original3 = writable(original).withChange(Change.of(ApplicationVersion.from(new SourceRevision("a", "b", "c"), 42))).get(); Application serialized3 = applicationSerializer.fromSlime(applicationSerializer.toSlime(original3)); assertEquals(original3.change(), serialized3.change()); assertEquals(serialized3.change().application().get().source(), original3.change().application().get().source()); - Application original4 = writable(original).withChange(Change.empty()); + Application original4 = writable(original).withChange(Change.empty()).get(); Application serialized4 = applicationSerializer.fromSlime(applicationSerializer.toSlime(original4)); assertEquals(original4.change(), serialized4.change()); - Application original5 = writable(original).withChange(Change.of(ApplicationVersion.from(new SourceRevision("a", "b", "c"), 42))); + Application original5 = writable(original).withChange(Change.of(ApplicationVersion.from(new SourceRevision("a", "b", "c"), 42))).get(); Application serialized5 = applicationSerializer.fromSlime(applicationSerializer.toSlime(original5)); assertEquals(original5.change(), serialized5.change()); - Application original6 = writable(original).withOutstandingChange(Change.of(ApplicationVersion.from(new SourceRevision("a", "b", "c"), 42))); + Application original6 = writable(original).withOutstandingChange(Change.of(ApplicationVersion.from(new SourceRevision("a", "b", "c"), 42))).get(); Application serialized6 = applicationSerializer.fromSlime(applicationSerializer.toSlime(original6)); assertEquals(original6.outstandingChange(), serialized6.outstandingChange()); } diff --git a/docprocs/src/test/java/com/yahoo/docprocs/indexing/DocumentScriptTestCase.java b/docprocs/src/test/java/com/yahoo/docprocs/indexing/DocumentScriptTestCase.java index 5b1a4412b41..419b60432c4 100644 --- a/docprocs/src/test/java/com/yahoo/docprocs/indexing/DocumentScriptTestCase.java +++ b/docprocs/src/test/java/com/yahoo/docprocs/indexing/DocumentScriptTestCase.java @@ -1,11 +1,13 @@ // Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.docprocs.indexing; +import com.yahoo.document.ArrayDataType; import com.yahoo.document.DataType; import com.yahoo.document.Document; import com.yahoo.document.DocumentType; import com.yahoo.document.DocumentUpdate; import com.yahoo.document.Field; +import com.yahoo.document.MapDataType; import com.yahoo.document.StructDataType; import com.yahoo.document.annotation.SpanTree; import com.yahoo.document.annotation.SpanTrees; @@ -16,6 +18,7 @@ import com.yahoo.document.datatypes.StringFieldValue; import com.yahoo.document.datatypes.Struct; import com.yahoo.document.datatypes.WeightedSet; import com.yahoo.document.fieldpathupdate.AssignFieldPathUpdate; +import com.yahoo.document.fieldpathupdate.FieldPathUpdate; import com.yahoo.document.update.FieldUpdate; import com.yahoo.document.update.MapValueUpdate; import com.yahoo.document.update.ValueUpdate; @@ -30,6 +33,7 @@ import org.junit.Test; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @@ -157,6 +161,82 @@ public class DocumentScriptTestCase { assertSpanTrees(str, "mySpanTree"); } + private class FieldPathFixture { + final DocumentType type; + final StructDataType structType; + final DataType structMap; + final DataType structArray; + + FieldPathFixture() { + type = newDocumentType(); + structType = new StructDataType("mystruct"); + structType.addField(new Field("title", DataType.STRING)); + structType.addField(new Field("rating", DataType.INT)); + structArray = new ArrayDataType(structType); + type.addField(new Field("structarray", structArray)); + structMap = new MapDataType(DataType.STRING, structType); + type.addField(new Field("structmap", structMap)); + type.addField(new Field("structfield", structType)); + } + + DocumentUpdate executeWithUpdate(String fieldName, FieldPathUpdate updateIn) { + DocumentUpdate update = new DocumentUpdate(type, "doc:scheme:"); + update.addFieldPathUpdate(updateIn); + return newScript(type, fieldName).execute(ADAPTER_FACTORY, update); + } + + FieldPathUpdate executeWithUpdateAndExpectFieldPath(String fieldName, FieldPathUpdate updateIn) { + DocumentUpdate update = executeWithUpdate(fieldName, updateIn); + assertEquals(1, update.getFieldPathUpdates().size()); + return update.getFieldPathUpdates().get(0); + } + } + + @Test + public void array_field_path_updates_survive_indexing_scripts() { + FieldPathFixture f = new FieldPathFixture(); + + Struct newElemValue = new Struct(f.structType); + newElemValue.setFieldValue("title", "iron moose 2, the moosening"); + + FieldPathUpdate updated = f.executeWithUpdateAndExpectFieldPath("structarray", new AssignFieldPathUpdate(f.type, "structarray[10]", newElemValue)); + + assertTrue(updated instanceof AssignFieldPathUpdate); + AssignFieldPathUpdate assignUpdate = (AssignFieldPathUpdate)updated; + assertEquals("structarray[10]", assignUpdate.getOriginalFieldPath()); + assertEquals(newElemValue, assignUpdate.getFieldValue()); + } + + @Test + public void map_field_path_updates_survive_indexing_scripts() { + FieldPathFixture f = new FieldPathFixture(); + + Struct newElemValue = new Struct(f.structType); + newElemValue.setFieldValue("title", "iron moose 3, moose in new york"); + + FieldPathUpdate updated = f.executeWithUpdateAndExpectFieldPath("structmap", new AssignFieldPathUpdate(f.type, "structmap{foo}", newElemValue)); + + assertTrue(updated instanceof AssignFieldPathUpdate); + AssignFieldPathUpdate assignUpdate = (AssignFieldPathUpdate)updated; + assertEquals("structmap{foo}", assignUpdate.getOriginalFieldPath()); + assertEquals(newElemValue, assignUpdate.getFieldValue()); + } + + @Test + public void nested_struct_fieldpath_update_is_not_converted_to_regular_field_value_update() { + FieldPathFixture f = new FieldPathFixture(); + + StringFieldValue newTitleValue = new StringFieldValue("iron moose 4, moose with a vengeance"); + DocumentUpdate update = f.executeWithUpdate("structfield", new AssignFieldPathUpdate(f.type, "structfield.title", newTitleValue)); + + assertEquals(1, update.getFieldPathUpdates().size()); + assertEquals(0, update.getFieldUpdates().size()); + assertTrue(update.getFieldPathUpdates().get(0) instanceof AssignFieldPathUpdate); + AssignFieldPathUpdate assignUpdate = (AssignFieldPathUpdate)update.getFieldPathUpdates().get(0); + assertEquals("structfield.title", assignUpdate.getOriginalFieldPath()); + assertEquals(newTitleValue, assignUpdate.getFieldValue()); + } + private static FieldValue processDocument(FieldValue fieldValue) { DocumentType docType = new DocumentType("myDocumentType"); docType.addField("myField", fieldValue.getDataType()); @@ -184,11 +264,15 @@ public class DocumentScriptTestCase { return update.getFieldUpdate("myField").getValueUpdate(0); } + private static DocumentScript newScript(DocumentType docType, String fieldName) { + return new DocumentScript(docType.getName(), Collections.singletonList(fieldName), + new StatementExpression(new InputExpression(fieldName), + new IndexExpression(fieldName))); + } + private static DocumentScript newScript(DocumentType docType) { String fieldName = docType.getFields().iterator().next().getName(); - return new DocumentScript(docType.getName(), Arrays.asList(fieldName), - new StatementExpression(new InputExpression(fieldName), - new IndexExpression(fieldName))); + return newScript(docType, fieldName); } private static StringFieldValue newString(String... spanTrees) { @@ -210,6 +294,7 @@ public class DocumentScriptTestCase { DocumentType type = new DocumentType("documentType"); type.addField("documentField", DataType.STRING); type.addField("extraField", DataType.STRING); + return type; } diff --git a/document/src/main/java/com/yahoo/document/datatypes/Array.java b/document/src/main/java/com/yahoo/document/datatypes/Array.java index e37a32f28f4..01326bcea62 100644 --- a/document/src/main/java/com/yahoo/document/datatypes/Array.java +++ b/document/src/main/java/com/yahoo/document/datatypes/Array.java @@ -290,7 +290,8 @@ public final class Array<T extends FieldValue> extends CollectionFieldValue<T> i if (pos < fieldPath.size()) { switch (fieldPath.get(pos).getType()) { case ARRAY_INDEX: - return iterateSubset(fieldPath.get(pos).getLookupIndex(), fieldPath.get(pos).getLookupIndex(), fieldPath, null, pos + 1, handler); + final int elemIndex = fieldPath.get(pos).getLookupIndex(); + return iterateSubset(elemIndex, elemIndex, fieldPath, null, pos + 1, handler); case VARIABLE: { FieldPathIteratorHandler.IndexValue val = handler.getVariables().get(fieldPath.get(pos).getVariableName()); if (val != null) { diff --git a/fat-model-dependencies/pom.xml b/fat-model-dependencies/pom.xml index 1415ca6e5aa..0011d108b98 100644 --- a/fat-model-dependencies/pom.xml +++ b/fat-model-dependencies/pom.xml @@ -16,13 +16,6 @@ <groupId>com.yahoo.vespa</groupId> <artifactId>config-model</artifactId> <version>${project.version}</version> - <exclusions> - <exclusion> - <!-- Large, and installed separately as part of Vespa --> - <groupId>org.tensorflow</groupId> - <artifactId>libtensorflow_jni</artifactId> - </exclusion> - </exclusions> </dependency> <dependency> <groupId>com.yahoo.vespa</groupId> diff --git a/indexinglanguage/src/main/java/com/yahoo/vespa/indexinglanguage/FieldPathUpdateHelper.java b/indexinglanguage/src/main/java/com/yahoo/vespa/indexinglanguage/FieldPathUpdateHelper.java index 171c6a8eb9a..5c170fe147e 100644 --- a/indexinglanguage/src/main/java/com/yahoo/vespa/indexinglanguage/FieldPathUpdateHelper.java +++ b/indexinglanguage/src/main/java/com/yahoo/vespa/indexinglanguage/FieldPathUpdateHelper.java @@ -20,19 +20,10 @@ public abstract class FieldPathUpdateHelper { if (!(update instanceof AssignFieldPathUpdate)) { return false; } - for (FieldPathEntry entry : update.getFieldPath()) { - switch (entry.getType()) { - case STRUCT_FIELD: - case MAP_ALL_KEYS: - case MAP_ALL_VALUES: - continue; - case ARRAY_INDEX: - case MAP_KEY: - case VARIABLE: - return false; - } - } - return true; + // Only consider field path updates that touch a top-level field as 'complete', + // as these may be converted to regular field value updates. + return ((update.getFieldPath().size() == 1) + && update.getFieldPath().get(0).getType() == FieldPathEntry.Type.STRUCT_FIELD); } public static void applyUpdate(FieldPathUpdate update, Document doc) { diff --git a/indexinglanguage/src/main/java/com/yahoo/vespa/indexinglanguage/IdentityFieldPathUpdateAdapter.java b/indexinglanguage/src/main/java/com/yahoo/vespa/indexinglanguage/IdentityFieldPathUpdateAdapter.java new file mode 100644 index 00000000000..42c9bd8c10c --- /dev/null +++ b/indexinglanguage/src/main/java/com/yahoo/vespa/indexinglanguage/IdentityFieldPathUpdateAdapter.java @@ -0,0 +1,68 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.indexinglanguage; + +import com.yahoo.document.DataType; +import com.yahoo.document.Document; +import com.yahoo.document.DocumentUpdate; +import com.yahoo.document.FieldPath; +import com.yahoo.document.datatypes.FieldValue; +import com.yahoo.document.fieldpathupdate.FieldPathUpdate; +import com.yahoo.vespa.indexinglanguage.expressions.Expression; +import com.yahoo.vespa.indexinglanguage.expressions.FieldValueAdapter; + +/** + * No-op update adapter which simply passes through the input update unchanged. + * I.e. getOutput() will return a DocumentUpdate containing only the FieldPathUpdate + * the IdentityFieldPathUpdateAdapter was created with. All other applicable calls are + * forwarded to the provided DocumentAdapter instance. + * + * This removes the need for a potentially lossy round-trip of update -> synthetic document -> update. + */ +public class IdentityFieldPathUpdateAdapter implements UpdateAdapter { + + private final FieldPathUpdate update; + private final DocumentAdapter fwdAdapter; + + public IdentityFieldPathUpdateAdapter(FieldPathUpdate update, DocumentAdapter fwdAdapter) { + this.update = update; + this.fwdAdapter = fwdAdapter; + } + + @Override + public DocumentUpdate getOutput() { + Document doc = fwdAdapter.getFullOutput(); + DocumentUpdate upd = new DocumentUpdate(doc.getDataType(), doc.getId()); + upd.addFieldPathUpdate(update); + return upd; + } + + @Override + public Expression getExpression(Expression expression) { + return expression; + } + + @Override + public FieldValue getInputValue(String fieldName) { + return fwdAdapter.getInputValue(fieldName); + } + + @Override + public FieldValue getInputValue(FieldPath fieldPath) { + return fwdAdapter.getInputValue(fieldPath); + } + + @Override + public FieldValueAdapter setOutputValue(Expression exp, String fieldName, FieldValue fieldValue) { + return fwdAdapter.setOutputValue(exp, fieldName, fieldValue); + } + + @Override + public DataType getInputType(Expression exp, String fieldName) { + return fwdAdapter.getInputType(exp, fieldName); + } + + @Override + public void tryOutputType(Expression exp, String fieldName, DataType valueType) { + fwdAdapter.tryOutputType(exp, fieldName, valueType); + } +} diff --git a/indexinglanguage/src/main/java/com/yahoo/vespa/indexinglanguage/SimpleAdapterFactory.java b/indexinglanguage/src/main/java/com/yahoo/vespa/indexinglanguage/SimpleAdapterFactory.java index 2ad09dfbdc4..509bdcaa32d 100644 --- a/indexinglanguage/src/main/java/com/yahoo/vespa/indexinglanguage/SimpleAdapterFactory.java +++ b/indexinglanguage/src/main/java/com/yahoo/vespa/indexinglanguage/SimpleAdapterFactory.java @@ -49,10 +49,12 @@ public class SimpleAdapterFactory implements AdapterFactory { Document complete = new Document(docType, upd.getId()); for (FieldPathUpdate fieldUpd : upd) { if (FieldPathUpdateHelper.isComplete(fieldUpd)) { + // A 'complete' field path update is basically a regular top-level field update + // in wolf's clothing. Convert it to a regular field update to be friendlier + // towards the search core backend. FieldPathUpdateHelper.applyUpdate(fieldUpd, complete); } else { - Document partial = FieldPathUpdateHelper.newPartialDocument(docId, fieldUpd); - ret.add(new FieldPathUpdateAdapter(newDocumentAdapter(partial, true), fieldUpd)); + ret.add(new IdentityFieldPathUpdateAdapter(fieldUpd, newDocumentAdapter(complete, true))); } } for (FieldUpdate fieldUpd : upd.getFieldUpdates()) { diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpServerConformanceTest.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpServerConformanceTest.java index 80c1cb8b458..77411fc080e 100644 --- a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpServerConformanceTest.java +++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpServerConformanceTest.java @@ -323,7 +323,7 @@ public class HttpServerConformanceTest extends ServerProviderConformanceTest { @Override @Test public void testRequestContentWriteExceptionAfterResponseWriteWithSyncCompletion() throws Throwable { - new TestRunner().expect(success()) + new TestRunner().expect(anyOf(success(), successNoContent())) .execute(); } diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/StorageMaintainer.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/StorageMaintainer.java index 7f2d1f1eff7..a7bf22591d4 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/StorageMaintainer.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/StorageMaintainer.java @@ -276,8 +276,8 @@ public class StorageMaintainer { */ public void handleCoreDumpsForContainer(ContainerName containerName, NodeSpec node, boolean force) { // Sample number of coredumps on the host - try { - numberOfCoredumpsOnHost.sample(Files.list(environment.pathInNodeAdminToDoneCoredumps()).count()); + try (Stream<Path> files = Files.list(environment.pathInNodeAdminToDoneCoredumps())) { + numberOfCoredumpsOnHost.sample(files.count()); } catch (IOException e) { // Ignore for now - this is either test or a misconfiguration } diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/identity/AthenzCredentialsMaintainer.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/identity/AthenzCredentialsMaintainer.java index f7e9c3ca1d8..ff85c49bb13 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/identity/AthenzCredentialsMaintainer.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/identity/AthenzCredentialsMaintainer.java @@ -1,6 +1,8 @@ // 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.node.admin.maintenance.identity; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.yahoo.vespa.athenz.api.AthenzService; import com.yahoo.vespa.athenz.client.zts.DefaultZtsClient; import com.yahoo.vespa.athenz.client.zts.InstanceIdentity; @@ -9,7 +11,7 @@ import com.yahoo.vespa.athenz.identity.ServiceIdentityProvider; import com.yahoo.vespa.athenz.identityprovider.api.EntityBindingsMapper; import com.yahoo.vespa.athenz.identityprovider.api.IdentityDocumentClient; import com.yahoo.vespa.athenz.identityprovider.api.SignedIdentityDocument; -import com.yahoo.vespa.athenz.identityprovider.api.VespaUniqueInstanceId; +import com.yahoo.vespa.athenz.identityprovider.api.bindings.SignedIdentityDocumentEntity; import com.yahoo.vespa.athenz.identityprovider.client.DefaultIdentityDocumentClient; import com.yahoo.vespa.athenz.identityprovider.client.InstanceCsrGenerator; import com.yahoo.vespa.athenz.tls.AthenzIdentityVerifier; @@ -19,9 +21,9 @@ import com.yahoo.vespa.athenz.tls.KeyUtils; import com.yahoo.vespa.athenz.tls.Pkcs10Csr; import com.yahoo.vespa.athenz.tls.SslContextBuilder; import com.yahoo.vespa.athenz.tls.X509CertificateUtils; +import com.yahoo.vespa.athenz.utils.SiaUtils; import com.yahoo.vespa.hosted.dockerapi.ContainerName; import com.yahoo.vespa.hosted.node.admin.component.Environment; -import com.yahoo.vespa.hosted.node.admin.configserver.noderepository.NodeSpec; import com.yahoo.vespa.hosted.node.admin.util.PrefixLogger; import javax.net.ssl.SSLContext; @@ -38,7 +40,6 @@ import java.security.cert.X509Certificate; import java.time.Clock; import java.time.Duration; import java.time.Instant; -import java.util.Set; import static java.util.Collections.singleton; @@ -53,12 +54,15 @@ public class AthenzCredentialsMaintainer { private static final Duration REFRESH_PERIOD = Duration.ofDays(1); private static final Path CONTAINER_SIA_DIRECTORY = Paths.get("/var/lib/sia"); + private static final ObjectMapper mapper = new ObjectMapper().registerModule(new JavaTimeModule()); + private final boolean enabled; private final PrefixLogger log; private final String hostname; private final Path trustStorePath; private final Path privateKeyFile; private final Path certificateFile; + private final Path identityDocumentFile; private final AthenzService containerIdentity; private final URI ztsEndpoint; private final Clock clock; @@ -66,8 +70,6 @@ public class AthenzCredentialsMaintainer { private final IdentityDocumentClient identityDocumentClient; private final InstanceCsrGenerator csrGenerator; private final AthenzService configserverIdentity; - private final String zoneRegion; - private final String zoneEnvironment; public AthenzCredentialsMaintainer(String hostname, Environment environment, @@ -82,8 +84,9 @@ public class AthenzCredentialsMaintainer { this.configserverIdentity = environment.getConfigserverAthenzIdentity(); this.csrGenerator = new InstanceCsrGenerator(environment.getCertificateDnsSuffix()); this.trustStorePath = environment.getTrustStorePath(); - this.privateKeyFile = getPrivateKeyFile(containerSiaDirectory, containerIdentity); - this.certificateFile = getCertificateFile(containerSiaDirectory, containerIdentity); + this.privateKeyFile = SiaUtils.getPrivateKeyFile(containerSiaDirectory, containerIdentity); + this.certificateFile = SiaUtils.getCertificateFile(containerSiaDirectory, containerIdentity); + this.identityDocumentFile = containerSiaDirectory.resolve("vespa-node-identity-document.json"); this.hostIdentityProvider = hostIdentityProvider; this.identityDocumentClient = new DefaultIdentityDocumentClient( @@ -91,15 +94,12 @@ public class AthenzCredentialsMaintainer { hostIdentityProvider, new AthenzIdentityVerifier(singleton(configserverIdentity))); this.clock = Clock.systemUTC(); - this.zoneRegion = environment.getRegion(); - this.zoneEnvironment = environment.getEnvironment(); } /** - * @param nodeSpec Node specification * @return Returns true if credentials were updated */ - public boolean converge(NodeSpec nodeSpec) { + public boolean converge() { try { if (!enabled) { log.debug("Feature disabled on this host - not fetching certificate"); @@ -107,26 +107,25 @@ public class AthenzCredentialsMaintainer { } log.debug("Checking certificate"); Instant now = clock.instant(); - VespaUniqueInstanceId instanceId = getVespaUniqueInstanceId(nodeSpec); - Set<String> ipAddresses = nodeSpec.getIpAddresses(); - if (!Files.exists(privateKeyFile) || !Files.exists(certificateFile)) { - log.info("Certificate and/or private key file does not exist"); + if (!Files.exists(privateKeyFile) || !Files.exists(certificateFile) || !Files.exists(identityDocumentFile)) { + log.info("Certificate/private key/identity document file does not exist"); Files.createDirectories(privateKeyFile.getParent()); Files.createDirectories(certificateFile.getParent()); - registerIdentity(instanceId, ipAddresses); + Files.createDirectories(identityDocumentFile.getParent()); + registerIdentity(); return true; } X509Certificate certificate = readCertificateFromFile(); Instant expiry = certificate.getNotAfter().toInstant(); if (isCertificateExpired(expiry, now)) { log.info(String.format("Certificate has expired (expiry=%s)", expiry.toString())); - registerIdentity(instanceId, ipAddresses); + registerIdentity(); return true; } Duration age = Duration.between(certificate.getNotBefore().toInstant(), now); if (shouldRefreshCredentials(age)) { log.info(String.format("Certificate is ready to be refreshed (age=%s)", age.toString())); - refreshIdentity(instanceId, ipAddresses); + refreshIdentity(); return true; } log.debug("Certificate is still valid"); @@ -148,19 +147,6 @@ public class AthenzCredentialsMaintainer { } } - private VespaUniqueInstanceId getVespaUniqueInstanceId(NodeSpec nodeSpec) { - NodeSpec.Membership membership = nodeSpec.getMembership().get(); - NodeSpec.Owner owner = nodeSpec.getOwner().get(); - return new VespaUniqueInstanceId( - membership.getIndex(), - membership.getClusterId(), - owner.getInstance(), - owner.getApplication(), - owner.getTenant(), - zoneRegion, - zoneEnvironment); - } - private boolean shouldRefreshCredentials(Duration age) { return age.compareTo(REFRESH_PERIOD) >= 0; } @@ -174,32 +160,32 @@ public class AthenzCredentialsMaintainer { return now.isAfter(expiry.minus(EXPIRY_MARGIN)); } - private void registerIdentity(VespaUniqueInstanceId instanceId, Set<String> ipAddresses) { + private void registerIdentity() { KeyPair keyPair = KeyUtils.generateKeypair(KeyAlgorithm.RSA); - Pkcs10Csr csr = csrGenerator.generateCsr(containerIdentity, instanceId, ipAddresses, keyPair); SignedIdentityDocument signedIdentityDocument = identityDocumentClient.getNodeIdentityDocument(hostname); + Pkcs10Csr csr = csrGenerator.generateCsr( + containerIdentity, signedIdentityDocument.providerUniqueId(), signedIdentityDocument.ipAddresses(), keyPair); try (ZtsClient ztsClient = new DefaultZtsClient(ztsEndpoint, hostIdentityProvider)) { InstanceIdentity instanceIdentity = ztsClient.registerInstance( configserverIdentity, containerIdentity, - instanceId.asDottedString(), + signedIdentityDocument.providerUniqueId().asDottedString(), EntityBindingsMapper.toAttestationData(signedIdentityDocument), false, csr); + writeIdentityDocument(signedIdentityDocument); writePrivateKeyAndCertificate(keyPair.getPrivate(), instanceIdentity.certificate()); log.info("Instance successfully registered and credentials written to file"); } catch (IOException e) { throw new UncheckedIOException(e); - } catch (Exception e) { - // TODO Change close() in ZtsClient to not throw checked exception - throw new RuntimeException(e); } } - private void refreshIdentity(VespaUniqueInstanceId instanceId, Set<String> ipAddresses) { + private void refreshIdentity() { + SignedIdentityDocument identityDocument = readIdentityDocument(); KeyPair keyPair = KeyUtils.generateKeypair(KeyAlgorithm.RSA); - Pkcs10Csr csr = csrGenerator.generateCsr(containerIdentity, instanceId, ipAddresses, keyPair); + Pkcs10Csr csr = csrGenerator.generateCsr(containerIdentity, identityDocument.providerUniqueId(), identityDocument.ipAddresses(), keyPair); SSLContext containerIdentitySslContext = new SslContextBuilder() .withKeyStore(privateKeyFile.toFile(), certificateFile.toFile()) @@ -210,16 +196,34 @@ public class AthenzCredentialsMaintainer { ztsClient.refreshInstance( configserverIdentity, containerIdentity, - instanceId.asDottedString(), + identityDocument.providerUniqueId().asDottedString(), false, csr); writePrivateKeyAndCertificate(keyPair.getPrivate(), instanceIdentity.certificate()); log.info("Instance successfully refreshed and credentials written to file"); } catch (IOException e) { throw new UncheckedIOException(e); - } catch (Exception e) { - // TODO Change close() in ZtsClient to not throw checked exception - throw new RuntimeException(e); + } + } + + private SignedIdentityDocument readIdentityDocument() { + try { + SignedIdentityDocumentEntity entity = mapper.readValue(identityDocumentFile.toFile(), SignedIdentityDocumentEntity.class); + return EntityBindingsMapper.toSignedIdentityDocument(entity); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + private void writeIdentityDocument(SignedIdentityDocument signedIdentityDocument) { + try { + SignedIdentityDocumentEntity entity = + EntityBindingsMapper.toSignedIdentityDocumentEntity(signedIdentityDocument); + Path tempIdentityDocumentFile = toTempPath(identityDocumentFile); + mapper.writeValue(tempIdentityDocumentFile.toFile(), entity); + Files.move(tempIdentityDocumentFile, identityDocumentFile, StandardCopyOption.ATOMIC_MOVE); + } catch (IOException e) { + throw new UncheckedIOException(e); } } @@ -237,18 +241,4 @@ public class AthenzCredentialsMaintainer { return Paths.get(file.toAbsolutePath().toString() + ".tmp"); } - // TODO Move to vespa-athenz - private static Path getPrivateKeyFile(Path root, AthenzService service) { - return root - .resolve("keys") - .resolve(String.format("%s.%s.key.pem", service.getDomain().getName(), service.getName())); - } - - // TODO Move to vespa-athenz - private static Path getCertificateFile(Path root, AthenzService service) { - return root - .resolve("certs") - .resolve(String.format("%s.%s.cert.pem", service.getDomain().getName(), service.getName())); - } - } diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeagent/NodeAgentImpl.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeagent/NodeAgentImpl.java index 7fa9a90b744..5f1b7aefcfe 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeagent/NodeAgentImpl.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeagent/NodeAgentImpl.java @@ -498,7 +498,7 @@ public class NodeAgentImpl implements NodeAgent { runLocalResumeScriptIfNeeded(node); - athenzCredentialsMaintainer.converge(node); + athenzCredentialsMaintainer.converge(); doBeforeConverge(node); diff --git a/node-maintainer/src/main/java/com/yahoo/vespa/hosted/node/maintainer/CoredumpHandler.java b/node-maintainer/src/main/java/com/yahoo/vespa/hosted/node/maintainer/CoredumpHandler.java index 99dfdb48334..63c74c17dd5 100644 --- a/node-maintainer/src/main/java/com/yahoo/vespa/hosted/node/maintainer/CoredumpHandler.java +++ b/node-maintainer/src/main/java/com/yahoo/vespa/hosted/node/maintainer/CoredumpHandler.java @@ -72,7 +72,7 @@ class CoredumpHandler { FileHelper.deleteDirectories(doneCoredumpsPath, Duration.ofDays(10), Optional.empty()); } - private void handleNewCoredumps() throws IOException { + private void handleNewCoredumps() { Path processingCoredumps = enqueueCoredumps(); processAndReportCoredumps(processingCoredumps); } @@ -82,12 +82,12 @@ class CoredumpHandler { * Moves a coredump to a new directory under the processing/ directory. Limit to only processing * one coredump at the time, starting with the oldest. */ - Path enqueueCoredumps() throws IOException { + Path enqueueCoredumps() { Path processingCoredumpsPath = coredumpsPath.resolve(PROCESSING_DIRECTORY_NAME); processingCoredumpsPath.toFile().mkdirs(); - if (Files.list(processingCoredumpsPath).count() > 0) return processingCoredumpsPath; + if (!FileHelper.listContentsOfDirectory(processingCoredumpsPath).isEmpty()) return processingCoredumpsPath; - Files.list(coredumpsPath) + FileHelper.listContentsOfDirectory(coredumpsPath).stream() .filter(path -> path.toFile().isFile() && ! path.getFileName().toString().startsWith(".")) .min((Comparator.comparingLong(o -> o.toFile().lastModified()))) .ifPresent(coredumpPath -> { @@ -101,10 +101,10 @@ class CoredumpHandler { return processingCoredumpsPath; } - void processAndReportCoredumps(Path processingCoredumpsPath) throws IOException { + void processAndReportCoredumps(Path processingCoredumpsPath) { doneCoredumpsPath.toFile().mkdirs(); - Files.list(processingCoredumpsPath) + FileHelper.listContentsOfDirectory(processingCoredumpsPath).stream() .filter(path -> path.toFile().isDirectory()) .forEach(coredumpDirectory -> { try { @@ -130,7 +130,7 @@ class CoredumpHandler { String collectMetadata(Path coredumpDirectory, Map<String, Object> nodeAttributes) throws IOException { Path metadataPath = coredumpDirectory.resolve(METADATA_FILE_NAME); if (!Files.exists(metadataPath)) { - Path coredumpPath = Files.list(coredumpDirectory).findFirst() + Path coredumpPath = FileHelper.listContentsOfDirectory(coredumpDirectory).stream().findFirst() .orElseThrow(() -> new RuntimeException("No coredump file found in processing directory " + coredumpDirectory)); Map<String, Object> metadata = coreCollector.collect(coredumpPath, installStatePath); metadata.putAll(nodeAttributes); diff --git a/node-maintainer/src/main/java/com/yahoo/vespa/hosted/node/maintainer/FileHelper.java b/node-maintainer/src/main/java/com/yahoo/vespa/hosted/node/maintainer/FileHelper.java index ae872042853..7b93e7ad98d 100644 --- a/node-maintainer/src/main/java/com/yahoo/vespa/hosted/node/maintainer/FileHelper.java +++ b/node-maintainer/src/main/java/com/yahoo/vespa/hosted/node/maintainer/FileHelper.java @@ -2,6 +2,7 @@ package com.yahoo.vespa.hosted.node.maintainer; import java.io.IOException; +import java.io.UncheckedIOException; import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.NoSuchFileException; @@ -63,7 +64,7 @@ public class FileHelper { throw new IllegalArgumentException("Number of files to keep must be a positive number"); } - List<Path> pathsInDeleteDir = Files.list(basePath) + List<Path> pathsInDeleteDir = listContentsOfDirectory(basePath).stream() .filter(Files::isRegularFile) .sorted(Comparator.comparing(FileHelper::getLastModifiedTime)) .skip(nMostRecentToKeep) @@ -153,13 +154,16 @@ public class FileHelper { return pattern == null || pattern.matcher(path.getFileName().toString()).find(); } - static List<Path> listContentsOfDirectory(Path basePath) { + /** + * @return list all files in a directory, returns empty list if directory does not exist + */ + public static List<Path> listContentsOfDirectory(Path basePath) { try (Stream<Path> directoryStream = Files.list(basePath)) { return directoryStream.collect(Collectors.toList()); } catch (NoSuchFileException ignored) { return Collections.emptyList(); } catch (IOException e) { - throw new RuntimeException("Failed to list contents of directory " + basePath.toAbsolutePath(), e); + throw new UncheckedIOException("Failed to list contents of directory " + basePath.toAbsolutePath(), e); } } @@ -167,7 +171,7 @@ public class FileHelper { try { return Files.getLastModifiedTime(path, LinkOption.NOFOLLOW_LINKS); } catch (IOException e) { - throw new RuntimeException("Failed to get last modified time of " + path.toAbsolutePath(), e); + throw new UncheckedIOException("Failed to get last modified time of " + path.toAbsolutePath(), e); } } } diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/GroupPreparer.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/GroupPreparer.java index 93704d244b5..d31b4438a38 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/GroupPreparer.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/GroupPreparer.java @@ -9,7 +9,6 @@ import com.yahoo.transaction.Mutex; import com.yahoo.vespa.hosted.provision.Node; import com.yahoo.vespa.hosted.provision.NodeRepository; -import java.time.Clock; import java.util.List; /** @@ -67,6 +66,7 @@ public class GroupPreparer { allocation.offer(prioritizer.prioritize()); if (! allocation.fullfilled()) throw new OutOfCapacityException("Could not satisfy " + requestedNodes + " for " + cluster + + " in " + application.toShortString() + outOfCapacityDetails(allocation)); // Extend reservation for already reserved nodes diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/DockerProvisioningTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/DockerProvisioningTest.java index e1b1d74c6d0..2cabee98c0d 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/DockerProvisioningTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/DockerProvisioningTest.java @@ -160,13 +160,13 @@ public class DockerProvisioningTest { assertEquals(setOf("host1", "host2"), hostsOf(tester.getNodes(application1, Node.State.active))); try { - ApplicationId application2 = tester.makeApplicationId(); + ApplicationId application2 = ApplicationId.from("tenant1", "app1", "default"); prepareAndActivate(application2, 3, false, tester); fail("Expected allocation failure"); } catch (Exception e) { assertEquals("No room for 3 nodes as 2 of 4 hosts are exclusive", - "Could not satisfy request for 3 nodes of flavor 'dockerSmall' for container cluster 'myContainer' group 0 6.39: Not enough nodes available due to host exclusivity constraints.", + "Could not satisfy request for 3 nodes of flavor 'dockerSmall' for container cluster 'myContainer' group 0 6.39 in tenant1.app1: Not enough nodes available due to host exclusivity constraints.", e.getMessage()); } diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/v2/filter/NodeIdentifierTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/v2/filter/NodeIdentifierTest.java index c0cead74f5f..11c7832091b 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/v2/filter/NodeIdentifierTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/v2/filter/NodeIdentifierTest.java @@ -29,6 +29,7 @@ import java.security.cert.X509Certificate; import java.time.Instant; import java.util.Optional; +import static com.yahoo.vespa.athenz.identityprovider.api.IdentityType.*; import static com.yahoo.vespa.athenz.tls.KeyAlgorithm.RSA; import static com.yahoo.vespa.athenz.tls.SignatureAlgorithm.SHA256_WITH_RSA; import static java.util.Collections.emptySet; @@ -161,7 +162,7 @@ public class NodeIdentifierTest { Pkcs10Csr csr = Pkcs10CsrBuilder .fromKeypair(new X500Principal("CN=" + TENANT_NODE_IDENTITY), KEYPAIR, SHA256_WITH_RSA) .build(); - VespaUniqueInstanceId vespaUniqueInstanceId = new VespaUniqueInstanceId(clusterIndex, clusterId, INSTANCE_ID, application, tenant, region, environment); + VespaUniqueInstanceId vespaUniqueInstanceId = new VespaUniqueInstanceId(clusterIndex, clusterId, INSTANCE_ID, application, tenant, region, environment, NODE); X509Certificate certificate = X509CertificateBuilder .fromCsr(csr, ATHENZ_YAHOO_CA_CERT.getSubjectX500Principal(), Instant.EPOCH, Instant.EPOCH.plusSeconds(60), KEYPAIR.getPrivate(), SHA256_WITH_RSA, 1) .addSubjectAlternativeName(vespaUniqueInstanceId.asDottedString() + ".instanceid.athenz.provider-name.vespa.yahoo.cloud") diff --git a/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/ServiceMonitorInstanceLookupService.java b/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/ServiceMonitorInstanceLookupService.java index a09ec29dada..d1d5f3e8c95 100644 --- a/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/ServiceMonitorInstanceLookupService.java +++ b/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/ServiceMonitorInstanceLookupService.java @@ -46,7 +46,7 @@ public class ServiceMonitorInstanceLookupService implements InstanceLookupServic return Optional.empty(); } if (applicationInstancesUsingHost.size() > 1) { - throw new AssertionError( + throw new IllegalStateException( "Major assumption broken: Multiple application instances contain host " + hostName.s() + ": " + applicationInstancesUsingHost); } diff --git a/service-monitor/pom.xml b/service-monitor/pom.xml index 70f9d4aa655..b8065ed3636 100644 --- a/service-monitor/pom.xml +++ b/service-monitor/pom.xml @@ -64,6 +64,12 @@ <version>${project.version}</version> </dependency> <dependency> + <groupId>com.yahoo.vespa</groupId> + <artifactId>vespa-athenz</artifactId> + <version>${project.version}</version> + <scope>provided</scope> + </dependency> + <dependency> <groupId>com.google.inject</groupId> <artifactId>guice</artifactId> <scope>provided</scope> @@ -76,6 +82,23 @@ <scope>provided</scope> </dependency> <dependency> + <groupId>com.fasterxml.jackson.core</groupId> + <artifactId>jackson-core</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>com.fasterxml.jackson.core</groupId> + <artifactId>jackson-databind</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.apache.httpcomponents</groupId> + <artifactId>httpclient</artifactId> + <version>4.5</version> + <!-- This is necessary to get 4.4's HostnameVerifier API of SSLConnectionSocketFactory::new --> + <scope>compile</scope> + </dependency> + <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <scope>test</scope> diff --git a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/ServiceStatusProvider.java b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/ServiceStatusProvider.java index 35003313775..75e61eef772 100644 --- a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/ServiceStatusProvider.java +++ b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/ServiceStatusProvider.java @@ -11,7 +11,13 @@ import com.yahoo.vespa.applicationmodel.ServiceType; * @author hakon */ public interface ServiceStatusProvider { - /** Get the {@link ServiceStatus} of a particular service. */ + /** + * Get the {@link ServiceStatus} of a particular service. + * + * <p>{@link ServiceStatus#NOT_CHECKED NOT_CHECKED} must be returned if the + * service status provider does does not monitor the service status for + * the particular application, cluster, service type, and config id. + */ ServiceStatus getStatus(ApplicationId applicationId, ClusterId clusterId, ServiceType serviceType, diff --git a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/application/ApplicationInstanceGenerator.java b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/application/ApplicationInstanceGenerator.java index ec2702bcfaf..cbdcce125cc 100644 --- a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/application/ApplicationInstanceGenerator.java +++ b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/application/ApplicationInstanceGenerator.java @@ -1,13 +1,148 @@ // Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.service.monitor.application; +import com.yahoo.config.model.api.ApplicationInfo; +import com.yahoo.config.model.api.HostInfo; +import com.yahoo.config.model.api.ServiceInfo; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.Zone; import com.yahoo.vespa.applicationmodel.ApplicationInstance; +import com.yahoo.vespa.applicationmodel.ApplicationInstanceId; +import com.yahoo.vespa.applicationmodel.ClusterId; +import com.yahoo.vespa.applicationmodel.ConfigId; +import com.yahoo.vespa.applicationmodel.HostName; +import com.yahoo.vespa.applicationmodel.ServiceCluster; +import com.yahoo.vespa.applicationmodel.ServiceClusterKey; +import com.yahoo.vespa.applicationmodel.ServiceInstance; +import com.yahoo.vespa.applicationmodel.ServiceStatus; +import com.yahoo.vespa.applicationmodel.ServiceType; +import com.yahoo.vespa.applicationmodel.TenantId; import com.yahoo.vespa.service.monitor.ServiceStatusProvider; +import com.yahoo.vespa.service.monitor.internal.ServiceId; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import static com.yahoo.vespa.service.monitor.application.ConfigServerApplication.CONFIG_SERVER_APPLICATION; /** + * Class to generate an ApplicationInstance given service status for a standard (deployed) application. + * * @author hakon */ -public interface ApplicationInstanceGenerator { - /** Make an ApplicationInstance based on current service status. */ - ApplicationInstance makeApplicationInstance(ServiceStatusProvider serviceStatusProvider); +public class ApplicationInstanceGenerator { + public static final String CLUSTER_ID_PROPERTY_NAME = "clustername"; + + private final ApplicationInfo applicationInfo; + private final Zone zone; + + public ApplicationInstanceGenerator(ApplicationInfo applicationInfo, Zone zone) { + this.applicationInfo = applicationInfo; + this.zone = zone; + } + + public ApplicationInstance makeApplicationInstance(ServiceStatusProvider serviceStatusProvider) { + Map<ServiceClusterKey, Set<ServiceInstance>> groupedServiceInstances = new HashMap<>(); + + for (HostInfo host : applicationInfo.getModel().getHosts()) { + HostName hostName = new HostName(host.getHostname()); + for (ServiceInfo serviceInfo : host.getServices()) { + ServiceClusterKey serviceClusterKey = toServiceClusterKey(serviceInfo); + ServiceInstance serviceInstance = + toServiceInstance( + applicationInfo.getApplicationId(), + serviceClusterKey.clusterId(), + serviceInfo, + hostName, + serviceStatusProvider); + + if (!groupedServiceInstances.containsKey(serviceClusterKey)) { + groupedServiceInstances.put(serviceClusterKey, new HashSet<>()); + } + groupedServiceInstances.get(serviceClusterKey).add(serviceInstance); + } + } + + Set<ServiceCluster> serviceClusters = groupedServiceInstances.entrySet().stream() + .map(entry -> new ServiceCluster( + entry.getKey().clusterId(), + entry.getKey().serviceType(), + entry.getValue())) + .collect(Collectors.toSet()); + + ApplicationInstance applicationInstance = new ApplicationInstance( + new TenantId(applicationInfo.getApplicationId().tenant().toString()), + toApplicationInstanceId(applicationInfo, zone), + serviceClusters); + + // Fill back-references + for (ServiceCluster serviceCluster : applicationInstance.serviceClusters()) { + serviceCluster.setApplicationInstance(applicationInstance); + for (ServiceInstance serviceInstance : serviceCluster.serviceInstances()) { + serviceInstance.setServiceCluster(serviceCluster); + } + } + + return applicationInstance; + } + + private ServiceInstance toServiceInstance( + ApplicationId applicationId, + ClusterId clusterId, + ServiceInfo serviceInfo, + HostName hostName, + ServiceStatusProvider serviceStatusProvider) { + ConfigId configId = toConfigId(serviceInfo); + + ServiceStatus status = serviceStatusProvider.getStatus( + applicationId, + clusterId, + toServiceType(serviceInfo), configId); + + return new ServiceInstance(configId, hostName, status); + } + + private ApplicationInstanceId toApplicationInstanceId(ApplicationInfo applicationInfo, Zone zone) { + if (applicationInfo.getApplicationId().equals(CONFIG_SERVER_APPLICATION.getApplicationId())) { + // Removing this historical discrepancy would break orchestration during rollout. + // An alternative may be to use a feature flag and flip it between releases, + // once that's available. + return new ApplicationInstanceId(applicationInfo.getApplicationId().application().value()); + } else { + return new ApplicationInstanceId(String.format("%s:%s:%s:%s", + applicationInfo.getApplicationId().application().value(), + zone.environment().value(), + zone.region().value(), + applicationInfo.getApplicationId().instance().value())); + } + } + + public static ServiceId getServiceId(ApplicationInfo applicationInfo, ServiceInfo serviceInfo) { + return new ServiceId( + applicationInfo.getApplicationId(), + getClusterId(serviceInfo), + toServiceType(serviceInfo), + toConfigId(serviceInfo)); + } + + private static ClusterId getClusterId(ServiceInfo serviceInfo) { + return new ClusterId(serviceInfo.getProperty(CLUSTER_ID_PROPERTY_NAME).orElse("")); + } + + private static ServiceClusterKey toServiceClusterKey(ServiceInfo serviceInfo) { + ClusterId clusterId = getClusterId(serviceInfo); + ServiceType serviceType = toServiceType(serviceInfo); + return new ServiceClusterKey(clusterId, serviceType); + } + + private static ServiceType toServiceType(ServiceInfo serviceInfo) { + return new ServiceType(serviceInfo.getServiceType()); + } + + private static ConfigId toConfigId(ServiceInfo serviceInfo) { + return new ConfigId(serviceInfo.getConfigId()); + } } diff --git a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/application/ConfigServerAppGenerator.java b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/application/ConfigServerAppGenerator.java deleted file mode 100644 index 76ca59cf583..00000000000 --- a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/application/ConfigServerAppGenerator.java +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.service.monitor.application; - -import com.yahoo.vespa.applicationmodel.ApplicationInstance; -import com.yahoo.vespa.applicationmodel.ConfigId; -import com.yahoo.vespa.applicationmodel.HostName; -import com.yahoo.vespa.applicationmodel.ServiceCluster; -import com.yahoo.vespa.applicationmodel.ServiceInstance; -import com.yahoo.vespa.applicationmodel.ServiceStatus; -import com.yahoo.vespa.service.monitor.ServiceStatusProvider; - -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; - -/** - * Class for generating an ApplicationInstance for the synthesized config server application. - * - * @author hakon - */ -public class ConfigServerAppGenerator implements ApplicationInstanceGenerator { - private final List<String> hostnames; - - public ConfigServerAppGenerator(List<String> hostnames) { - this.hostnames = hostnames; - } - - @Override - public ApplicationInstance makeApplicationInstance(ServiceStatusProvider statusProvider) { - Set<ServiceInstance> serviceInstances = hostnames.stream() - .map(hostname -> makeServiceInstance(hostname, statusProvider)) - .collect(Collectors.toSet()); - - ServiceCluster serviceCluster = new ServiceCluster( - ConfigServerApplication.CLUSTER_ID, - ConfigServerApplication.SERVICE_TYPE, - serviceInstances); - - Set<ServiceCluster> serviceClusters = new HashSet<>(); - serviceClusters.add(serviceCluster); - - ApplicationInstance applicationInstance = new ApplicationInstance( - ConfigServerApplication.TENANT_ID, - ConfigServerApplication.APPLICATION_INSTANCE_ID, - serviceClusters); - - // Fill back-references - serviceCluster.setApplicationInstance(applicationInstance); - for (ServiceInstance serviceInstance : serviceCluster.serviceInstances()) { - serviceInstance.setServiceCluster(serviceCluster); - } - - return applicationInstance; - } - - private ServiceInstance makeServiceInstance(String hostname, ServiceStatusProvider statusProvider) { - ConfigId configId = new ConfigId(ConfigServerApplication.CONFIG_ID_PREFIX + hostname); - ServiceStatus status = statusProvider.getStatus( - ConfigServerApplication.CONFIG_SERVER_APPLICATION.getApplicationId(), - ConfigServerApplication.CLUSTER_ID, - ConfigServerApplication.SERVICE_TYPE, - configId); - - return new ServiceInstance(configId, new HostName(hostname), status); - } -} diff --git a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/application/ConfigServerApplication.java b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/application/ConfigServerApplication.java index 132bb0927b8..5ad38cebcfc 100644 --- a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/application/ConfigServerApplication.java +++ b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/application/ConfigServerApplication.java @@ -1,12 +1,26 @@ // Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.service.monitor.application; +import com.yahoo.cloud.config.ConfigserverConfig; +import com.yahoo.config.model.api.ApplicationInfo; +import com.yahoo.config.model.api.HostInfo; +import com.yahoo.config.model.api.PortInfo; +import com.yahoo.config.model.api.ServiceInfo; import com.yahoo.config.provision.ClusterSpec; import com.yahoo.config.provision.NodeType; import com.yahoo.vespa.applicationmodel.ApplicationInstanceId; import com.yahoo.vespa.applicationmodel.ClusterId; +import com.yahoo.vespa.applicationmodel.ConfigId; import com.yahoo.vespa.applicationmodel.ServiceType; import com.yahoo.vespa.applicationmodel.TenantId; +import com.yahoo.vespa.service.monitor.internal.ModelGenerator; +import com.yahoo.vespa.service.monitor.internal.health.ApplicationHealthMonitor; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; /** * A service/application model of the config server with health status. @@ -21,8 +35,44 @@ public class ConfigServerApplication extends HostedVespaApplication { public static final ServiceType SERVICE_TYPE = new ServiceType("configserver"); public static final String CONFIG_ID_PREFIX = "configid."; + public static ConfigId configIdFrom(int index) { + return new ConfigId(CONFIG_ID_PREFIX + index); + } + private ConfigServerApplication() { super("zone-config-servers", NodeType.config, ClusterSpec.Type.admin, ClusterSpec.Id.from("zone-config-servers")); } + + public ApplicationInfo makeApplicationInfo(ConfigserverConfig config) { + List<HostInfo> hostInfos = new ArrayList<>(); + List<ConfigserverConfig.Zookeeperserver> zooKeeperServers = config.zookeeperserver(); + for (int index = 0; index < zooKeeperServers.size(); ++index) { + String hostname = zooKeeperServers.get(index).hostname(); + hostInfos.add(makeHostInfo(hostname, config.httpport(), index)); + } + + return new ApplicationInfo( + CONFIG_SERVER_APPLICATION.getApplicationId(), + 0, + new HostsModel(hostInfos)); + } + + private static HostInfo makeHostInfo(String hostname, int port, int configIndex) { + PortInfo portInfo = new PortInfo(port, ApplicationHealthMonitor.PORT_TAGS_HEALTH); + + Map<String, String> properties = new HashMap<>(); + properties.put(ModelGenerator.CLUSTER_ID_PROPERTY_NAME, CLUSTER_ID.s()); + + ServiceInfo serviceInfo = new ServiceInfo( + // service name == service type for the first service of each type on each host + SERVICE_TYPE.s(), + SERVICE_TYPE.s(), + Collections.singletonList(portInfo), + properties, + configIdFrom(configIndex).s(), + hostname); + + return new HostInfo(hostname, Collections.singletonList(serviceInfo)); + } } diff --git a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/application/DeployedAppGenerator.java b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/application/DeployedAppGenerator.java deleted file mode 100644 index 2691a8bf1ee..00000000000 --- a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/application/DeployedAppGenerator.java +++ /dev/null @@ -1,127 +0,0 @@ -// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.service.monitor.application; - -import com.yahoo.config.model.api.ApplicationInfo; -import com.yahoo.config.model.api.HostInfo; -import com.yahoo.config.model.api.ServiceInfo; -import com.yahoo.config.provision.ApplicationId; -import com.yahoo.config.provision.Zone; -import com.yahoo.vespa.applicationmodel.ApplicationInstance; -import com.yahoo.vespa.applicationmodel.ApplicationInstanceId; -import com.yahoo.vespa.applicationmodel.ClusterId; -import com.yahoo.vespa.applicationmodel.ConfigId; -import com.yahoo.vespa.applicationmodel.HostName; -import com.yahoo.vespa.applicationmodel.ServiceCluster; -import com.yahoo.vespa.applicationmodel.ServiceClusterKey; -import com.yahoo.vespa.applicationmodel.ServiceInstance; -import com.yahoo.vespa.applicationmodel.ServiceStatus; -import com.yahoo.vespa.applicationmodel.ServiceType; -import com.yahoo.vespa.applicationmodel.TenantId; -import com.yahoo.vespa.service.monitor.ServiceStatusProvider; - -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; - -/** - * Class to generate an ApplicationInstance given service status for a standard (deployed) application. - * - * @author hakon - */ -public class DeployedAppGenerator implements ApplicationInstanceGenerator { - public static final String CLUSTER_ID_PROPERTY_NAME = "clustername"; - - private final ApplicationInfo applicationInfo; - private final Zone zone; - - public DeployedAppGenerator(ApplicationInfo applicationInfo, Zone zone) { - this.applicationInfo = applicationInfo; - this.zone = zone; - } - - @Override - public ApplicationInstance makeApplicationInstance(ServiceStatusProvider serviceStatusProvider) { - Map<ServiceClusterKey, Set<ServiceInstance>> groupedServiceInstances = new HashMap<>(); - - for (HostInfo host : applicationInfo.getModel().getHosts()) { - HostName hostName = new HostName(host.getHostname()); - for (ServiceInfo serviceInfo : host.getServices()) { - ServiceClusterKey serviceClusterKey = toServiceClusterKey(serviceInfo); - ServiceInstance serviceInstance = - toServiceInstance( - applicationInfo.getApplicationId(), - serviceClusterKey.clusterId(), - serviceInfo, - hostName, - serviceStatusProvider); - - if (!groupedServiceInstances.containsKey(serviceClusterKey)) { - groupedServiceInstances.put(serviceClusterKey, new HashSet<>()); - } - groupedServiceInstances.get(serviceClusterKey).add(serviceInstance); - } - } - - Set<ServiceCluster> serviceClusters = groupedServiceInstances.entrySet().stream() - .map(entry -> new ServiceCluster( - entry.getKey().clusterId(), - entry.getKey().serviceType(), - entry.getValue())) - .collect(Collectors.toSet()); - - ApplicationInstance applicationInstance = new ApplicationInstance( - new TenantId(applicationInfo.getApplicationId().tenant().toString()), - toApplicationInstanceId(applicationInfo, zone), - serviceClusters); - - // Fill back-references - for (ServiceCluster serviceCluster : applicationInstance.serviceClusters()) { - serviceCluster.setApplicationInstance(applicationInstance); - for (ServiceInstance serviceInstance : serviceCluster.serviceInstances()) { - serviceInstance.setServiceCluster(serviceCluster); - } - } - - return applicationInstance; - } - - static ClusterId getClusterId(ServiceInfo serviceInfo) { - return new ClusterId(serviceInfo.getProperty(CLUSTER_ID_PROPERTY_NAME).orElse("")); - } - - private ServiceClusterKey toServiceClusterKey(ServiceInfo serviceInfo) { - ClusterId clusterId = getClusterId(serviceInfo); - ServiceType serviceType = toServiceType(serviceInfo); - return new ServiceClusterKey(clusterId, serviceType); - } - - private ServiceInstance toServiceInstance( - ApplicationId applicationId, - ClusterId clusterId, - ServiceInfo serviceInfo, - HostName hostName, - ServiceStatusProvider serviceStatusProvider) { - ConfigId configId = new ConfigId(serviceInfo.getConfigId()); - - ServiceStatus status = serviceStatusProvider.getStatus( - applicationId, - clusterId, - toServiceType(serviceInfo), configId); - - return new ServiceInstance(configId, hostName, status); - } - - private ApplicationInstanceId toApplicationInstanceId(ApplicationInfo applicationInfo, Zone zone) { - return new ApplicationInstanceId(String.format("%s:%s:%s:%s", - applicationInfo.getApplicationId().application().value(), - zone.environment().value(), - zone.region().value(), - applicationInfo.getApplicationId().instance().value())); - } - - private ServiceType toServiceType(ServiceInfo serviceInfo) { - return new ServiceType(serviceInfo.getServiceType()); - } -} diff --git a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/application/HostsModel.java b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/application/HostsModel.java new file mode 100644 index 00000000000..225ffb0adbc --- /dev/null +++ b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/application/HostsModel.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.service.monitor.application; + +import com.yahoo.config.FileReference; +import com.yahoo.config.model.api.FileDistribution; +import com.yahoo.config.model.api.HostInfo; +import com.yahoo.config.model.api.Model; +import com.yahoo.config.provision.AllocatedHosts; +import com.yahoo.vespa.config.ConfigKey; +import com.yahoo.vespa.config.ConfigPayload; +import com.yahoo.vespa.config.buildergen.ConfigDefinition; + +import java.time.Instant; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +/** + * Model that only supports the subset necessary to create an ApplicationInstance. + * + * @author hakon + */ +public class HostsModel implements Model { + private final Collection<HostInfo> hosts; + + public HostsModel(List<HostInfo> hosts) { + this.hosts = Collections.unmodifiableCollection(hosts); + } + + @Override + public Collection<HostInfo> getHosts() { + return hosts; + } + + @Override + public ConfigPayload getConfig(ConfigKey<?> configKey, ConfigDefinition configDefinition) { + throw new UnsupportedOperationException(); + } + + @Override + public Set<ConfigKey<?>> allConfigsProduced() { + throw new UnsupportedOperationException(); + } + + @Override + public Set<String> allConfigIds() { + throw new UnsupportedOperationException(); + } + + @Override + public void distributeFiles(FileDistribution fileDistribution) { + throw new UnsupportedOperationException(); + } + + @Override + public Set<FileReference> fileReferences() { + throw new UnsupportedOperationException(); + } + + @Override + public AllocatedHosts allocatedHosts() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean allowModelVersionMismatch(Instant now) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean skipOldConfigModels(Instant now) { + throw new UnsupportedOperationException(); + } +} diff --git a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/application/ZoneApplication.java b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/application/ZoneApplication.java index 6bbf0cb6d1d..c10015d3bfa 100644 --- a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/application/ZoneApplication.java +++ b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/application/ZoneApplication.java @@ -21,8 +21,8 @@ public class ZoneApplication { .createHostedVespaApplicationId("routing"); public static boolean isNodeAdminService(ApplicationId applicationId, - ClusterId clusterId, - ServiceType serviceType) { + ClusterId clusterId, + ServiceType serviceType) { return Objects.equals(applicationId, ZONE_APPLICATION_ID) && Objects.equals(serviceType, ServiceType.CONTAINER) && Objects.equals(clusterId, ClusterId.NODE_ADMIN); diff --git a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/DuperModel.java b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/DuperModel.java new file mode 100644 index 00000000000..80e0bfd2710 --- /dev/null +++ b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/DuperModel.java @@ -0,0 +1,42 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.service.monitor.internal; + +import com.yahoo.cloud.config.ConfigserverConfig; +import com.yahoo.config.model.api.ApplicationInfo; +import com.yahoo.config.model.api.SuperModel; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static com.yahoo.vespa.service.monitor.application.ConfigServerApplication.CONFIG_SERVER_APPLICATION; + +/** + * The {@code DuperModel} unites the {@link com.yahoo.config.model.api.SuperModel SuperModel} + * with the synthetically produced applications like the config server application. + * + * @author hakon + */ +public class DuperModel { + private final List<ApplicationInfo> staticApplicationInfos = new ArrayList<>(); + + public DuperModel(ConfigserverConfig configServerConfig) { + // Single-tenant applications have the config server as part of the application model. + // TODO: Add health monitoring for config server when part of application model. + if (configServerConfig.multitenant()) { + staticApplicationInfos.add(CONFIG_SERVER_APPLICATION.makeApplicationInfo(configServerConfig)); + } + } + + /** For testing. */ + DuperModel(ApplicationInfo... staticApplicationInfos) { + this.staticApplicationInfos.addAll(Arrays.asList(staticApplicationInfos)); + } + + public List<ApplicationInfo> getApplicationInfos(SuperModel superModelSnapshot) { + List<ApplicationInfo> allApplicationInfos = new ArrayList<>(); + allApplicationInfos.addAll(staticApplicationInfos); + allApplicationInfos.addAll(superModelSnapshot.getAllApplicationInfos()); + return allApplicationInfos; + } +} diff --git a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/DuperModelListener.java b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/DuperModelListener.java new file mode 100644 index 00000000000..235c7db5c36 --- /dev/null +++ b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/DuperModelListener.java @@ -0,0 +1,28 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.service.monitor.internal; + +import com.yahoo.config.model.api.ApplicationInfo; +import com.yahoo.config.model.api.SuperModel; +import com.yahoo.config.provision.ApplicationId; + +/** + * Interface for listening for changes to the {@link DuperModel}. + * + * @author hakon + */ +public interface DuperModelListener { + /** + * An application has been activated: + * + * <ul> + * <li>A synthetic application like the config server application has been added/"activated" + * <li>A super model application has been activated (see + * {@link com.yahoo.config.model.api.SuperModelListener#applicationActivated(SuperModel, ApplicationInfo) + * SuperModelListener} + * </ul> + */ + void applicationActivated(ApplicationInfo application); + + /** Application has been removed. */ + void applicationRemoved(ApplicationId id); +} diff --git a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/ModelGenerator.java b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/ModelGenerator.java index 9da449289a7..ad2f223acf8 100644 --- a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/ModelGenerator.java +++ b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/ModelGenerator.java @@ -1,56 +1,40 @@ // Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.service.monitor.internal; -import com.yahoo.config.model.api.SuperModel; +import com.yahoo.config.model.api.ApplicationInfo; import com.yahoo.config.provision.Zone; import com.yahoo.vespa.applicationmodel.ApplicationInstance; import com.yahoo.vespa.applicationmodel.ApplicationInstanceReference; import com.yahoo.vespa.service.monitor.ServiceModel; import com.yahoo.vespa.service.monitor.ServiceStatusProvider; import com.yahoo.vespa.service.monitor.application.ApplicationInstanceGenerator; -import com.yahoo.vespa.service.monitor.application.ConfigServerAppGenerator; -import com.yahoo.vespa.service.monitor.application.DeployedAppGenerator; -import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Map; import java.util.function.Function; import java.util.stream.Collectors; /** - * Util to convert SuperModel to ServiceModel and application model classes + * Util to make ServiceModel and its related application model classes */ public class ModelGenerator { public static final String CLUSTER_ID_PROPERTY_NAME = "clustername"; - private final List<ApplicationInstanceGenerator> staticGenerators; - - public ModelGenerator(List<String> configServerHosts) { - if (configServerHosts.isEmpty()) { - staticGenerators = Collections.emptyList(); - } else { - staticGenerators = Collections.singletonList(new ConfigServerAppGenerator(configServerHosts)); - } - } - /** * Create service model based primarily on super model. * * If the configServerhosts is non-empty, a config server application is added. */ - ServiceModel toServiceModel( - SuperModel superModel, - Zone zone, - ServiceStatusProvider serviceStatusProvider) { - List<ApplicationInstanceGenerator> generators = new ArrayList<>(staticGenerators); - superModel.getAllApplicationInfos() - .forEach(info -> generators.add(new DeployedAppGenerator(info, zone))); - - Map<ApplicationInstanceReference, ApplicationInstance> applicationInstances = generators.stream() - .map(generator -> generator.makeApplicationInstance(serviceStatusProvider)) - .collect(Collectors.toMap(ApplicationInstance::reference, Function.identity())); + public ServiceModel toServiceModel(List<ApplicationInfo> allApplicationInfos, + Zone zone, + ServiceStatusProvider serviceStatusProvider) { + Map<ApplicationInstanceReference, ApplicationInstance> applicationInstances = + allApplicationInfos.stream() + .map(info -> new ApplicationInstanceGenerator(info, zone) + .makeApplicationInstance(serviceStatusProvider)) + .collect(Collectors.toMap(ApplicationInstance::reference, Function.identity())); return new ServiceModel(applicationInstances); } + } diff --git a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/MonitorManager.java b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/MonitorManager.java index 49863672c43..1edf3a18215 100644 --- a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/MonitorManager.java +++ b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/MonitorManager.java @@ -1,11 +1,10 @@ // Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.service.monitor.internal;// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.service.monitor.internal; -import com.yahoo.config.model.api.SuperModelListener; import com.yahoo.vespa.service.monitor.ServiceStatusProvider; /** * @author hakon */ -public interface MonitorManager extends SuperModelListener, ServiceStatusProvider { +public interface MonitorManager extends DuperModelListener, ServiceStatusProvider { } diff --git a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/ServiceId.java b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/ServiceId.java new file mode 100644 index 00000000000..993ea7fed5c --- /dev/null +++ b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/ServiceId.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.service.monitor.internal; + +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.vespa.applicationmodel.ClusterId; +import com.yahoo.vespa.applicationmodel.ConfigId; +import com.yahoo.vespa.applicationmodel.ServiceType; + +import javax.annotation.concurrent.Immutable; +import java.util.Objects; + +/** + * Identifies a service. + * + * @author hakon + */ +@Immutable +public class ServiceId { + private final ApplicationId applicationId; + private final ClusterId clusterId; + private final ServiceType serviceType; + private final ConfigId configId; + + public ServiceId(ApplicationId applicationId, + ClusterId clusterId, + ServiceType serviceType, + ConfigId configId) { + this.applicationId = applicationId; + this.clusterId = clusterId; + this.serviceType = serviceType; + this.configId = configId; + } + + public ApplicationId getApplicationId() { + return applicationId; + } + + public ClusterId getClusterId() { + return clusterId; + } + + public ServiceType getServiceType() { + return serviceType; + } + + public ConfigId getConfigId() { + return configId; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ServiceId serviceId = (ServiceId) o; + return Objects.equals(applicationId, serviceId.applicationId) && + Objects.equals(clusterId, serviceId.clusterId) && + Objects.equals(serviceType, serviceId.serviceType) && + Objects.equals(configId, serviceId.configId); + } + + @Override + public int hashCode() { + return Objects.hash(applicationId, clusterId, serviceType, configId); + } + + @Override + public String toString() { + return "ServiceId{" + + "applicationId=" + applicationId + + ", clusterId=" + clusterId + + ", serviceType=" + serviceType + + ", configId=" + configId + + '}'; + } +} diff --git a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/ServiceMonitorImpl.java b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/ServiceMonitorImpl.java index 97c4fdda0f3..bd8fd4a50e0 100644 --- a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/ServiceMonitorImpl.java +++ b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/ServiceMonitorImpl.java @@ -14,10 +14,7 @@ import com.yahoo.vespa.service.monitor.ServiceMonitor; import com.yahoo.vespa.service.monitor.internal.health.HealthMonitorManager; import com.yahoo.vespa.service.monitor.internal.slobrok.SlobrokMonitorManagerImpl; -import java.util.Collections; -import java.util.List; import java.util.Map; -import java.util.stream.Collectors; public class ServiceMonitorImpl implements ServiceMonitor { private final ServiceModelCache serviceModelCache; @@ -32,30 +29,20 @@ public class ServiceMonitorImpl implements ServiceMonitor { Zone zone = superModelProvider.getZone(); ServiceMonitorMetrics metrics = new ServiceMonitorMetrics(metric, timer); - UnionMonitorManager monitorManager = new UnionMonitorManager( - slobrokMonitorManager, - healthMonitorManager, - configserverConfig); + DuperModel duperModel = new DuperModel(configserverConfig); + UnionMonitorManager monitorManager = + new UnionMonitorManager(slobrokMonitorManager, healthMonitorManager); SuperModelListenerImpl superModelListener = new SuperModelListenerImpl( monitorManager, metrics, - new ModelGenerator(toConfigServerList(configserverConfig)), + duperModel, + new ModelGenerator(), zone); superModelListener.start(superModelProvider); serviceModelCache = new ServiceModelCache(superModelListener, timer); } - private List<String> toConfigServerList(ConfigserverConfig configserverConfig) { - if (configserverConfig.multitenant()) { - return configserverConfig.zookeeperserver().stream() - .map(ConfigserverConfig.Zookeeperserver::hostname) - .collect(Collectors.toList()); - } - - return Collections.emptyList(); - } - @Override public Map<ApplicationInstanceReference, ApplicationInstance> getAllApplicationInstances() { return serviceModelCache.get().getAllApplicationInstances(); diff --git a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/SuperModelListenerImpl.java b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/SuperModelListenerImpl.java index b2f3617131b..f509809c33d 100644 --- a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/SuperModelListenerImpl.java +++ b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/SuperModelListenerImpl.java @@ -8,7 +8,9 @@ import com.yahoo.config.model.api.SuperModelProvider; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.Zone; import com.yahoo.vespa.service.monitor.ServiceModel; +import com.yahoo.vespa.service.monitor.ServiceStatusProvider; +import java.util.List; import java.util.function.Supplier; import java.util.logging.Logger; @@ -16,6 +18,7 @@ public class SuperModelListenerImpl implements SuperModelListener, Supplier<Serv private static final Logger logger = Logger.getLogger(SuperModelListenerImpl.class.getName()); private final ServiceMonitorMetrics metrics; + private final DuperModel duperModel; private final ModelGenerator modelGenerator; private final Zone zone; @@ -27,10 +30,12 @@ public class SuperModelListenerImpl implements SuperModelListener, Supplier<Serv SuperModelListenerImpl(MonitorManager monitorManager, ServiceMonitorMetrics metrics, + DuperModel duperModel, ModelGenerator modelGenerator, Zone zone) { this.monitorManager = monitorManager; this.metrics = metrics; + this.duperModel = duperModel; this.modelGenerator = modelGenerator; this.zone = zone; } @@ -41,8 +46,7 @@ public class SuperModelListenerImpl implements SuperModelListener, Supplier<Serv // since applicationActivated()/applicationRemoved() may be called // asynchronously even before snapshot() returns. this.superModel = superModelProvider.snapshot(this); - superModel.getAllApplicationInfos().stream().forEach(application -> - monitorManager.applicationActivated(superModel, application)); + duperModel.getApplicationInfos(superModel).forEach(monitorManager::applicationActivated); } } @@ -50,7 +54,7 @@ public class SuperModelListenerImpl implements SuperModelListener, Supplier<Serv public void applicationActivated(SuperModel superModel, ApplicationInfo application) { synchronized (monitor) { this.superModel = superModel; - monitorManager.applicationActivated(superModel, application); + monitorManager.applicationActivated(application); } } @@ -58,7 +62,7 @@ public class SuperModelListenerImpl implements SuperModelListener, Supplier<Serv public void applicationRemoved(SuperModel superModel, ApplicationId id) { synchronized (monitor) { this.superModel = superModel; - monitorManager.applicationRemoved(superModel, id); + monitorManager.applicationRemoved(id); } } @@ -71,7 +75,9 @@ public class SuperModelListenerImpl implements SuperModelListener, Supplier<Serv dummy(measurement); // WARNING: The slobrok monitor manager may be out-of-sync with super model (no locking) - return modelGenerator.toServiceModel(superModel, zone, monitorManager); + List<ApplicationInfo> applicationInfos = duperModel.getApplicationInfos(superModel); + + return modelGenerator.toServiceModel(applicationInfos, zone, (ServiceStatusProvider) monitorManager); } } diff --git a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/UnionMonitorManager.java b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/UnionMonitorManager.java index 82d2043bd17..81cf6f2af5e 100644 --- a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/UnionMonitorManager.java +++ b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/UnionMonitorManager.java @@ -1,16 +1,12 @@ // Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.service.monitor.internal; -import com.yahoo.cloud.config.ConfigserverConfig; import com.yahoo.config.model.api.ApplicationInfo; -import com.yahoo.config.model.api.SuperModel; import com.yahoo.config.provision.ApplicationId; import com.yahoo.vespa.applicationmodel.ClusterId; import com.yahoo.vespa.applicationmodel.ConfigId; import com.yahoo.vespa.applicationmodel.ServiceStatus; import com.yahoo.vespa.applicationmodel.ServiceType; -import com.yahoo.vespa.service.monitor.application.ConfigServerApplication; -import com.yahoo.vespa.service.monitor.application.ZoneApplication; import com.yahoo.vespa.service.monitor.internal.health.HealthMonitorManager; import com.yahoo.vespa.service.monitor.internal.slobrok.SlobrokMonitorManagerImpl; @@ -20,14 +16,11 @@ import com.yahoo.vespa.service.monitor.internal.slobrok.SlobrokMonitorManagerImp public class UnionMonitorManager implements MonitorManager { private final SlobrokMonitorManagerImpl slobrokMonitorManager; private final HealthMonitorManager healthMonitorManager; - private final ConfigserverConfig configserverConfig; UnionMonitorManager(SlobrokMonitorManagerImpl slobrokMonitorManager, - HealthMonitorManager healthMonitorManager, - ConfigserverConfig configserverConfig) { + HealthMonitorManager healthMonitorManager) { this.slobrokMonitorManager = slobrokMonitorManager; this.healthMonitorManager = healthMonitorManager; - this.configserverConfig = configserverConfig; } @Override @@ -35,33 +28,25 @@ public class UnionMonitorManager implements MonitorManager { ClusterId clusterId, ServiceType serviceType, ConfigId configId) { - - if (applicationId.equals(ConfigServerApplication.CONFIG_SERVER_APPLICATION.getApplicationId())) { - // todo: use health - return ServiceStatus.NOT_CHECKED; + // Trust the new health monitoring status if it actually monitors the particular service. + ServiceStatus status = healthMonitorManager.getStatus(applicationId, clusterId, serviceType, configId); + if (status != ServiceStatus.NOT_CHECKED) { + return status; } - MonitorManager monitorManager = useHealth(applicationId, clusterId, serviceType) ? - healthMonitorManager : - slobrokMonitorManager; - - return monitorManager.getStatus(applicationId, clusterId, serviceType, configId); + // fallback is the older slobrok + return slobrokMonitorManager.getStatus(applicationId, clusterId, serviceType, configId); } @Override - public void applicationActivated(SuperModel superModel, ApplicationInfo application) { - slobrokMonitorManager.applicationActivated(superModel, application); - healthMonitorManager.applicationActivated(superModel, application); + public void applicationActivated(ApplicationInfo application) { + slobrokMonitorManager.applicationActivated(application); + healthMonitorManager.applicationActivated(application); } @Override - public void applicationRemoved(SuperModel superModel, ApplicationId id) { - slobrokMonitorManager.applicationRemoved(superModel, id); - healthMonitorManager.applicationRemoved(superModel, id); - } - - private boolean useHealth(ApplicationId applicationId, ClusterId clusterId, ServiceType serviceType) { - return !configserverConfig.nodeAdminInContainer() && - ZoneApplication.isNodeAdminService(applicationId, clusterId, serviceType); + public void applicationRemoved(ApplicationId id) { + slobrokMonitorManager.applicationRemoved(id); + healthMonitorManager.applicationRemoved(id); } } diff --git a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/health/ApplicationHealthMonitor.java b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/health/ApplicationHealthMonitor.java new file mode 100644 index 00000000000..bd2658db8aa --- /dev/null +++ b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/health/ApplicationHealthMonitor.java @@ -0,0 +1,102 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.service.monitor.internal.health; + +import com.yahoo.config.model.api.ApplicationInfo; +import com.yahoo.config.model.api.HostInfo; +import com.yahoo.config.model.api.PortInfo; +import com.yahoo.config.model.api.ServiceInfo; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.HostName; +import com.yahoo.vespa.applicationmodel.ClusterId; +import com.yahoo.vespa.applicationmodel.ConfigId; +import com.yahoo.vespa.applicationmodel.ServiceStatus; +import com.yahoo.vespa.applicationmodel.ServiceType; +import com.yahoo.vespa.service.monitor.ServiceStatusProvider; +import com.yahoo.vespa.service.monitor.application.ApplicationInstanceGenerator; +import com.yahoo.vespa.service.monitor.internal.ServiceId; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * Responsible for monitoring a whole application using /state/v1/health. + * + * @author hakon + */ +public class ApplicationHealthMonitor implements ServiceStatusProvider, AutoCloseable { + public static final String PORT_TAG_STATE = "STATE"; + public static final String PORT_TAG_HTTP = "HTTP"; + /** Port tags implying /state/v1/health is served */ + public static final List<String> PORT_TAGS_HEALTH = + Collections.unmodifiableList(Arrays.asList(PORT_TAG_HTTP, PORT_TAG_STATE)); + + private final Map<ServiceId, HealthMonitor> healthMonitors; + + public static ApplicationHealthMonitor startMonitoring(ApplicationInfo application) { + return new ApplicationHealthMonitor(makeHealthMonitors(application)); + } + + private ApplicationHealthMonitor(Map<ServiceId, HealthMonitor> healthMonitors) { + this.healthMonitors = healthMonitors; + } + + @Override + public ServiceStatus getStatus(ApplicationId applicationId, + ClusterId clusterId, + ServiceType serviceType, + ConfigId configId) { + ServiceId serviceId = new ServiceId(applicationId, clusterId, serviceType, configId); + HealthMonitor monitor = healthMonitors.get(serviceId); + if (monitor == null) { + return ServiceStatus.NOT_CHECKED; + } + + return monitor.getStatus(); + } + + @Override + public void close() { + healthMonitors.values().forEach(HealthMonitor::close); + healthMonitors.clear(); + } + + private static Map<ServiceId, HealthMonitor> makeHealthMonitors(ApplicationInfo application) { + Map<ServiceId, HealthMonitor> healthMonitors = new HashMap<>(); + for (HostInfo hostInfo : application.getModel().getHosts()) { + for (ServiceInfo serviceInfo : hostInfo.getServices()) { + for (PortInfo portInfo : serviceInfo.getPorts()) { + maybeCreateHealthMonitor( + application, + hostInfo, + serviceInfo, + portInfo) + .ifPresent(healthMonitor -> healthMonitors.put( + ApplicationInstanceGenerator.getServiceId(application, serviceInfo), + healthMonitor)); + } + } + } + return healthMonitors; + } + + private static Optional<HealthMonitor> maybeCreateHealthMonitor( + ApplicationInfo applicationInfo, + HostInfo hostInfo, + ServiceInfo serviceInfo, + PortInfo portInfo) { + if (portInfo.getTags().containsAll(PORT_TAGS_HEALTH)) { + HostName hostname = HostName.from(hostInfo.getHostname()); + HealthEndpoint endpoint = HealthEndpoint.forHttp(hostname, portInfo.getPort()); + // todo: make HealthMonitor + // HealthMonitor healthMonitor = new HealthMonitor(endpoint); + // healthMonitor.startMonitoring(); + return Optional.empty(); + } + + return Optional.empty(); + } +} diff --git a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/health/HealthClient.java b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/health/HealthClient.java new file mode 100644 index 00000000000..43a02a385be --- /dev/null +++ b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/health/HealthClient.java @@ -0,0 +1,139 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.service.monitor.internal.health; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.yahoo.vespa.athenz.api.AthenzService; +import com.yahoo.vespa.athenz.identity.ServiceIdentityProvider; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.config.Registry; +import org.apache.http.config.RegistryBuilder; +import org.apache.http.conn.ConnectionKeepAliveStrategy; +import org.apache.http.conn.HttpClientConnectionManager; +import org.apache.http.conn.socket.ConnectionSocketFactory; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.DefaultConnectionKeepAliveStrategy; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.impl.conn.BasicHttpClientConnectionManager; +import org.apache.http.protocol.HttpContext; +import org.apache.http.util.EntityUtils; + +import javax.net.ssl.SSLContext; + +/** + * @author hakon + */ +public class HealthClient implements AutoCloseable, ServiceIdentityProvider.Listener { + private static final ObjectMapper mapper = new ObjectMapper(); + private static final long MAX_CONTENT_LENGTH = 1L << 20; // 1 MB + private static final int DEFAULT_TIMEOUT_MILLIS = 1_000; + + private static final ConnectionKeepAliveStrategy KEEP_ALIVE_STRATEGY = + new DefaultConnectionKeepAliveStrategy() { + @Override + public long getKeepAliveDuration(HttpResponse response, HttpContext context) { + long keepAlive = super.getKeepAliveDuration(response, context); + if (keepAlive == -1) { + // Keep connections alive 60 seconds if a keep-alive value + // has not be explicitly set by the server + keepAlive = 60000; + } + return keepAlive; + } + }; + + private final HealthEndpoint endpoint; + + private volatile CloseableHttpClient httpClient; + + public HealthClient(HealthEndpoint endpoint) { + this.endpoint = endpoint; + } + + public void start() { + endpoint.getServiceIdentityProvider().ifPresent(provider -> { + onCredentialsUpdate(provider.getIdentitySslContext(), null); + provider.addIdentityListener(this); + }); + } + + @Override + public void onCredentialsUpdate(SSLContext sslContext, AthenzService ignored) { + SSLConnectionSocketFactory socketFactory = + new SSLConnectionSocketFactory(sslContext, endpoint.getHostnameVerifier().orElse(null)); + + Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create() + .register("https", socketFactory) + .build(); + + HttpClientConnectionManager connectionManager = new BasicHttpClientConnectionManager(registry); + + RequestConfig requestConfig = RequestConfig.custom() + .setConnectTimeout(DEFAULT_TIMEOUT_MILLIS) // establishment of connection + .setConnectionRequestTimeout(DEFAULT_TIMEOUT_MILLIS) // connection from connection manager + .setSocketTimeout(DEFAULT_TIMEOUT_MILLIS) // waiting for data + .build(); + + this.httpClient = HttpClients.custom() + .setKeepAliveStrategy(KEEP_ALIVE_STRATEGY) + .setConnectionManager(connectionManager) + .disableAutomaticRetries() + .setDefaultRequestConfig(requestConfig) + .build(); + } + + public HealthInfo getHealthInfo() { + try { + return probeHealth(); + } catch (Exception e) { + return HealthInfo.fromException(e); + } + } + + @Override + public void close() { + endpoint.getServiceIdentityProvider().ifPresent(provider -> provider.removeIdentityListener(this)); + + try { + httpClient.close(); + } catch (Exception e) { + // ignore + } + httpClient = null; + } + + private HealthInfo probeHealth() throws Exception { + HttpGet httpget = new HttpGet(endpoint.getStateV1HealthUrl().toString()); + CloseableHttpResponse httpResponse; + + CloseableHttpClient httpClient = this.httpClient; + if (httpClient == null) { + throw new IllegalStateException("HTTP client has closed"); + } + + httpResponse = httpClient.execute(httpget); + + int httpStatusCode = httpResponse.getStatusLine().getStatusCode(); + if (httpStatusCode < 200 || httpStatusCode >= 300) { + return HealthInfo.fromBadHttpStatusCode(httpStatusCode); + } + + HttpEntity bodyEntity = httpResponse.getEntity(); + long contentLength = bodyEntity.getContentLength(); + if (contentLength > MAX_CONTENT_LENGTH) { + throw new IllegalArgumentException("Content too long: " + contentLength + " bytes"); + } + String body = EntityUtils.toString(bodyEntity); + HealthResponse healthResponse = mapper.readValue(body, HealthResponse.class); + + if (healthResponse.status == null || healthResponse.status.code == null) { + return HealthInfo.fromHealthStatusCode(HealthResponse.Status.DEFAULT_STATUS); + } else { + return HealthInfo.fromHealthStatusCode(healthResponse.status.code); + } + } +} diff --git a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/health/HealthEndpoint.java b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/health/HealthEndpoint.java new file mode 100644 index 00000000000..e9d17a9ab70 --- /dev/null +++ b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/health/HealthEndpoint.java @@ -0,0 +1,57 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.service.monitor.internal.health; + +import com.yahoo.config.provision.HostName; +import com.yahoo.vespa.athenz.api.AthenzIdentity; +import com.yahoo.vespa.athenz.identity.ServiceIdentityProvider; +import com.yahoo.vespa.athenz.tls.AthenzIdentityVerifier; + +import javax.net.ssl.HostnameVerifier; +import java.net.URL; +import java.util.Collections; +import java.util.Optional; + +import static com.yahoo.yolean.Exceptions.uncheck; + +/** + * @author hakon + */ +class HealthEndpoint { + private final URL url; + private final Optional<HostnameVerifier> hostnameVerifier; + private final Optional<ServiceIdentityProvider> serviceIdentityProvider; + + static HealthEndpoint forHttp(HostName hostname, int port) { + URL url = uncheck(() -> new URL("http", hostname.value(), port, "/state/v1/health")); + return new HealthEndpoint(url, Optional.empty(), Optional.empty()); + } + + static HealthEndpoint forHttps(HostName hostname, + int port, + ServiceIdentityProvider serviceIdentityProvider, + AthenzIdentity remoteIdentity) { + URL url = uncheck(() -> new URL("https", hostname.value(), port, "/state/v1/health")); + HostnameVerifier peerVerifier = new AthenzIdentityVerifier(Collections.singleton(remoteIdentity)); + return new HealthEndpoint(url, Optional.of(serviceIdentityProvider), Optional.of(peerVerifier)); + } + + private HealthEndpoint(URL url, + Optional<ServiceIdentityProvider> serviceIdentityProvider, + Optional<HostnameVerifier> hostnameVerifier) { + this.url = url; + this.serviceIdentityProvider = serviceIdentityProvider; + this.hostnameVerifier = hostnameVerifier; + } + + public URL getStateV1HealthUrl() { + return url; + } + + public Optional<ServiceIdentityProvider> getServiceIdentityProvider() { + return serviceIdentityProvider; + } + + public Optional<HostnameVerifier> getHostnameVerifier() { + return hostnameVerifier; + } +} diff --git a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/health/HealthInfo.java b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/health/HealthInfo.java new file mode 100644 index 00000000000..a3fe3cb3106 --- /dev/null +++ b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/health/HealthInfo.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.service.monitor.internal.health; + +import com.yahoo.vespa.applicationmodel.ServiceStatus; +import com.yahoo.yolean.Exceptions; + +import java.time.Instant; +import java.util.Optional; +import java.util.OptionalInt; + +/** + * The result of a health lookup. + * + * @author hakon + */ +public class HealthInfo { + public static final String UP_STATUS_CODE = "up"; + + private final Optional<Exception> exception; + private final OptionalInt httpStatusCode; + private final Optional<String> healthStatusCode; + private final Instant time; + + static HealthInfo fromException(Exception exception) { + return new HealthInfo(Optional.of(exception), OptionalInt.empty(), Optional.empty()); + } + + static HealthInfo fromBadHttpStatusCode(int httpStatusCode) { + return new HealthInfo(Optional.empty(), OptionalInt.of(httpStatusCode), Optional.empty()); + } + + static HealthInfo fromHealthStatusCode(String healthStatusCode) { + return new HealthInfo(Optional.empty(), OptionalInt.empty(), Optional.of(healthStatusCode)); + } + + static HealthInfo empty() { + return new HealthInfo(Optional.empty(), OptionalInt.empty(), Optional.empty()); + } + + private HealthInfo(Optional<Exception> exception, + OptionalInt httpStatusCode, + Optional<String> healthStatusCode) { + this.exception = exception; + this.httpStatusCode = httpStatusCode; + this.healthStatusCode = healthStatusCode; + this.time = Instant.now(); + } + + public boolean isHealthy() { + return healthStatusCode.map(UP_STATUS_CODE::equals).orElse(false); + } + + public ServiceStatus toSerivceStatus() { + return isHealthy() ? ServiceStatus.UP : ServiceStatus.DOWN; + } + + public Instant time() { + return time; + } + + @Override + public String toString() { + if (isHealthy()) { + return UP_STATUS_CODE; + } else if (healthStatusCode.isPresent()) { + return "Bad health status code '" + healthStatusCode.get() + "'"; + } else if (exception.isPresent()) { + return Exceptions.toMessageString(exception.get()); + } else if (httpStatusCode.isPresent()) { + return "Bad HTTP response status code " + httpStatusCode.getAsInt(); + } else { + return "No health info available"; + } + } +} diff --git a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/health/HealthMonitor.java b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/health/HealthMonitor.java new file mode 100644 index 00000000000..fd809b32918 --- /dev/null +++ b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/health/HealthMonitor.java @@ -0,0 +1,73 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.service.monitor.internal.health; + +import com.yahoo.log.LogLevel; +import com.yahoo.vespa.applicationmodel.ServiceStatus; + +import java.time.Duration; +import java.util.Random; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; + +/** + * Used to monitor the health of a single URL endpoint. + * + * @author hakon + */ +public class HealthMonitor implements AutoCloseable { + private static final Logger logger = Logger.getLogger(HealthMonitor.class.getName()); + private static final Duration DELAY = Duration.ofSeconds(20); + // About 'static': Javadoc says "Instances of java.util.Random are threadsafe." + private static final Random random = new Random(); + + private final ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1); + private final HealthClient healthClient; + + private volatile HealthInfo lastHealthInfo = HealthInfo.empty(); + + public HealthMonitor(HealthEndpoint stateV1HealthEndpoint) { + this.healthClient = new HealthClient(stateV1HealthEndpoint); + } + + /** For testing. */ + HealthMonitor(HealthClient healthClient) { + this.healthClient = healthClient; + } + + public void startMonitoring() { + healthClient.start(); + executor.scheduleWithFixedDelay( + this::updateSynchronously, + initialDelayInSeconds(DELAY.getSeconds()), + DELAY.getSeconds(), + TimeUnit.SECONDS); + } + + public ServiceStatus getStatus() { + // todo: return lastHealthInfo.toServiceStatus(); + return ServiceStatus.NOT_CHECKED; + } + + @Override + public void close() { + executor.shutdown(); + + try { + executor.awaitTermination(2, TimeUnit.SECONDS); + } catch (InterruptedException e) { + logger.log(LogLevel.INFO, "Interrupted while waiting for health monitor termination: " + + e.getMessage()); + } + + healthClient.close(); + } + + private long initialDelayInSeconds(long maxInitialDelayInSeconds) { + return random.nextLong() % maxInitialDelayInSeconds; + } + + private void updateSynchronously() { + lastHealthInfo = healthClient.getHealthInfo(); + } +} diff --git a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/health/HealthMonitorManager.java b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/health/HealthMonitorManager.java index 5a4b7251ae2..473ef5e3a94 100644 --- a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/health/HealthMonitorManager.java +++ b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/health/HealthMonitorManager.java @@ -2,8 +2,8 @@ package com.yahoo.vespa.service.monitor.internal.health; import com.google.inject.Inject; +import com.yahoo.cloud.config.ConfigserverConfig; import com.yahoo.config.model.api.ApplicationInfo; -import com.yahoo.config.model.api.SuperModel; import com.yahoo.config.provision.ApplicationId; import com.yahoo.vespa.applicationmodel.ClusterId; import com.yahoo.vespa.applicationmodel.ConfigId; @@ -12,19 +12,38 @@ import com.yahoo.vespa.applicationmodel.ServiceType; import com.yahoo.vespa.service.monitor.application.ZoneApplication; import com.yahoo.vespa.service.monitor.internal.MonitorManager; +import java.util.HashMap; +import java.util.Map; + /** * @author hakon */ public class HealthMonitorManager implements MonitorManager { + private final Map<ApplicationId, ApplicationHealthMonitor> healthMonitors = new HashMap<>(); + private final ConfigserverConfig configserverConfig; + @Inject - public HealthMonitorManager() {} + public HealthMonitorManager(ConfigserverConfig configserverConfig) { + this.configserverConfig = configserverConfig; + } @Override - public void applicationActivated(SuperModel superModel, ApplicationInfo application) { + public void applicationActivated(ApplicationInfo application) { + if (applicationMonitored(application.getApplicationId())) { + ApplicationHealthMonitor monitor = + ApplicationHealthMonitor.startMonitoring(application); + healthMonitors.put(application.getApplicationId(), monitor); + } } @Override - public void applicationRemoved(SuperModel superModel, ApplicationId id) { + public void applicationRemoved(ApplicationId id) { + if (applicationMonitored(id)) { + ApplicationHealthMonitor monitor = healthMonitors.remove(id); + if (monitor != null) { + monitor.close(); + } + } } @Override @@ -32,13 +51,18 @@ public class HealthMonitorManager implements MonitorManager { ClusterId clusterId, ServiceType serviceType, ConfigId configId) { - // TODO: Do proper health check - if (ZoneApplication.isNodeAdminService(applicationId, clusterId, serviceType)) { + if (!configserverConfig.nodeAdminInContainer() && + ZoneApplication.isNodeAdminService(applicationId, clusterId, serviceType)) { + // If node admin doesn't run in a JDisc container, it must be monitored with health. + // TODO: Do proper health check return ServiceStatus.UP; } - throw new IllegalArgumentException("Health monitoring not implemented for application " + - applicationId.toShortString() + ", cluster " + clusterId.s() + ", serviceType " + - serviceType); + return ServiceStatus.NOT_CHECKED; + } + + private boolean applicationMonitored(ApplicationId id) { + // todo: health-check config server + return false; } } diff --git a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/health/HealthResponse.java b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/health/HealthResponse.java new file mode 100644 index 00000000000..574523ad564 --- /dev/null +++ b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/health/HealthResponse.java @@ -0,0 +1,35 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.service.monitor.internal.health; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.yahoo.text.JSON; + +/** + * Response entity from /state/v1/health + * + * @author hakon + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class HealthResponse { + @JsonProperty("status") + public Status status = new Status(); + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Status { + public static final String DEFAULT_STATUS = "down"; + + @JsonProperty("code") + public String code = DEFAULT_STATUS; + + @Override + public String toString() { + return "{ \"code\": \"" + JSON.escape(code) + "\" }"; + } + } + + @Override + public String toString() { + return "{ \"status\": " + status.toString() + " }"; + } +} diff --git a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/slobrok/SlobrokMonitorManagerImpl.java b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/slobrok/SlobrokMonitorManagerImpl.java index aaaab22e742..68958c94dfd 100644 --- a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/slobrok/SlobrokMonitorManagerImpl.java +++ b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/slobrok/SlobrokMonitorManagerImpl.java @@ -3,8 +3,6 @@ package com.yahoo.vespa.service.monitor.internal.slobrok; import com.google.inject.Inject; import com.yahoo.config.model.api.ApplicationInfo; -import com.yahoo.config.model.api.SuperModel; -import com.yahoo.config.model.api.SuperModelListener; import com.yahoo.config.provision.ApplicationId; import com.yahoo.jrt.slobrok.api.Mirror; import com.yahoo.log.LogLevel; @@ -13,6 +11,7 @@ import com.yahoo.vespa.applicationmodel.ConfigId; import com.yahoo.vespa.applicationmodel.ServiceStatus; import com.yahoo.vespa.applicationmodel.ServiceType; import com.yahoo.vespa.service.monitor.SlobrokApi; +import com.yahoo.vespa.service.monitor.application.ConfigServerApplication; import com.yahoo.vespa.service.monitor.internal.MonitorManager; import java.util.HashMap; @@ -21,7 +20,7 @@ import java.util.Optional; import java.util.function.Supplier; import java.util.logging.Logger; -public class SlobrokMonitorManagerImpl implements SuperModelListener, SlobrokApi, MonitorManager { +public class SlobrokMonitorManagerImpl implements SlobrokApi, MonitorManager { private static final Logger logger = Logger.getLogger(SlobrokMonitorManagerImpl.class.getName()); @@ -40,7 +39,11 @@ public class SlobrokMonitorManagerImpl implements SuperModelListener, SlobrokApi } @Override - public void applicationActivated(SuperModel superModel, ApplicationInfo application) { + public void applicationActivated(ApplicationInfo application) { + if (!applicationMonitoredWithSlobrok(application.getApplicationId())) { + return; + } + synchronized (monitor) { SlobrokMonitor slobrokMonitor = slobrokMonitors.computeIfAbsent( application.getApplicationId(), @@ -50,7 +53,11 @@ public class SlobrokMonitorManagerImpl implements SuperModelListener, SlobrokApi } @Override - public void applicationRemoved(SuperModel superModel, ApplicationId id) { + public void applicationRemoved(ApplicationId id) { + if (!applicationMonitoredWithSlobrok(id)) { + return; + } + synchronized (monitor) { SlobrokMonitor slobrokMonitor = slobrokMonitors.remove(id); if (slobrokMonitor == null) { @@ -79,6 +86,10 @@ public class SlobrokMonitorManagerImpl implements SuperModelListener, SlobrokApi ClusterId clusterId, ServiceType serviceType, ConfigId configId) { + if (!applicationMonitoredWithSlobrok(applicationId)) { + return ServiceStatus.NOT_CHECKED; + } + Optional<String> slobrokServiceName = findSlobrokServiceName(serviceType, configId); if (slobrokServiceName.isPresent()) { synchronized (monitor) { @@ -95,6 +106,14 @@ public class SlobrokMonitorManagerImpl implements SuperModelListener, SlobrokApi } } + private boolean applicationMonitoredWithSlobrok(ApplicationId applicationId) { + if (applicationId.equals(ConfigServerApplication.CONFIG_SERVER_APPLICATION.getApplicationId())) { + return false; + } + + return true; + } + /** * Get the Slobrok service name of the service, or empty if the service * is not registered with Slobrok. diff --git a/service-monitor/src/test/java/com/yahoo/vespa/service/monitor/application/ConfigServerAppGeneratorTest.java b/service-monitor/src/test/java/com/yahoo/vespa/service/monitor/application/ApplicationInstanceGeneratorTest.java index 58f99786017..899cc59bb34 100644 --- a/service-monitor/src/test/java/com/yahoo/vespa/service/monitor/application/ConfigServerAppGeneratorTest.java +++ b/service-monitor/src/test/java/com/yahoo/vespa/service/monitor/application/ApplicationInstanceGeneratorTest.java @@ -1,22 +1,27 @@ // Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.service.monitor.application; +import com.yahoo.cloud.config.ConfigserverConfig; +import com.yahoo.config.model.api.ApplicationInfo; +import com.yahoo.config.provision.Zone; import com.yahoo.vespa.applicationmodel.ApplicationInstance; import com.yahoo.vespa.applicationmodel.ServiceStatus; import com.yahoo.vespa.service.monitor.ServiceStatusProvider; +import com.yahoo.vespa.service.monitor.internal.ConfigserverUtil; import org.junit.Test; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; +import static com.yahoo.vespa.service.monitor.application.ConfigServerApplication.CONFIG_SERVER_APPLICATION; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -public class ConfigServerAppGeneratorTest { +public class ApplicationInstanceGeneratorTest { private static final String configServer1 = "cfg1.yahoo.com"; private static final String configServer2 = "cfg2.yahoo.com"; private static final String configServer3 = "cfg3.yahoo.com"; @@ -28,9 +33,17 @@ public class ConfigServerAppGeneratorTest { private final ServiceStatusProvider statusProvider = mock(ServiceStatusProvider.class); @Test - public void toApplicationInstance() throws Exception { + public void toApplicationInstance() { when(statusProvider.getStatus(any(), any(), any(), any())).thenReturn(ServiceStatus.NOT_CHECKED); - ApplicationInstance applicationInstance = new ConfigServerAppGenerator(configServerList) + ConfigserverConfig config = ConfigserverUtil.create( + true, + true, + configServer1, + configServer2, + configServer3); + Zone zone = mock(Zone.class); + ApplicationInfo configServer = CONFIG_SERVER_APPLICATION.makeApplicationInfo(config); + ApplicationInstance applicationInstance = new ApplicationInstanceGenerator(configServer, zone) .makeApplicationInstance(statusProvider); assertEquals( diff --git a/service-monitor/src/test/java/com/yahoo/vespa/service/monitor/internal/ConfigserverUtil.java b/service-monitor/src/test/java/com/yahoo/vespa/service/monitor/internal/ConfigserverUtil.java new file mode 100644 index 00000000000..85df02949a6 --- /dev/null +++ b/service-monitor/src/test/java/com/yahoo/vespa/service/monitor/internal/ConfigserverUtil.java @@ -0,0 +1,52 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.service.monitor.internal; + +import com.yahoo.cloud.config.ConfigserverConfig; +import com.yahoo.config.model.api.ApplicationInfo; +import com.yahoo.vespa.service.monitor.application.ConfigServerApplication; + +/** + * @author hakon + */ +public class ConfigserverUtil { + /** Create a ConfigserverConfig with the given settings. */ + public static ConfigserverConfig create( + boolean nodeAdminInContainer, + boolean multitenant, + String configServerHostname1, + String configServerHostname2, + String configServerHostname3) { + return new ConfigserverConfig( + new ConfigserverConfig.Builder() + .nodeAdminInContainer(nodeAdminInContainer) + .multitenant(multitenant) + .zookeeperserver(new ConfigserverConfig.Zookeeperserver.Builder().hostname(configServerHostname1).port(1)) + .zookeeperserver(new ConfigserverConfig.Zookeeperserver.Builder().hostname(configServerHostname2).port(2)) + .zookeeperserver(new ConfigserverConfig.Zookeeperserver.Builder().hostname(configServerHostname3).port(3))); + } + + public static ConfigserverConfig createExampleConfigserverConfig() { + return create(true, true, "cfg1", "cfg2", "cfg3"); + } + + public static ConfigserverConfig createExampleConfigserverConfig(boolean nodeAdminInContainer, + boolean multitenant) { + return create(nodeAdminInContainer, multitenant, "cfg1", "cfg2", "cfg3"); + } + + public static ApplicationInfo makeConfigServerApplicationInfo( + String configServerHostname1, + String configServerHostname2, + String configServerHostname3) { + return ConfigServerApplication.CONFIG_SERVER_APPLICATION.makeApplicationInfo(create( + true, + true, + configServerHostname1, + configServerHostname2, + configServerHostname3)); + } + + public static ApplicationInfo makeExampleConfigServer() { + return makeConfigServerApplicationInfo("cfg1", "cfg2", "cfg3"); + } +} diff --git a/service-monitor/src/test/java/com/yahoo/vespa/service/monitor/internal/DuperModelTest.java b/service-monitor/src/test/java/com/yahoo/vespa/service/monitor/internal/DuperModelTest.java new file mode 100644 index 00000000000..c9d19d0ccd9 --- /dev/null +++ b/service-monitor/src/test/java/com/yahoo/vespa/service/monitor/internal/DuperModelTest.java @@ -0,0 +1,53 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.service.monitor.internal; + +import com.yahoo.cloud.config.ConfigserverConfig; +import com.yahoo.config.model.api.ApplicationInfo; +import com.yahoo.config.model.api.SuperModel; +import com.yahoo.vespa.applicationmodel.ServiceStatus; +import com.yahoo.vespa.service.monitor.ServiceStatusProvider; +import com.yahoo.vespa.service.monitor.application.ConfigServerApplication; +import org.junit.Test; + +import java.util.Collections; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * @author hakon + */ +public class DuperModelTest { + private final ServiceStatusProvider statusProvider = mock(ServiceStatusProvider.class); + + @Test + public void toApplicationInstance() { + when(statusProvider.getStatus(any(), any(), any(), any())).thenReturn(ServiceStatus.NOT_CHECKED); + ConfigserverConfig config = ConfigserverUtil.createExampleConfigserverConfig(); + DuperModel duperModel = new DuperModel(config); + SuperModel superModel = mock(SuperModel.class); + ApplicationInfo superModelApplicationInfo = mock(ApplicationInfo.class); + when(superModel.getAllApplicationInfos()).thenReturn(Collections.singletonList(superModelApplicationInfo)); + List<ApplicationInfo> applicationInfos = duperModel.getApplicationInfos(superModel); + assertEquals(2, applicationInfos.size()); + assertEquals(ConfigServerApplication.CONFIG_SERVER_APPLICATION.getApplicationId(), applicationInfos.get(0).getApplicationId()); + assertSame(superModelApplicationInfo, applicationInfos.get(1)); + } + + @Test + public void toApplicationInstanceInSingleTenantMode() { + when(statusProvider.getStatus(any(), any(), any(), any())).thenReturn(ServiceStatus.NOT_CHECKED); + ConfigserverConfig config = ConfigserverUtil.createExampleConfigserverConfig(true, false); + DuperModel duperModel = new DuperModel(config); + SuperModel superModel = mock(SuperModel.class); + ApplicationInfo superModelApplicationInfo = mock(ApplicationInfo.class); + when(superModel.getAllApplicationInfos()).thenReturn(Collections.singletonList(superModelApplicationInfo)); + List<ApplicationInfo> applicationInfos = duperModel.getApplicationInfos(superModel); + assertEquals(1, applicationInfos.size()); + assertSame(superModelApplicationInfo, applicationInfos.get(0)); + } +} diff --git a/service-monitor/src/test/java/com/yahoo/vespa/service/monitor/internal/ModelGeneratorTest.java b/service-monitor/src/test/java/com/yahoo/vespa/service/monitor/internal/ModelGeneratorTest.java index a21691ee4d0..5a57451a298 100644 --- a/service-monitor/src/test/java/com/yahoo/vespa/service/monitor/internal/ModelGeneratorTest.java +++ b/service-monitor/src/test/java/com/yahoo/vespa/service/monitor/internal/ModelGeneratorTest.java @@ -1,6 +1,7 @@ // Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.service.monitor.internal; +import com.yahoo.cloud.config.ConfigserverConfig; import com.yahoo.config.model.api.SuperModel; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.RegionName; @@ -15,13 +16,9 @@ import com.yahoo.vespa.service.monitor.application.ConfigServerApplication; import com.yahoo.vespa.service.monitor.internal.slobrok.SlobrokMonitorManagerImpl; import org.junit.Test; -import java.util.Collections; import java.util.Iterator; -import java.util.List; import java.util.Map; import java.util.Set; -import java.util.stream.Collectors; -import java.util.stream.Stream; import static org.junit.Assert.assertEquals; import static org.mockito.Matchers.any; @@ -35,13 +32,12 @@ public class ModelGeneratorTest { private final int PORT = 2; @Test - public void toApplicationModelWithConfigServerApplication() throws Exception { - SuperModel superModel = - ExampleModel.createExampleSuperModelWithOneRpcPort(HOSTNAME, PORT); + public void toApplicationModel() throws Exception { + SuperModel superModel = ExampleModel.createExampleSuperModelWithOneRpcPort(HOSTNAME, PORT); - List<String> configServerHosts = Stream.of("cfg1", "cfg2", "cfg3") - .collect(Collectors.toList()); - ModelGenerator modelGenerator = new ModelGenerator(configServerHosts); + ConfigserverConfig config = ConfigserverUtil.createExampleConfigserverConfig(); + DuperModel duperModel = new DuperModel(config); + ModelGenerator modelGenerator = new ModelGenerator(); Zone zone = new Zone(Environment.from(ENVIRONMENT), RegionName.from(REGION)); @@ -51,7 +47,7 @@ public class ModelGeneratorTest { ServiceModel serviceModel = modelGenerator.toServiceModel( - superModel, + duperModel.getApplicationInfos(superModel), zone, slobrokMonitorManager); @@ -78,32 +74,6 @@ public class ModelGeneratorTest { } } - @Test - public void toApplicationModel() throws Exception { - SuperModel superModel = - ExampleModel.createExampleSuperModelWithOneRpcPort(HOSTNAME, PORT); - ModelGenerator modelGenerator = new ModelGenerator(Collections.emptyList()); - - Zone zone = new Zone(Environment.from(ENVIRONMENT), RegionName.from(REGION)); - - SlobrokMonitorManagerImpl slobrokMonitorManager = mock(SlobrokMonitorManagerImpl.class); - when(slobrokMonitorManager.getStatus(any(), any(), any(), any())) - .thenReturn(ServiceStatus.UP); - - ServiceModel serviceModel = - modelGenerator.toServiceModel( - superModel, - zone, - slobrokMonitorManager); - - Map<ApplicationInstanceReference, - ApplicationInstance> applicationInstances = - serviceModel.getAllApplicationInstances(); - - assertEquals(1, applicationInstances.size()); - verifyOtherApplication(applicationInstances.values().iterator().next()); - } - private void verifyOtherApplication(ApplicationInstance applicationInstance) { assertEquals(String.format("%s:%s:%s:%s:%s", ExampleModel.TENANT, diff --git a/service-monitor/src/test/java/com/yahoo/vespa/service/monitor/internal/SuperModelListenerImplTest.java b/service-monitor/src/test/java/com/yahoo/vespa/service/monitor/internal/SuperModelListenerImplTest.java index 83bad0ddb2a..eb6d6d583f7 100644 --- a/service-monitor/src/test/java/com/yahoo/vespa/service/monitor/internal/SuperModelListenerImplTest.java +++ b/service-monitor/src/test/java/com/yahoo/vespa/service/monitor/internal/SuperModelListenerImplTest.java @@ -14,6 +14,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -22,11 +23,13 @@ public class SuperModelListenerImplTest { public void sanityCheck() { SlobrokMonitorManagerImpl slobrokMonitorManager = mock(SlobrokMonitorManagerImpl.class); ServiceMonitorMetrics metrics = mock(ServiceMonitorMetrics.class); + DuperModel duperModel = mock(DuperModel.class); ModelGenerator modelGenerator = mock(ModelGenerator.class); Zone zone = mock(Zone.class); SuperModelListenerImpl listener = new SuperModelListenerImpl( slobrokMonitorManager, metrics, + duperModel, modelGenerator, zone); @@ -38,13 +41,15 @@ public class SuperModelListenerImplTest { ApplicationInfo application2 = mock(ApplicationInfo.class); List<ApplicationInfo> applications = Stream.of(application1, application2) .collect(Collectors.toList()); - when(superModel.getAllApplicationInfos()).thenReturn(applications); + when(duperModel.getApplicationInfos(superModel)).thenReturn(applications); listener.start(superModelProvider); - verify(slobrokMonitorManager).applicationActivated(superModel, application1); - verify(slobrokMonitorManager).applicationActivated(superModel, application2); + verify(duperModel, times(1)).getApplicationInfos(superModel); + verify(slobrokMonitorManager).applicationActivated(application1); + verify(slobrokMonitorManager).applicationActivated(application2); ServiceModel serviceModel = listener.get(); - verify(modelGenerator).toServiceModel(superModel, zone, slobrokMonitorManager); + verify(duperModel, times(2)).getApplicationInfos(superModel); + verify(modelGenerator).toServiceModel(applications, zone, slobrokMonitorManager); } }
\ No newline at end of file diff --git a/service-monitor/src/test/java/com/yahoo/vespa/service/monitor/internal/UnionMonitorManagerTest.java b/service-monitor/src/test/java/com/yahoo/vespa/service/monitor/internal/UnionMonitorManagerTest.java index b7c3ed8e1e1..79916e43712 100644 --- a/service-monitor/src/test/java/com/yahoo/vespa/service/monitor/internal/UnionMonitorManagerTest.java +++ b/service-monitor/src/test/java/com/yahoo/vespa/service/monitor/internal/UnionMonitorManagerTest.java @@ -1,95 +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.service.monitor.internal; -import com.yahoo.cloud.config.ConfigserverConfig; -import com.yahoo.config.provision.ApplicationId; -import com.yahoo.vespa.applicationmodel.ClusterId; import com.yahoo.vespa.applicationmodel.ConfigId; -import com.yahoo.vespa.applicationmodel.ServiceType; +import com.yahoo.vespa.applicationmodel.ServiceStatus; import com.yahoo.vespa.service.monitor.internal.health.HealthMonitorManager; import com.yahoo.vespa.service.monitor.internal.slobrok.SlobrokMonitorManagerImpl; import org.junit.Test; import static com.yahoo.vespa.applicationmodel.ClusterId.NODE_ADMIN; +import static com.yahoo.vespa.applicationmodel.ServiceStatus.*; +import static com.yahoo.vespa.applicationmodel.ServiceStatus.NOT_CHECKED; +import static com.yahoo.vespa.applicationmodel.ServiceStatus.UP; import static com.yahoo.vespa.applicationmodel.ServiceType.CONTAINER; import static com.yahoo.vespa.service.monitor.application.ZoneApplication.ZONE_APPLICATION_ID; +import static org.junit.Assert.assertSame; import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; public class UnionMonitorManagerTest { - @Test - public void nodeAdminInContainer() { - testWith( - true, - ZONE_APPLICATION_ID, - NODE_ADMIN, - CONTAINER, - 1, - 0); - } - - @Test - public void nodeAdminOutsideContainer() { - boolean inContainer = false; - - // When nodeAdminInContainer is set, then only the node admin cluster should use health - testWith( - inContainer, - ZONE_APPLICATION_ID, - NODE_ADMIN, - CONTAINER, - 0, - 1); - - testWith( - inContainer, - ApplicationId.fromSerializedForm("a:b:default"), - NODE_ADMIN, - CONTAINER, - 1, - 0); + private final SlobrokMonitorManagerImpl slobrokMonitorManager = mock(SlobrokMonitorManagerImpl.class); + private final HealthMonitorManager healthMonitorManager = mock(HealthMonitorManager.class); - testWith( - inContainer, - ZONE_APPLICATION_ID, - new ClusterId("foo"), - CONTAINER, - 1, - 0); + private final UnionMonitorManager manager = new UnionMonitorManager( + slobrokMonitorManager, + healthMonitorManager); - testWith( - inContainer, - ZONE_APPLICATION_ID, - NODE_ADMIN, - new ServiceType("foo"), - 1, - 0); + @Test + public void verifyHealthTakesPriority() { + testWith(UP, DOWN, UP); + testWith(NOT_CHECKED, DOWN, DOWN); + testWith(NOT_CHECKED, NOT_CHECKED, NOT_CHECKED); } - private void testWith(boolean nodeAdminInContainer, - ApplicationId applicationId, - ClusterId clusterId, - ServiceType serviceType, - int expectedSlobrokCalls, - int expectedHealthCalls) { - SlobrokMonitorManagerImpl slobrokMonitorManager = mock(SlobrokMonitorManagerImpl.class); - HealthMonitorManager healthMonitorManager = mock(HealthMonitorManager.class); - - ConfigserverConfig.Builder builder = new ConfigserverConfig.Builder(); - builder.nodeAdminInContainer(nodeAdminInContainer); - ConfigserverConfig config = new ConfigserverConfig(builder); - - - UnionMonitorManager manager = new UnionMonitorManager( - slobrokMonitorManager, - healthMonitorManager, - config); - - manager.getStatus(applicationId, clusterId, serviceType, new ConfigId("config-id")); - - verify(slobrokMonitorManager, times(expectedSlobrokCalls)).getStatus(any(), any(), any(), any()); - verify(healthMonitorManager, times(expectedHealthCalls)).getStatus(any(), any(), any(), any()); + private void testWith(ServiceStatus healthStatus, + ServiceStatus slobrokStatus, + ServiceStatus expectedStatus) { + when(healthMonitorManager.getStatus(any(), any(), any(), any())).thenReturn(healthStatus); + when(slobrokMonitorManager.getStatus(any(), any(), any(), any())).thenReturn(slobrokStatus); + ServiceStatus status = manager.getStatus(ZONE_APPLICATION_ID, NODE_ADMIN, CONTAINER, new ConfigId("config-id")); + assertSame(expectedStatus, status); } }
\ No newline at end of file diff --git a/service-monitor/src/test/java/com/yahoo/vespa/service/monitor/internal/health/ApplicationHealthMonitorTest.java b/service-monitor/src/test/java/com/yahoo/vespa/service/monitor/internal/health/ApplicationHealthMonitorTest.java new file mode 100644 index 00000000000..51b0503565f --- /dev/null +++ b/service-monitor/src/test/java/com/yahoo/vespa/service/monitor/internal/health/ApplicationHealthMonitorTest.java @@ -0,0 +1,24 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.service.monitor.internal.health; + +import com.yahoo.vespa.applicationmodel.ServiceStatus; +import com.yahoo.vespa.service.monitor.application.ConfigServerApplication; +import com.yahoo.vespa.service.monitor.internal.ConfigserverUtil; +import org.junit.Test; + +import static com.yahoo.vespa.applicationmodel.ServiceStatus.NOT_CHECKED; +import static org.junit.Assert.assertEquals; + +public class ApplicationHealthMonitorTest { + @Test + public void sanityCheck() { + ApplicationHealthMonitor monitor = ApplicationHealthMonitor.startMonitoring( + ConfigserverUtil.makeExampleConfigServer()); + ServiceStatus status = monitor.getStatus( + ConfigServerApplication.CONFIG_SERVER_APPLICATION.getApplicationId(), + ConfigServerApplication.CLUSTER_ID, + ConfigServerApplication.SERVICE_TYPE, + ConfigServerApplication.configIdFrom(0)); + assertEquals(NOT_CHECKED, status); + } +}
\ No newline at end of file diff --git a/service-monitor/src/test/java/com/yahoo/vespa/service/monitor/internal/health/HealthMonitorManagerTest.java b/service-monitor/src/test/java/com/yahoo/vespa/service/monitor/internal/health/HealthMonitorManagerTest.java new file mode 100644 index 00000000000..b9d25406f9b --- /dev/null +++ b/service-monitor/src/test/java/com/yahoo/vespa/service/monitor/internal/health/HealthMonitorManagerTest.java @@ -0,0 +1,49 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.service.monitor.internal.health; + +import com.yahoo.cloud.config.ConfigserverConfig; +import com.yahoo.config.model.api.ApplicationInfo; +import com.yahoo.vespa.applicationmodel.ClusterId; +import com.yahoo.vespa.applicationmodel.ConfigId; +import com.yahoo.vespa.applicationmodel.ServiceStatus; +import com.yahoo.vespa.applicationmodel.ServiceType; +import com.yahoo.vespa.service.monitor.application.ZoneApplication; +import com.yahoo.vespa.service.monitor.internal.ConfigserverUtil; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class HealthMonitorManagerTest { + @Test + public void addRemove() { + ConfigserverConfig config = ConfigserverUtil.createExampleConfigserverConfig(); + HealthMonitorManager manager = new HealthMonitorManager(config); + ApplicationInfo applicationInfo = ConfigserverUtil.makeExampleConfigServer(); + manager.applicationActivated(applicationInfo); + manager.applicationRemoved(applicationInfo.getApplicationId()); + } + + @Test + public void withNodeAdmin() { + ConfigserverConfig config = ConfigserverUtil.createExampleConfigserverConfig(); + HealthMonitorManager manager = new HealthMonitorManager(config); + ServiceStatus status = manager.getStatus( + ZoneApplication.ZONE_APPLICATION_ID, + ClusterId.NODE_ADMIN, + ServiceType.CONTAINER, + new ConfigId("config-id-1")); + assertEquals(ServiceStatus.NOT_CHECKED, status); + } + + @Test + public void withHostAdmin() { + ConfigserverConfig config = ConfigserverUtil.createExampleConfigserverConfig(false, true); + HealthMonitorManager manager = new HealthMonitorManager(config); + ServiceStatus status = manager.getStatus( + ZoneApplication.ZONE_APPLICATION_ID, + ClusterId.NODE_ADMIN, + ServiceType.CONTAINER, + new ConfigId("config-id-1")); + assertEquals(ServiceStatus.UP, status); + } +}
\ No newline at end of file diff --git a/service-monitor/src/test/java/com/yahoo/vespa/service/monitor/internal/health/HealthMonitorTest.java b/service-monitor/src/test/java/com/yahoo/vespa/service/monitor/internal/health/HealthMonitorTest.java new file mode 100644 index 00000000000..cca1530ad97 --- /dev/null +++ b/service-monitor/src/test/java/com/yahoo/vespa/service/monitor/internal/health/HealthMonitorTest.java @@ -0,0 +1,21 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.service.monitor.internal.health; + +import com.yahoo.vespa.applicationmodel.ServiceStatus; +import org.junit.Test; + +import java.net.MalformedURLException; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; + +public class HealthMonitorTest { + @Test + public void basicTests() throws MalformedURLException { + HealthClient healthClient = mock(HealthClient.class); + try (HealthMonitor monitor = new HealthMonitor(healthClient)) { + monitor.startMonitoring(); + assertEquals(ServiceStatus.NOT_CHECKED, monitor.getStatus()); + } + } +}
\ No newline at end of file diff --git a/service-monitor/src/test/java/com/yahoo/vespa/service/monitor/internal/slobrok/SlobrokMonitorManagerImplTest.java b/service-monitor/src/test/java/com/yahoo/vespa/service/monitor/internal/slobrok/SlobrokMonitorManagerImplTest.java index 8e4443df83b..a567559980b 100644 --- a/service-monitor/src/test/java/com/yahoo/vespa/service/monitor/internal/slobrok/SlobrokMonitorManagerImplTest.java +++ b/service-monitor/src/test/java/com/yahoo/vespa/service/monitor/internal/slobrok/SlobrokMonitorManagerImplTest.java @@ -2,7 +2,7 @@ package com.yahoo.vespa.service.monitor.internal.slobrok; import com.yahoo.config.model.api.ApplicationInfo; -import com.yahoo.config.model.api.SuperModel; +import com.yahoo.config.provision.ApplicationId; import com.yahoo.vespa.applicationmodel.ClusterId; import com.yahoo.vespa.applicationmodel.ConfigId; import com.yahoo.vespa.applicationmodel.ServiceStatus; @@ -28,18 +28,19 @@ public class SlobrokMonitorManagerImplTest { private final SlobrokMonitorManagerImpl slobrokMonitorManager = new SlobrokMonitorManagerImpl(slobrokMonitorFactory); private final SlobrokMonitor slobrokMonitor = mock(SlobrokMonitor.class); - private final SuperModel superModel = mock(SuperModel.class); + private final ApplicationId applicationId = ApplicationId.from("tenant", "app", "instance"); private final ApplicationInfo application = mock(ApplicationInfo.class); private final ClusterId clusterId = new ClusterId("cluster-id"); @Before public void setup() { when(slobrokMonitorFactory.get()).thenReturn(slobrokMonitor); + when(application.getApplicationId()).thenReturn(applicationId); } @Test public void testActivationOfApplication() { - slobrokMonitorManager.applicationActivated(superModel, application); + slobrokMonitorManager.applicationActivated(application); verify(slobrokMonitorFactory, times(1)).get(); } @@ -51,14 +52,14 @@ public class SlobrokMonitorManagerImplTest { @Test public void testGetStatus_ApplicationInSlobrok() { - slobrokMonitorManager.applicationActivated(superModel, application); + slobrokMonitorManager.applicationActivated(application); when(slobrokMonitor.registeredInSlobrok("config.id")).thenReturn(true); assertEquals(ServiceStatus.UP, getStatus("topleveldispatch")); } @Test public void testGetStatus_ServiceNotInSlobrok() { - slobrokMonitorManager.applicationActivated(superModel, application); + slobrokMonitorManager.applicationActivated(application); when(slobrokMonitor.registeredInSlobrok("config.id")).thenReturn(false); assertEquals(ServiceStatus.DOWN, getStatus("topleveldispatch")); } diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/EntityBindingsMapper.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/EntityBindingsMapper.java index 1504119d9cc..ab127b19bf1 100644 --- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/EntityBindingsMapper.java +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/EntityBindingsMapper.java @@ -10,8 +10,13 @@ import com.yahoo.vespa.athenz.identityprovider.api.bindings.SignedIdentityDocume import com.yahoo.vespa.athenz.identityprovider.api.bindings.VespaUniqueInstanceIdEntity; import com.yahoo.vespa.athenz.utils.AthenzIdentities; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Path; import java.util.Base64; +import static com.yahoo.vespa.athenz.identityprovider.api.VespaUniqueInstanceId.fromDottedString; + /** * Utility class for mapping objects model types and their Jackson binding versions. * @@ -33,7 +38,7 @@ public class EntityBindingsMapper { public static VespaUniqueInstanceId toVespaUniqueInstanceId(VespaUniqueInstanceIdEntity entity) { return new VespaUniqueInstanceId( - entity.clusterIndex, entity.clusterId, entity.instance, entity.application, entity.tenant, entity.region, entity.environment); + entity.clusterIndex, entity.clusterId, entity.instance, entity.application, entity.tenant, entity.region, entity.environment, entity.type != null ? IdentityType.fromId(entity.type) : null); // TODO Remove support for legacy representation without type } public static IdentityDocument toIdentityDocument(IdentityDocumentEntity entity) { @@ -50,17 +55,22 @@ public class EntityBindingsMapper { toIdentityDocument(entity.identityDocument), entity.signature, entity.signingKeyVersion, - VespaUniqueInstanceId.fromDottedString(entity.providerUniqueId), + fromDottedString(entity.providerUniqueId), entity.dnsSuffix, (AthenzService) AthenzIdentities.from(entity.providerService), entity.ztsEndpoint, - entity.documentVersion); + entity.documentVersion, + entity.configServerHostname, + entity.instanceHostname, + entity.createdAt, + entity.ipAddresses, + entity.identityType != null ? IdentityType.fromId(entity.identityType) : null); // TODO Remove support for legacy representation without type } public static VespaUniqueInstanceIdEntity toVespaUniqueInstanceIdEntity(VespaUniqueInstanceId model) { return new VespaUniqueInstanceIdEntity( model.tenant(), model.application(), model.environment(), model.region(), - model.instance(), model.clusterId(), model.clusterIndex()); + model.instance(), model.clusterId(), model.clusterIndex(), model.type() != null ? model.type().id() : null); // TODO Remove support for legacy representation without type } public static IdentityDocumentEntity toIdentityDocumentEntity(IdentityDocument model) { @@ -84,10 +94,33 @@ public class EntityBindingsMapper { model.dnsSuffix(), model.providerService().getFullName(), model.ztsEndpoint(), - model.documentVersion()); + model.documentVersion(), + model.configServerHostname(), + model.instanceHostname(), + model.createdAt(), + model.ipAddresses(), + model.identityType() != null ? model.identityType().id() : null); // TODO Remove support for legacy representation without type } catch (JsonProcessingException e) { throw new RuntimeException(e); } } + public static SignedIdentityDocument readSignedIdentityDocumentFromFile(Path file) { + try { + SignedIdentityDocumentEntity entity = mapper.readValue(file.toFile(), SignedIdentityDocumentEntity.class); + return EntityBindingsMapper.toSignedIdentityDocument(entity); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + public static void writeSignedIdentityDocumentToFile(Path file, SignedIdentityDocument document) { + try { + SignedIdentityDocumentEntity entity = EntityBindingsMapper.toSignedIdentityDocumentEntity(document); + mapper.writeValue(file.toFile(), entity); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + } diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/IdentityDocument.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/IdentityDocument.java index 8da2bd0a343..82d0a3d622c 100644 --- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/IdentityDocument.java +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/IdentityDocument.java @@ -8,7 +8,9 @@ import java.util.Set; * The identity document that contains the instance specific information * * @author bjorncs + * @deprecated Will soon be inlined into {@link SignedIdentityDocument} */ +@Deprecated public class IdentityDocument { private final VespaUniqueInstanceId providerUniqueId; private final String configServerHostname; diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/IdentityType.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/IdentityType.java new file mode 100644 index 00000000000..4ca2e34a618 --- /dev/null +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/IdentityType.java @@ -0,0 +1,25 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.athenz.identityprovider.api; + +import java.util.Arrays; + +/** + * Represents the types of identities that the configserver can provide. + * + * @author bjorncs + */ +public enum IdentityType {TENANT("tenant"), NODE("node"); + private final String id; + + IdentityType(String id) { this.id = id; } + + public String id() { return id; } + + public static IdentityType fromId(String id) { + return Arrays.stream(values()) + .filter(v -> v.id.equals(id)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("Invalid id: " + id)); + } +} + diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/SignedIdentityDocument.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/SignedIdentityDocument.java index d184efc0221..60be42544c7 100644 --- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/SignedIdentityDocument.java +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/SignedIdentityDocument.java @@ -4,6 +4,8 @@ package com.yahoo.vespa.athenz.identityprovider.api; import com.yahoo.vespa.athenz.api.AthenzService; import java.net.URI; +import java.time.Instant; +import java.util.Set; /** * A signed identity document which contains a {@link IdentityDocument} @@ -22,6 +24,11 @@ public class SignedIdentityDocument { private final AthenzService providerService; private final URI ztsEndpoint; private final int documentVersion; + private final String configServerHostname; + private final String instanceHostname; + private final Instant createdAt; + private final Set<String> ipAddresses; + private final IdentityType identityType; public SignedIdentityDocument(IdentityDocument identityDocument, String signature, @@ -30,7 +37,12 @@ public class SignedIdentityDocument { String dnsSuffix, AthenzService providerService, URI ztsEndpoint, - int documentVersion) { + int documentVersion, + String configServerHostname, + String instanceHostname, + Instant createdAt, + Set<String> ipAddresses, + IdentityType identityType) { this.identityDocument = identityDocument; this.signature = signature; this.signingKeyVersion = signingKeyVersion; @@ -39,6 +51,11 @@ public class SignedIdentityDocument { this.providerService = providerService; this.ztsEndpoint = ztsEndpoint; this.documentVersion = documentVersion; + this.configServerHostname = configServerHostname; + this.instanceHostname = instanceHostname; + this.createdAt = createdAt; + this.ipAddresses = ipAddresses; + this.identityType = identityType; } public IdentityDocument identityDocument() { @@ -72,4 +89,24 @@ public class SignedIdentityDocument { public int documentVersion() { return documentVersion; } + + public String configServerHostname() { + return configServerHostname; + } + + public String instanceHostname() { + return instanceHostname; + } + + public Instant createdAt() { + return createdAt; + } + + public Set<String> ipAddresses() { + return ipAddresses; + } + + public IdentityType identityType() { + return identityType; + } } diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/VespaUniqueInstanceId.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/VespaUniqueInstanceId.java index 5539ba53882..be94cc59691 100644 --- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/VespaUniqueInstanceId.java +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/VespaUniqueInstanceId.java @@ -4,6 +4,8 @@ package com.yahoo.vespa.athenz.identityprovider.api; import java.util.Objects; /** + * Represents the unique instance id as used in Vespa's integration with Athenz Copper Argos + * * @author bjorncs */ public class VespaUniqueInstanceId { @@ -15,6 +17,7 @@ public class VespaUniqueInstanceId { private final String tenant; private final String region; private final String environment; + private final IdentityType type; public VespaUniqueInstanceId(int clusterIndex, String clusterId, @@ -22,7 +25,8 @@ public class VespaUniqueInstanceId { String application, String tenant, String region, - String environment) { + String environment, + IdentityType type) { this.clusterIndex = clusterIndex; this.clusterId = clusterId; this.instance = instance; @@ -30,21 +34,43 @@ public class VespaUniqueInstanceId { this.tenant = tenant; this.region = region; this.environment = environment; + this.type = type; } + // TODO Remove support for legacy representation without type + @Deprecated + public VespaUniqueInstanceId(int clusterIndex, + String clusterId, + String instance, + String application, + String tenant, + String region, + String environment) { + this(clusterIndex, clusterId, instance, application, tenant, region, environment, null); + } + + + // TODO Remove support for legacy representation without type public static VespaUniqueInstanceId fromDottedString(String instanceId) { String[] tokens = instanceId.split("\\."); - if (tokens.length != 7) { + if (tokens.length != 7 && tokens.length != 8) { throw new IllegalArgumentException("Invalid instance id: " + instanceId); } return new VespaUniqueInstanceId( - Integer.parseInt(tokens[0]), tokens[1], tokens[2], tokens[3], tokens[4], tokens[5], tokens[6]); + Integer.parseInt(tokens[0]), tokens[1], tokens[2], tokens[3], tokens[4], tokens[5], tokens[6], tokens.length == 8 ? IdentityType.fromId(tokens[7]) : null); } + // TODO Remove support for legacy representation without type public String asDottedString() { - return String.format( - "%d.%s.%s.%s.%s.%s.%s", - clusterIndex, clusterId, instance, application, tenant, region, environment); + if (type != null) { + return String.format( + "%d.%s.%s.%s.%s.%s.%s.%s", + clusterIndex, clusterId, instance, application, tenant, region, environment, type.id()); + } else { + return String.format( + "%d.%s.%s.%s.%s.%s.%s", + clusterIndex, clusterId, instance, application, tenant, region, environment); + } } public int clusterIndex() { @@ -75,6 +101,8 @@ public class VespaUniqueInstanceId { return environment; } + public IdentityType type() { return type; } + @Override public String toString() { return "VespaUniqueInstanceId{" + @@ -85,6 +113,7 @@ public class VespaUniqueInstanceId { ", tenant='" + tenant + '\'' + ", region='" + region + '\'' + ", environment='" + environment + '\'' + + ", type=" + type + '}'; } @@ -99,11 +128,12 @@ public class VespaUniqueInstanceId { Objects.equals(application, that.application) && Objects.equals(tenant, that.tenant) && Objects.equals(region, that.region) && - Objects.equals(environment, that.environment); + Objects.equals(environment, that.environment) && + type == that.type; } @Override public int hashCode() { - return Objects.hash(clusterIndex, clusterId, instance, application, tenant, region, environment); + return Objects.hash(clusterIndex, clusterId, instance, application, tenant, region, environment, type); } } diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/bindings/IdentityDocumentApi.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/bindings/IdentityDocumentApi.java index 775a49349a3..fc5392411c1 100644 --- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/bindings/IdentityDocumentApi.java +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/bindings/IdentityDocumentApi.java @@ -5,7 +5,6 @@ import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; -import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; /** @@ -16,11 +15,6 @@ public interface IdentityDocumentApi { @GET @Produces(MediaType.APPLICATION_JSON) - @Deprecated - SignedIdentityDocumentEntity getIdentityDocument(@QueryParam("hostname") String hostname); - - @GET - @Produces(MediaType.APPLICATION_JSON) @Path("/node/{host}") SignedIdentityDocumentEntity getNodeIdentityDocument(@PathParam("host") String host); diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/bindings/IdentityDocumentEntity.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/bindings/IdentityDocumentEntity.java index 58a4f1e24bf..b4b2e82ab0e 100644 --- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/bindings/IdentityDocumentEntity.java +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/bindings/IdentityDocumentEntity.java @@ -10,8 +10,10 @@ import java.util.Set; /** * @author bjorncs + * @deprecated Will soon be inlined into {@link SignedIdentityDocumentEntity} */ @JsonIgnoreProperties(ignoreUnknown = true) +@Deprecated public class IdentityDocumentEntity { @JsonProperty("provider-unique-id") diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/bindings/SignedIdentityDocumentEntity.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/bindings/SignedIdentityDocumentEntity.java index e397b81ef9e..aa514b3caf3 100644 --- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/bindings/SignedIdentityDocumentEntity.java +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/bindings/SignedIdentityDocumentEntity.java @@ -11,8 +11,10 @@ import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import java.io.IOException; import java.io.UncheckedIOException; import java.net.URI; +import java.time.Instant; import java.util.Base64; import java.util.Objects; +import java.util.Set; /** * @author bjorncs @@ -31,6 +33,11 @@ public class SignedIdentityDocumentEntity { @JsonProperty("provider-service") public final String providerService; @JsonProperty("zts-endpoint") public final URI ztsEndpoint; @JsonProperty("document-version") public final int documentVersion; + @JsonProperty("configserver-hostname") public final String configServerHostname; + @JsonProperty("instance-hostname") public final String instanceHostname; + @JsonProperty("created-at") public final Instant createdAt; + @JsonProperty("ip-addresses") public final Set<String> ipAddresses; + @JsonProperty("identity-type") public final String identityType; @JsonCreator public SignedIdentityDocumentEntity(@JsonProperty("identity-document") String rawIdentityDocument, @@ -40,7 +47,12 @@ public class SignedIdentityDocumentEntity { @JsonProperty("dns-suffix") String dnsSuffix, @JsonProperty("provider-service") String providerService, @JsonProperty("zts-endpoint") URI ztsEndpoint, - @JsonProperty("document-version") int documentVersion) { + @JsonProperty("document-version") int documentVersion, + @JsonProperty("configserver-hostname") String configServerHostname, + @JsonProperty("instance-hostname") String instanceHostname, + @JsonProperty("created-at") Instant createdAt, + @JsonProperty("ip-addresses") Set<String> ipAddresses, + @JsonProperty("identity-type") String identityType) { this.rawIdentityDocument = rawIdentityDocument; this.identityDocument = parseIdentityDocument(rawIdentityDocument); this.signature = signature; @@ -50,6 +62,11 @@ public class SignedIdentityDocumentEntity { this.providerService = providerService; this.ztsEndpoint = ztsEndpoint; this.documentVersion = documentVersion; + this.configServerHostname = configServerHostname; + this.instanceHostname = instanceHostname; + this.createdAt = createdAt; + this.ipAddresses = ipAddresses; + this.identityType = identityType; } private static IdentityDocumentEntity parseIdentityDocument(String rawIdentityDocument) { @@ -73,7 +90,16 @@ public class SignedIdentityDocumentEntity { ", identityDocument=" + identityDocument + ", signature='" + signature + '\'' + ", signingKeyVersion=" + signingKeyVersion + + ", providerUniqueId='" + providerUniqueId + '\'' + + ", dnsSuffix='" + dnsSuffix + '\'' + + ", providerService='" + providerService + '\'' + + ", ztsEndpoint=" + ztsEndpoint + ", documentVersion=" + documentVersion + + ", configServerHostname='" + configServerHostname + '\'' + + ", instanceHostname='" + instanceHostname + '\'' + + ", createdAt=" + createdAt + + ", ipAddresses=" + ipAddresses + + ", identityType=" + identityType + '}'; } @@ -86,11 +112,20 @@ public class SignedIdentityDocumentEntity { documentVersion == that.documentVersion && Objects.equals(rawIdentityDocument, that.rawIdentityDocument) && Objects.equals(identityDocument, that.identityDocument) && - Objects.equals(signature, that.signature); + Objects.equals(signature, that.signature) && + Objects.equals(providerUniqueId, that.providerUniqueId) && + Objects.equals(dnsSuffix, that.dnsSuffix) && + Objects.equals(providerService, that.providerService) && + Objects.equals(ztsEndpoint, that.ztsEndpoint) && + Objects.equals(configServerHostname, that.configServerHostname) && + Objects.equals(instanceHostname, that.instanceHostname) && + Objects.equals(createdAt, that.createdAt) && + Objects.equals(ipAddresses, that.ipAddresses) && + Objects.equals(identityType, identityType); } @Override public int hashCode() { - return Objects.hash(rawIdentityDocument, identityDocument, signature, signingKeyVersion, documentVersion); + return Objects.hash(rawIdentityDocument, identityDocument, signature, signingKeyVersion, providerUniqueId, dnsSuffix, providerService, ztsEndpoint, documentVersion, configServerHostname, instanceHostname, createdAt, ipAddresses, identityType); } } diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/bindings/VespaUniqueInstanceIdEntity.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/bindings/VespaUniqueInstanceIdEntity.java index 3c521e992ad..3fdbb49b28e 100644 --- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/bindings/VespaUniqueInstanceIdEntity.java +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/bindings/VespaUniqueInstanceIdEntity.java @@ -2,6 +2,7 @@ package com.yahoo.vespa.athenz.identityprovider.api.bindings; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import java.util.Objects; @@ -26,14 +27,18 @@ public class VespaUniqueInstanceIdEntity { public final String clusterId; @JsonProperty("cluster-index") public final int clusterIndex; + @JsonProperty("type") + public final String type; + @JsonCreator public VespaUniqueInstanceIdEntity(@JsonProperty("tenant") String tenant, @JsonProperty("application") String application, @JsonProperty("environment") String environment, @JsonProperty("region") String region, @JsonProperty("instance") String instance, @JsonProperty("cluster-id") String clusterId, - @JsonProperty("cluster-index") int clusterIndex) { + @JsonProperty("cluster-index") int clusterIndex, + @JsonProperty("type") String type) { this.tenant = tenant; this.application = application; this.environment = environment; @@ -41,8 +46,21 @@ public class VespaUniqueInstanceIdEntity { this.instance = instance; this.clusterId = clusterId; this.clusterIndex = clusterIndex; + this.type = type; } + @Deprecated + public VespaUniqueInstanceIdEntity(String tenant, + String application, + String environment, + String region, + String instance, + String clusterId, + int clusterIndex) { + this(tenant, application, environment, region, instance, clusterId, clusterIndex, null); + } + + @Override public String toString() { return "VespaUniqueInstanceIdEntity{" + @@ -53,6 +71,7 @@ public class VespaUniqueInstanceIdEntity { ", instance='" + instance + '\'' + ", clusterId='" + clusterId + '\'' + ", clusterIndex=" + clusterIndex + + ", type='" + type + '\'' + '}'; } @@ -67,11 +86,12 @@ public class VespaUniqueInstanceIdEntity { Objects.equals(environment, that.environment) && Objects.equals(region, that.region) && Objects.equals(instance, that.instance) && - Objects.equals(clusterId, that.clusterId); + Objects.equals(clusterId, that.clusterId) && + Objects.equals(type, that.type); } @Override public int hashCode() { - return Objects.hash(tenant, application, environment, region, instance, clusterId, clusterIndex); + return Objects.hash(tenant, application, environment, region, instance, clusterId, clusterIndex, type); } } diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/AthenzCredentialsService.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/AthenzCredentialsService.java index 96e93ca419d..e8ef2d9f97e 100644 --- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/AthenzCredentialsService.java +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/AthenzCredentialsService.java @@ -2,6 +2,7 @@ package com.yahoo.vespa.athenz.identityprovider.client; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.yahoo.container.core.identity.IdentityConfig; import com.yahoo.vespa.athenz.api.AthenzService; import com.yahoo.vespa.athenz.identityprovider.api.EntityBindingsMapper; @@ -28,7 +29,7 @@ import static com.yahoo.vespa.athenz.tls.KeyStoreType.JKS; */ class AthenzCredentialsService { - private static final ObjectMapper mapper = new ObjectMapper(); + private static final ObjectMapper mapper = new ObjectMapper().registerModule(new JavaTimeModule()); private final IdentityConfig identityConfig; private final IdentityDocumentClient identityDocumentClient; diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/DefaultIdentityDocumentClient.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/DefaultIdentityDocumentClient.java index 90d1312c9f9..b9aba6e66b0 100644 --- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/DefaultIdentityDocumentClient.java +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/client/DefaultIdentityDocumentClient.java @@ -2,14 +2,12 @@ package com.yahoo.vespa.athenz.identityprovider.client; import com.fasterxml.jackson.databind.ObjectMapper; -import com.yahoo.vespa.athenz.api.AthenzService; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.yahoo.vespa.athenz.identity.ServiceIdentityProvider; import com.yahoo.vespa.athenz.identityprovider.api.EntityBindingsMapper; import com.yahoo.vespa.athenz.identityprovider.api.IdentityDocumentClient; import com.yahoo.vespa.athenz.identityprovider.api.SignedIdentityDocument; -import com.yahoo.vespa.athenz.identityprovider.api.VespaUniqueInstanceId; import com.yahoo.vespa.athenz.identityprovider.api.bindings.SignedIdentityDocumentEntity; -import com.yahoo.vespa.athenz.utils.AthenzIdentities; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.client.methods.RequestBuilder; @@ -34,7 +32,7 @@ import java.util.function.Supplier; public class DefaultIdentityDocumentClient implements IdentityDocumentClient { private static final String IDENTITY_DOCUMENT_API = "/athenz/v1/provider/identity-document/"; - private static final ObjectMapper objectMapper = new ObjectMapper(); + private static final ObjectMapper objectMapper = new ObjectMapper().registerModule(new JavaTimeModule()); private final Supplier<SSLContext> sslContextSupplier; private final HostnameVerifier hostnameVerifier; @@ -82,15 +80,7 @@ public class DefaultIdentityDocumentClient implements IdentityDocumentClient { String responseContent = EntityUtils.toString(response.getEntity()); if (HttpStatus.isSuccess(response.getStatusLine().getStatusCode())) { SignedIdentityDocumentEntity entity = objectMapper.readValue(responseContent, SignedIdentityDocumentEntity.class); - return new SignedIdentityDocument( - EntityBindingsMapper.toIdentityDocument(entity.identityDocument), - entity.signature, - entity.signingKeyVersion, - VespaUniqueInstanceId.fromDottedString(entity.providerUniqueId), - entity.dnsSuffix, - (AthenzService) AthenzIdentities.from(entity.providerService), - entity.ztsEndpoint, - entity.documentVersion); + return EntityBindingsMapper.toSignedIdentityDocument(entity); } else { throw new RuntimeException( String.format( diff --git a/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/identityprovider/api/VespaUniqueInstanceIdTest.java b/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/identityprovider/api/VespaUniqueInstanceIdTest.java index 8c4e4c1262d..86b6c566987 100644 --- a/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/identityprovider/api/VespaUniqueInstanceIdTest.java +++ b/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/identityprovider/api/VespaUniqueInstanceIdTest.java @@ -2,6 +2,7 @@ package com.yahoo.vespa.athenz.identityprovider.api; import org.junit.Test; +import static com.yahoo.vespa.athenz.identityprovider.api.IdentityType.*; import static org.junit.Assert.*; /** @@ -12,6 +13,18 @@ public class VespaUniqueInstanceIdTest { @Test public void can_serialize_to_and_deserialize_from_string() { VespaUniqueInstanceId id = + new VespaUniqueInstanceId(1, "cluster-id", "instance", "application", "tenant", "region", "environment", TENANT); + String stringRepresentation = id.asDottedString(); + String expectedStringRepresentation = "1.cluster-id.instance.application.tenant.region.environment.tenant"; + assertEquals(expectedStringRepresentation, stringRepresentation); + VespaUniqueInstanceId deserializedId = VespaUniqueInstanceId.fromDottedString(stringRepresentation); + assertEquals(id, deserializedId); + } + + // TODO Remove support for legacy representation without type + @Test + public void supports_legacy_representation_without_type() { + VespaUniqueInstanceId id = new VespaUniqueInstanceId(1, "cluster-id", "instance", "application", "tenant", "region", "environment"); String stringRepresentation = id.asDottedString(); String expectedStringRepresentation = "1.cluster-id.instance.application.tenant.region.environment"; diff --git a/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/identityprovider/client/AthenzIdentityProviderImplTest.java b/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/identityprovider/client/AthenzIdentityProviderImplTest.java index 2e9b29f5327..7ad465a7d80 100644 --- a/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/identityprovider/client/AthenzIdentityProviderImplTest.java +++ b/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/identityprovider/client/AthenzIdentityProviderImplTest.java @@ -11,6 +11,7 @@ import com.yahoo.test.ManualClock; import com.yahoo.vespa.athenz.api.AthenzService; import com.yahoo.vespa.athenz.identityprovider.api.EntityBindingsMapper; import com.yahoo.vespa.athenz.identityprovider.api.IdentityDocument; +import com.yahoo.vespa.athenz.identityprovider.api.IdentityType; import com.yahoo.vespa.athenz.identityprovider.api.SignedIdentityDocument; import com.yahoo.vespa.athenz.identityprovider.api.VespaUniqueInstanceId; import com.yahoo.vespa.athenz.tls.KeyStoreBuilder; @@ -132,7 +133,7 @@ public class AthenzIdentityProviderImplTest { } private static String getIdentityDocument() throws JsonProcessingException { - VespaUniqueInstanceId instanceId = new VespaUniqueInstanceId(0, "default", "default", "application", "tenant", "us-north-1", "dev"); + VespaUniqueInstanceId instanceId = new VespaUniqueInstanceId(0, "default", "default", "application", "tenant", "us-north-1", "dev", IdentityType.TENANT); SignedIdentityDocument signedIdentityDocument = new SignedIdentityDocument( new IdentityDocument(instanceId, "localhost", "x.y.com", Instant.EPOCH, Collections.emptySet()), "dummysignature", @@ -141,7 +142,12 @@ public class AthenzIdentityProviderImplTest { "dev-us-north-1.vespa.cloud", new AthenzService("vespa.vespa.provider_dev_us-north-1"), URI.create("https://zts:4443/zts/v1"), - 1); + 1, + "localhost", + "x.y.com", + Instant.EPOCH, + Collections.emptySet(), + IdentityType.TENANT); return new ObjectMapper().registerModule(new JavaTimeModule()) .writeValueAsString(EntityBindingsMapper.toSignedIdentityDocumentEntity(signedIdentityDocument)); |