diff options
76 files changed, 1029 insertions, 238 deletions
diff --git a/config-model/src/main/java/com/yahoo/config/model/deploy/DeployState.java b/config-model/src/main/java/com/yahoo/config/model/deploy/DeployState.java index 21a8297910f..1892c8920a7 100644 --- a/config-model/src/main/java/com/yahoo/config/model/deploy/DeployState.java +++ b/config-model/src/main/java/com/yahoo/config/model/deploy/DeployState.java @@ -15,6 +15,7 @@ import com.yahoo.config.model.api.ContainerEndpoint; import com.yahoo.config.model.api.HostProvisioner; import com.yahoo.config.model.api.Model; import com.yahoo.config.model.api.ModelContext; +import com.yahoo.config.model.api.TlsSecrets; import com.yahoo.config.model.api.ValidationParameters; import com.yahoo.config.model.application.provider.BaseDeployLogger; import com.yahoo.config.model.application.provider.MockFileRegistry; @@ -256,6 +257,8 @@ public class DeployState implements ConfigDefinitionStore { public Instant now() { return now; } + public Optional<TlsSecrets> tlsSecrets() { return properties.tlsSecrets(); } + public static class Builder { private ApplicationPackage applicationPackage = MockApplicationPackage.createEmpty(); @@ -273,6 +276,7 @@ public class DeployState implements ConfigDefinitionStore { private Zone zone = Zone.defaultZone(); private Instant now = Instant.now(); private Version wantedNodeVespaVersion = Vtag.currentVersion; + private Optional<TlsSecrets> tlsSecrets = Optional.empty(); public Builder applicationPackage(ApplicationPackage applicationPackage) { this.applicationPackage = applicationPackage; diff --git a/config-model/src/main/java/com/yahoo/config/model/deploy/TestProperties.java b/config-model/src/main/java/com/yahoo/config/model/deploy/TestProperties.java index 87ff9d1bb2a..d974db73547 100644 --- a/config-model/src/main/java/com/yahoo/config/model/deploy/TestProperties.java +++ b/config-model/src/main/java/com/yahoo/config/model/deploy/TestProperties.java @@ -5,6 +5,7 @@ import com.google.common.collect.ImmutableList; import com.yahoo.config.model.api.ConfigServerSpec; import com.yahoo.config.model.api.ContainerEndpoint; import com.yahoo.config.model.api.ModelContext; +import com.yahoo.config.model.api.TlsSecrets; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.HostName; import com.yahoo.config.provision.Rotation; @@ -13,6 +14,7 @@ import com.yahoo.config.provision.Zone; import java.net.URI; import java.util.Collections; import java.util.List; +import java.util.Optional; import java.util.Set; /** @@ -39,6 +41,7 @@ public class TestProperties implements ModelContext.Properties { private boolean useFdispatchByDefault = true; private boolean dispatchWithProtobuf = true; private boolean useAdaptiveDispatch = false; + private Optional<TlsSecrets> tlsSecrets = Optional.empty(); @Override public boolean multitenant() { return multitenant; } @@ -58,6 +61,7 @@ public class TestProperties implements ModelContext.Properties { @Override public boolean useDedicatedNodeForLogserver() { return useDedicatedNodeForLogserver; } @Override public boolean useFdispatchByDefault() { return useFdispatchByDefault; } @Override public boolean dispatchWithProtobuf() { return dispatchWithProtobuf; } + @Override public Optional<TlsSecrets> tlsSecrets() { return tlsSecrets; } public TestProperties setApplicationId(ApplicationId applicationId) { this.applicationId = applicationId; @@ -90,6 +94,11 @@ public class TestProperties implements ModelContext.Properties { } + public TestProperties setTlsSecrets(Optional<TlsSecrets> tlsSecrets) { + this.tlsSecrets = tlsSecrets; + return this; + } + public static class Spec implements ConfigServerSpec { private final String hostName; diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/Index.java b/config-model/src/main/java/com/yahoo/searchdefinition/Index.java index d7e9e0da081..0ea3f5c24a3 100644 --- a/config-model/src/main/java/com/yahoo/searchdefinition/Index.java +++ b/config-model/src/main/java/com/yahoo/searchdefinition/Index.java @@ -56,8 +56,8 @@ public class Index implements Cloneable, Serializable { /** The boolean index definition, if set */ private BooleanIndexDefinition boolIndex; - // TODO: Remove when experimental posting list format is made default - private boolean experimentalPostingListFormat = false; + /** Whether the posting lists of this index field should have interleaved features (num occs, field length) in document id stream. */ + private boolean interleavedFeatures = false; public Index(String name) { this(name, false); @@ -184,12 +184,12 @@ public class Index implements Cloneable, Serializable { boolIndex = def; } - public void setExperimentalPostingListFormat(boolean value) { - experimentalPostingListFormat = value; + public void setInterleavedFeatures(boolean value) { + interleavedFeatures = value; } - public boolean useExperimentalPostingListFormat() { - return experimentalPostingListFormat; + public boolean useInterleavedFeatures() { + return interleavedFeatures; } } diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/derived/IndexSchema.java b/config-model/src/main/java/com/yahoo/searchdefinition/derived/IndexSchema.java index 3b62807ce73..60b8ee78c7b 100644 --- a/config-model/src/main/java/com/yahoo/searchdefinition/derived/IndexSchema.java +++ b/config-model/src/main/java/com/yahoo/searchdefinition/derived/IndexSchema.java @@ -114,7 +114,7 @@ public class IndexSchema extends Derived implements IndexschemaConfig.Producer { .prefix(f.hasPrefix()) .phrases(f.hasPhrases()) .positions(f.hasPositions()) - .interleavedfeatures(f.useExperimentalPostingListFormat()); + .interleavedfeatures(f.useInterleavedFeatures()); if (!f.getCollectionType().equals("SINGLE")) { ifB.collectiontype(IndexschemaConfig.Indexfield.Collectiontype.Enum.valueOf(f.getCollectionType())); } @@ -175,8 +175,8 @@ public class IndexSchema extends Derived implements IndexschemaConfig.Producer { private boolean phrases = false; // TODO dead, but keep a while to ensure config compatibility? private boolean positions = true;// TODO dead, but keep a while to ensure config compatibility? private BooleanIndexDefinition boolIndex = null; - // TODO: Remove when experimental posting list format is made default - private boolean experimentalPostingListFormat = false; + // Whether the posting lists of this index field should have interleaved features (num occs, field length) in document id stream. + private boolean interleavedFeatures = false; public IndexField(String name, Index.Type type, DataType sdFieldType) { this.name = name; @@ -186,7 +186,7 @@ public class IndexSchema extends Derived implements IndexschemaConfig.Producer { public void setIndexSettings(com.yahoo.searchdefinition.Index index) { if (type.equals(Index.Type.TEXT)) { prefix = index.isPrefix(); - experimentalPostingListFormat = index.useExperimentalPostingListFormat(); + interleavedFeatures = index.useInterleavedFeatures(); } sdType = index.getType(); boolIndex = index.getBooleanIndexDefiniton(); @@ -209,7 +209,7 @@ public class IndexSchema extends Derived implements IndexschemaConfig.Producer { public boolean hasPrefix() { return prefix; } public boolean hasPhrases() { return phrases; } public boolean hasPositions() { return positions; } - public boolean useExperimentalPostingListFormat() { return experimentalPostingListFormat; } + public boolean useInterleavedFeatures() { return interleavedFeatures; } public BooleanIndexDefinition getBooleanIndexDefinition() { return boolIndex; diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/IndexOperation.java b/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/IndexOperation.java index 459bb247e5f..39f543c7db3 100644 --- a/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/IndexOperation.java +++ b/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/IndexOperation.java @@ -29,8 +29,7 @@ public class IndexOperation implements FieldOperation { private OptionalLong lowerBound = OptionalLong.empty(); private OptionalLong upperBound = OptionalLong.empty(); private OptionalDouble densePostingListThreshold = OptionalDouble.empty(); - // TODO: Remove when experimental posting list format is made default - private Optional<Boolean> experimentalPostingListFormat = Optional.empty(); + private Optional<Boolean> enableBm25 = Optional.empty(); public String getIndexName() { return indexName; @@ -89,8 +88,8 @@ public class IndexOperation implements FieldOperation { index.setBooleanIndexDefiniton( new BooleanIndexDefinition(arity, lowerBound, upperBound, densePostingListThreshold)); } - if (experimentalPostingListFormat.isPresent()) { - index.setExperimentalPostingListFormat(experimentalPostingListFormat.get()); + if (enableBm25.isPresent()) { + index.setInterleavedFeatures(enableBm25.get()); } } @@ -117,8 +116,8 @@ public class IndexOperation implements FieldOperation { public void setDensePostingListThreshold(double densePostingListThreshold) { this.densePostingListThreshold = OptionalDouble.of(densePostingListThreshold); } - public void setExperimentalPostingListFormat(boolean value) { - experimentalPostingListFormat = Optional.of(value); + public void setEnableBm25(boolean value) { + enableBm25 = Optional.of(value); } } diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/ApplicationContainer.java b/config-model/src/main/java/com/yahoo/vespa/model/container/ApplicationContainer.java index b381168838f..48f7fa3c1a2 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/container/ApplicationContainer.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/container/ApplicationContainer.java @@ -1,8 +1,15 @@ // Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.model.container; +import com.yahoo.config.model.api.TlsSecrets; import com.yahoo.config.model.api.container.ContainerServiceType; import com.yahoo.config.model.producer.AbstractConfigProducer; +import com.yahoo.vespa.model.container.http.ConnectorFactory; +import com.yahoo.vespa.model.container.http.Http; +import com.yahoo.vespa.model.container.http.JettyHttpServer; +import com.yahoo.vespa.model.container.http.ssl.ConfiguredDirectSslProvider; + +import java.util.Optional; /** * A container that is typically used by container clusters set up from the user application. @@ -15,14 +22,23 @@ public final class ApplicationContainer extends Container { private final boolean isHostedVespa; - - public ApplicationContainer(AbstractConfigProducer parent, String name, int index, boolean isHostedVespa) { - this(parent, name, false, index, isHostedVespa); + public ApplicationContainer(AbstractConfigProducer parent, String name, int index, boolean isHostedVespa, Optional<TlsSecrets> tlsSecrets) { + this(parent, name, false, index, isHostedVespa, tlsSecrets); } - public ApplicationContainer(AbstractConfigProducer parent, String name, boolean retired, int index, boolean isHostedVespa) { + public ApplicationContainer(AbstractConfigProducer parent, String name, boolean retired, int index, boolean isHostedVespa, Optional<TlsSecrets> tlsSecrets) { super(parent, name, retired, index); this.isHostedVespa = isHostedVespa; + + if (isHostedVespa && tlsSecrets.isPresent()) { + String connectorName = "tls4443"; + + JettyHttpServer server = Optional.ofNullable(getHttp()) + .map(Http::getHttpServer) + .orElse(getDefaultHttpServer()); + server.addConnector(new ConnectorFactory(connectorName, 4443, + new ConfiguredDirectSslProvider(server.getComponentId().getName(), tlsSecrets.get().key(), tlsSecrets.get().certificate(), null, null))); + } } @Override diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/ApplicationContainerCluster.java b/config-model/src/main/java/com/yahoo/vespa/model/container/ApplicationContainerCluster.java index 9cbaa5f91af..e9db64f8e4b 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/container/ApplicationContainerCluster.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/container/ApplicationContainerCluster.java @@ -4,6 +4,7 @@ package com.yahoo.vespa.model.container; import com.yahoo.component.ComponentId; import com.yahoo.config.FileReference; import com.yahoo.config.application.api.ComponentInfo; +import com.yahoo.config.model.api.TlsSecrets; import com.yahoo.config.model.deploy.DeployState; import com.yahoo.config.model.producer.AbstractConfigProducer; import com.yahoo.container.BundlesConfig; @@ -22,6 +23,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.LinkedHashSet; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -45,13 +47,18 @@ public final class ApplicationContainerCluster extends ContainerCluster<Applicat private ContainerModelEvaluation modelEvaluation; + private Optional<TlsSecrets> tlsSecrets; + public ApplicationContainerCluster(AbstractConfigProducer<?> parent, String subId, String name, DeployState deployState) { super(parent, subId, name, deployState); + + this.tlsSecrets = deployState.tlsSecrets(); restApiGroup = new ConfigProducerGroup<>(this, "rest-api"); servletGroup = new ConfigProducerGroup<>(this, "servlet"); addSimpleComponent(DEFAULT_LINGUISTICS_PROVIDER); addSimpleComponent("com.yahoo.container.jdisc.SecretStoreProvider"); + addSimpleComponent("com.yahoo.container.jdisc.DeprecatedSecretStoreProvider"); addSimpleComponent("com.yahoo.container.jdisc.CertificateStoreProvider"); } @@ -139,4 +146,8 @@ public final class ApplicationContainerCluster extends ContainerCluster<Applicat if (modelEvaluation != null) modelEvaluation.getConfig(builder); } + public Optional<TlsSecrets> getTlsSecrets() { + return tlsSecrets; + } + } diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/http/ssl/ConfiguredDirectSslProvider.java b/config-model/src/main/java/com/yahoo/vespa/model/container/http/ssl/ConfiguredDirectSslProvider.java new file mode 100644 index 00000000000..28dba3331d3 --- /dev/null +++ b/config-model/src/main/java/com/yahoo/vespa/model/container/http/ssl/ConfiguredDirectSslProvider.java @@ -0,0 +1,66 @@ +// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.model.container.http.ssl; + +import com.yahoo.component.ComponentId; +import com.yahoo.container.bundle.BundleInstantiationSpecification; +import com.yahoo.jdisc.http.ConnectorConfig; +import com.yahoo.jdisc.http.ssl.impl.ConfiguredSslContextFactoryProvider; +import com.yahoo.osgi.provider.model.ComponentModel; +import com.yahoo.vespa.model.container.component.SimpleComponent; + +import java.util.Optional; + +import static com.yahoo.component.ComponentSpecification.fromString; + +/** + * Configure SSL with PEM encoded certificate/key strings + * + * @author mortent + * @author andreer + */ +public class ConfiguredDirectSslProvider extends SimpleComponent implements ConnectorConfig.Producer { + public static final String COMPONENT_ID_PREFIX = "configured-ssl-provider@"; + public static final String COMPONENT_CLASS = ConfiguredSslContextFactoryProvider.class.getName(); + public static final String COMPONENT_BUNDLE = "jdisc_http_service"; + + private final String privateKey; + private final String certificate; + private final String caCertificatePath; + private final ConnectorConfig.Ssl.ClientAuth.Enum clientAuthentication; + + public ConfiguredDirectSslProvider(String servername, String privateKey, String certificate, String caCertificatePath, String clientAuthentication) { + super(new ComponentModel( + new BundleInstantiationSpecification(new ComponentId(COMPONENT_ID_PREFIX+servername), + fromString(COMPONENT_CLASS), + fromString(COMPONENT_BUNDLE)))); + this.privateKey = privateKey; + this.certificate = certificate; + this.caCertificatePath = caCertificatePath; + this.clientAuthentication = mapToConfigEnum(clientAuthentication); + } + + @Override + public void getConfig(ConnectorConfig.Builder builder) { + builder.ssl.enabled(true); + builder.ssl.privateKey(privateKey); + builder.ssl.certificate(certificate); + builder.ssl.caCertificateFile(Optional.ofNullable(caCertificatePath).orElse("")); + builder.ssl.clientAuth(clientAuthentication); + } + + public SimpleComponent getComponent() { + return new SimpleComponent(new ComponentModel(getComponentId().stringValue(), COMPONENT_CLASS, COMPONENT_BUNDLE)); + } + + private static ConnectorConfig.Ssl.ClientAuth.Enum mapToConfigEnum(String clientAuthValue) { + if ("disabled".equals(clientAuthValue)) { + return ConnectorConfig.Ssl.ClientAuth.Enum.DISABLED; + } else if ("want".equals(clientAuthValue)) { + return ConnectorConfig.Ssl.ClientAuth.Enum.WANT_AUTH; + } else if ("need".equals(clientAuthValue)) { + return ConnectorConfig.Ssl.ClientAuth.Enum.NEED_AUTH; + } else { + return ConnectorConfig.Ssl.ClientAuth.Enum.DISABLED; + } + } +} diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/http/ssl/ConfiguredSslProvider.java b/config-model/src/main/java/com/yahoo/vespa/model/container/http/ssl/ConfiguredFilebasedSslProvider.java index 3c36933c030..4f84a01ff94 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/container/http/ssl/ConfiguredSslProvider.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/container/http/ssl/ConfiguredFilebasedSslProvider.java @@ -13,9 +13,11 @@ import java.util.Optional; import static com.yahoo.component.ComponentSpecification.fromString; /** + * Configure SSL using file references + * * @author mortent */ -public class ConfiguredSslProvider extends SimpleComponent implements ConnectorConfig.Producer { +public class ConfiguredFilebasedSslProvider extends SimpleComponent implements ConnectorConfig.Producer { public static final String COMPONENT_ID_PREFIX = "configured-ssl-provider@"; public static final String COMPONENT_CLASS = ConfiguredSslContextFactoryProvider.class.getName(); public static final String COMPONENT_BUNDLE = "jdisc_http_service"; @@ -25,7 +27,7 @@ public class ConfiguredSslProvider extends SimpleComponent implements ConnectorC private final String caCertificatePath; private final ConnectorConfig.Ssl.ClientAuth.Enum clientAuthentication; - public ConfiguredSslProvider(String servername, String privateKeyPath, String certificatePath, String caCertificatePath, String clientAuthentication) { + public ConfiguredFilebasedSslProvider(String servername, String privateKeyPath, String certificatePath, String caCertificatePath, String clientAuthentication) { super(new ComponentModel( new BundleInstantiationSpecification(new ComponentId(COMPONENT_ID_PREFIX+servername), fromString(COMPONENT_CLASS), diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/http/xml/JettyConnectorBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/container/http/xml/JettyConnectorBuilder.java index 23865eb9bdd..1b457b1250a 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/container/http/xml/JettyConnectorBuilder.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/container/http/xml/JettyConnectorBuilder.java @@ -9,7 +9,7 @@ import com.yahoo.vespa.model.builder.xml.dom.VespaDomBuilder; import com.yahoo.vespa.model.container.component.SimpleComponent; import com.yahoo.vespa.model.container.http.ConnectorFactory; import com.yahoo.vespa.model.container.http.ssl.CustomSslProvider; -import com.yahoo.vespa.model.container.http.ssl.ConfiguredSslProvider; +import com.yahoo.vespa.model.container.http.ssl.ConfiguredFilebasedSslProvider; import com.yahoo.vespa.model.container.http.ssl.DefaultSslProvider; import org.w3c.dom.Element; @@ -39,7 +39,7 @@ public class JettyConnectorBuilder extends VespaDomBuilder.DomConfigProducerBuil String certificateFile = XML.getValue(XML.getChild(sslConfigurator, "certificate-file")); Optional<String> caCertificateFile = XmlHelper.getOptionalChildValue(sslConfigurator, "ca-certificates-file"); Optional<String> clientAuthentication = XmlHelper.getOptionalChildValue(sslConfigurator, "client-authentication"); - return new ConfiguredSslProvider( + return new ConfiguredFilebasedSslProvider( serverName, privateKeyFile, certificateFile, diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java index f68ddecad9d..57e0b969929 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java @@ -431,7 +431,7 @@ public class ContainerModelBuilder extends ConfigModelBuilder<ContainerModel> { } private void addStandaloneNode(ApplicationContainerCluster cluster) { - ApplicationContainer container = new ApplicationContainer(cluster, "standalone", cluster.getContainers().size(), cluster.isHostedVespa()); + ApplicationContainer container = new ApplicationContainer(cluster, "standalone", cluster.getContainers().size(), cluster.isHostedVespa(), cluster.getTlsSecrets()); cluster.addContainers(Collections.singleton(container)); } @@ -497,7 +497,7 @@ public class ContainerModelBuilder extends ConfigModelBuilder<ContainerModel> { Element nodesElement = XML.getChild(containerElement, "nodes"); Element rotationsElement = XML.getChild(containerElement, "rotations"); if (nodesElement == null) { // default single node on localhost - ApplicationContainer node = new ApplicationContainer(cluster, "container.0", 0, cluster.isHostedVespa()); + ApplicationContainer node = new ApplicationContainer(cluster, "container.0", 0, cluster.isHostedVespa(), cluster.getTlsSecrets()); HostResource host = allocateSingleNodeHost(cluster, log, containerElement, context); node.setHostResource(host); node.initService(context.getDeployLogger()); @@ -686,7 +686,7 @@ public class ContainerModelBuilder extends ConfigModelBuilder<ContainerModel> { List<ApplicationContainer> nodes = new ArrayList<>(); for (Map.Entry<HostResource, ClusterMembership> entry : hosts.entrySet()) { String id = "container." + entry.getValue().index(); - ApplicationContainer container = new ApplicationContainer(cluster, id, entry.getValue().retired(), entry.getValue().index(), cluster.isHostedVespa()); + ApplicationContainer container = new ApplicationContainer(cluster, id, entry.getValue().retired(), entry.getValue().index(), cluster.isHostedVespa(), cluster.getTlsSecrets()); container.setHostResource(entry.getKey()); container.initService(deployLogger); nodes.add(container); diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerServiceBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerServiceBuilder.java index fd0797d6098..46271d3c0a2 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerServiceBuilder.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerServiceBuilder.java @@ -22,7 +22,7 @@ public class ContainerServiceBuilder extends VespaDomBuilder.DomConfigProducerBu @Override protected ApplicationContainer doBuild(DeployState deployState, AbstractConfigProducer parent, Element nodeElem) { - return new ApplicationContainer(parent, id, index, deployState.isHosted()); + return new ApplicationContainer(parent, id, index, deployState.isHosted(), deployState.tlsSecrets()); } } diff --git a/config-model/src/main/java/com/yahoo/vespa/model/content/Content.java b/config-model/src/main/java/com/yahoo/vespa/model/content/Content.java index 74caf2d8026..8eda707be99 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/content/Content.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/content/Content.java @@ -324,7 +324,7 @@ public class Content extends ConfigModel { if (!processedHosts.contains(host)) { String containerName = String.valueOf(searchNode.getDistributionKey()); ApplicationContainer docprocService = new ApplicationContainer(indexingCluster, containerName, index, - modelContext.getDeployState().isHosted()); + modelContext.getDeployState().isHosted(), modelContext.getDeployState().tlsSecrets()); index++; docprocService.useDynamicPorts(); docprocService.setHostResource(host); diff --git a/config-model/src/main/javacc/SDParser.jj b/config-model/src/main/javacc/SDParser.jj index 571ad452b01..6dde12f0fac 100644 --- a/config-model/src/main/javacc/SDParser.jj +++ b/config-model/src/main/javacc/SDParser.jj @@ -331,7 +331,7 @@ TOKEN : | < LOWERBOUND: "lower-bound" > | < UPPERBOUND: "upper-bound" > | < DENSEPOSTINGLISTTHRESHOLD: "dense-posting-list-threshold" > -| < EXPERIMENTALPOSTINGLISTFORMAT: "experimental-posting-list-format" > +| < ENABLE_BM25: "enable-bm25" > | < SUMMARYFEATURES_SL: "summary-features" (" ")* ":" (~["}","\n"])* ("\n")? > | < SUMMARYFEATURES_ML: "summary-features" (<SEARCHLIB_SKIP>)? "{" (~["}"])* "}" > | < RANKFEATURES_SL: "rank-features" (" ")* ":" (~["}","\n"])* ("\n")? > @@ -1782,7 +1782,7 @@ Object indexBody(IndexOperation index) : | <LOWERBOUND> <COLON> num = consumeLong() { index.setLowerBound(num); } | <UPPERBOUND> <COLON> num = consumeLong() { index.setUpperBound(num); } | <DENSEPOSTINGLISTTHRESHOLD> <COLON> threshold = consumeFloat() { index.setDensePostingListThreshold(threshold); } - | <EXPERIMENTALPOSTINGLISTFORMAT> { index.setExperimentalPostingListFormat(true); } + | <ENABLE_BM25> { index.setEnableBm25(true); } ) { return null; } } diff --git a/config-model/src/test/derived/indexschema/index-info.cfg b/config-model/src/test/derived/indexschema/index-info.cfg index 46c2c3fc307..a83ec45c5e9 100644 --- a/config-model/src/test/derived/indexschema/index-info.cfg +++ b/config-model/src/test/derived/indexschema/index-info.cfg @@ -133,15 +133,15 @@ indexinfo[].command[].indexname "exact2" indexinfo[].command[].command "lowercase" indexinfo[].command[].indexname "exact2" indexinfo[].command[].command "exact @@" -indexinfo[].command[].indexname "experimental" +indexinfo[].command[].indexname "bm25_field" indexinfo[].command[].command "index" -indexinfo[].command[].indexname "experimental" +indexinfo[].command[].indexname "bm25_field" indexinfo[].command[].command "lowercase" -indexinfo[].command[].indexname "experimental" +indexinfo[].command[].indexname "bm25_field" indexinfo[].command[].command "stem:BEST" -indexinfo[].command[].indexname "experimental" +indexinfo[].command[].indexname "bm25_field" indexinfo[].command[].command "normalize" -indexinfo[].command[].indexname "experimental" +indexinfo[].command[].indexname "bm25_field" indexinfo[].command[].command "plain-tokens" indexinfo[].command[].indexname "ia" indexinfo[].command[].command "index" diff --git a/config-model/src/test/derived/indexschema/indexschema.cfg b/config-model/src/test/derived/indexschema/indexschema.cfg index 998e53136a4..e8d064723da 100644 --- a/config-model/src/test/derived/indexschema/indexschema.cfg +++ b/config-model/src/test/derived/indexschema/indexschema.cfg @@ -78,7 +78,7 @@ indexfield[].phrases false indexfield[].positions true indexfield[].averageelementlen 512 indexfield[].interleavedfeatures false -indexfield[].name "experimental" +indexfield[].name "bm25_field" indexfield[].datatype STRING indexfield[].collectiontype SINGLE indexfield[].prefix false diff --git a/config-model/src/test/derived/indexschema/indexschema.sd b/config-model/src/test/derived/indexschema/indexschema.sd index 44956f30e9e..49f0f7dfca6 100644 --- a/config-model/src/test/derived/indexschema/indexschema.sd +++ b/config-model/src/test/derived/indexschema/indexschema.sd @@ -56,9 +56,9 @@ search indexschema { exact } } - field experimental type string { + field bm25_field type string { indexing: index - index: experimental-posting-list-format + index: enable-bm25 } # integer fields diff --git a/config-model/src/test/derived/indexschema/vsmfields.cfg b/config-model/src/test/derived/indexschema/vsmfields.cfg index 30ed67f61b7..9dcffd30313 100644 --- a/config-model/src/test/derived/indexschema/vsmfields.cfg +++ b/config-model/src/test/derived/indexschema/vsmfields.cfg @@ -55,7 +55,7 @@ fieldspec[].searchmethod AUTOUTF8 fieldspec[].arg1 "exact" fieldspec[].maxlength 1048576 fieldspec[].fieldtype INDEX -fieldspec[].name "experimental" +fieldspec[].name "bm25_field" fieldspec[].searchmethod AUTOUTF8 fieldspec[].arg1 "" fieldspec[].maxlength 1048576 @@ -138,8 +138,8 @@ documenttype[].index[].name "exact1" documenttype[].index[].field[].name "exact1" documenttype[].index[].name "exact2" documenttype[].index[].field[].name "exact2" -documenttype[].index[].name "experimental" -documenttype[].index[].field[].name "experimental" +documenttype[].index[].name "bm25_field" +documenttype[].index[].field[].name "bm25_field" documenttype[].index[].name "ia" documenttype[].index[].field[].name "ia" documenttype[].index[].name "ib" diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/ContainerClusterTest.java b/config-model/src/test/java/com/yahoo/vespa/model/container/ContainerClusterTest.java index ba7fbef439c..ac85a958ed5 100755 --- a/config-model/src/test/java/com/yahoo/vespa/model/container/ContainerClusterTest.java +++ b/config-model/src/test/java/com/yahoo/vespa/model/container/ContainerClusterTest.java @@ -5,6 +5,7 @@ import com.yahoo.cloud.config.ClusterInfoConfig; import com.yahoo.cloud.config.ConfigserverConfig; import com.yahoo.cloud.config.RoutingProviderConfig; import com.yahoo.config.application.api.DeployLogger; +import com.yahoo.config.model.api.TlsSecrets; import com.yahoo.config.model.deploy.DeployState; import com.yahoo.config.model.deploy.TestProperties; import com.yahoo.config.model.test.MockRoot; @@ -13,6 +14,7 @@ import com.yahoo.config.provision.RegionName; import com.yahoo.config.provision.SystemName; import com.yahoo.config.provision.Zone; import com.yahoo.container.handler.ThreadpoolConfig; +import com.yahoo.jdisc.http.ConnectorConfig; import com.yahoo.search.config.QrStartConfig; import com.yahoo.vespa.model.Host; import com.yahoo.vespa.model.HostResource; @@ -20,15 +22,22 @@ import com.yahoo.vespa.model.admin.clustercontroller.ClusterControllerContainer; import com.yahoo.vespa.model.admin.clustercontroller.ClusterControllerContainerCluster; import com.yahoo.vespa.model.container.component.Component; import com.yahoo.vespa.model.container.docproc.ContainerDocproc; +import com.yahoo.vespa.model.container.http.ConnectorFactory; import com.yahoo.vespa.model.container.search.ContainerSearch; import com.yahoo.vespa.model.container.search.searchchain.SearchChains; +import org.hamcrest.Matchers; import org.junit.Test; import java.util.Collection; import java.util.Iterator; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; /** * @author Simon Thoresen Hult @@ -210,9 +219,40 @@ public class ContainerClusterTest { assertEquals(0, cluster.getAllComponents().stream().map(c -> c.getClassId().getName()).filter(c -> c.equals("com.yahoo.jdisc.http.filter.security.RoutingConfigProvider")).count()); } + @Test + public void requireThatProvidingTlsSecretOpensPort4443() { + DeployState state = new DeployState.Builder().properties(new TestProperties().setHostedVespa(true).setTlsSecrets(Optional.of(new TlsSecrets("CERT", "KEY")))).build(); + MockRoot root = new MockRoot("foo", state); + ApplicationContainerCluster cluster = new ApplicationContainerCluster(root, "container0", "container1", state); + + addContainer(state.getDeployLogger(), cluster, "c1", "host-c1"); + Optional<ApplicationContainer> container = cluster.getContainers().stream().findFirst(); + assertTrue(container.isPresent()); + + var httpServer = (container.get().getHttp() == null) ? container.get().getDefaultHttpServer() : container.get().getHttp().getHttpServer(); + + // Verify that there are two connectors + List<ConnectorFactory> connectorFactories = httpServer.getConnectorFactories(); + assertEquals(2, connectorFactories.size()); + List<Integer> ports = connectorFactories.stream() + .map(ConnectorFactory::getListenPort) + .collect(Collectors.toList()); + assertThat(ports, Matchers.containsInAnyOrder(8080, 4443)); + + ConnectorFactory tlsPort = connectorFactories.stream().filter(connectorFactory -> connectorFactory.getListenPort() == 4443).findFirst().orElseThrow(); + + ConnectorConfig.Builder builder = new ConnectorConfig.Builder(); + tlsPort.getConfig(builder); + + ConnectorConfig connectorConfig = new ConnectorConfig(builder); + assertTrue(connectorConfig.ssl().enabled()); + assertEquals("CERT", connectorConfig.ssl().certificate()); + assertEquals("KEY", connectorConfig.ssl().privateKey()); + assertEquals(4443, connectorConfig.listenPort()); + } private static void addContainer(DeployLogger deployLogger, ApplicationContainerCluster cluster, String name, String hostName) { - ApplicationContainer container = new ApplicationContainer(cluster, name, 0, cluster.isHostedVespa()); + ApplicationContainer container = new ApplicationContainer(cluster, name, 0, cluster.isHostedVespa(), cluster.getTlsSecrets()); container.setHostResource(new HostResource(new Host(null, hostName))); container.initService(deployLogger); cluster.addContainer(container); diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/xml/JettyContainerModelBuilderTest.java b/config-model/src/test/java/com/yahoo/vespa/model/container/xml/JettyContainerModelBuilderTest.java index 03e115f0608..880cccf02e4 100644 --- a/config-model/src/test/java/com/yahoo/vespa/model/container/xml/JettyContainerModelBuilderTest.java +++ b/config-model/src/test/java/com/yahoo/vespa/model/container/xml/JettyContainerModelBuilderTest.java @@ -1,16 +1,19 @@ // 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.container.xml; +import com.yahoo.config.model.api.TlsSecrets; import com.yahoo.config.model.builder.xml.test.DomBuilderTest; +import com.yahoo.config.model.deploy.DeployState; +import com.yahoo.config.model.deploy.TestProperties; import com.yahoo.container.ComponentsConfig; import com.yahoo.container.jdisc.FilterBindingsProvider; import com.yahoo.jdisc.http.ConnectorConfig; -import com.yahoo.vespa.model.container.ContainerCluster; import com.yahoo.vespa.model.container.ApplicationContainerCluster; +import com.yahoo.vespa.model.container.ContainerCluster; import com.yahoo.vespa.model.container.component.SimpleComponent; import com.yahoo.vespa.model.container.http.ConnectorFactory; import com.yahoo.vespa.model.container.http.JettyHttpServer; -import com.yahoo.vespa.model.container.http.ssl.ConfiguredSslProvider; +import com.yahoo.vespa.model.container.http.ssl.ConfiguredFilebasedSslProvider; import org.junit.Test; import org.w3c.dom.Element; @@ -21,6 +24,7 @@ import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.CoreMatchers.nullValue; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; @@ -174,7 +178,7 @@ public class JettyContainerModelBuilderTest extends ContainerModelBuilderTestBas ContainerCluster cluster = (ContainerCluster) root.getChildren().get("default"); List<ConnectorFactory> connectorFactories = cluster.getChildrenByTypeRecursive(ConnectorFactory.class); - connectorFactories.forEach(connectorFactory -> assertChildComponentExists(connectorFactory, ConfiguredSslProvider.COMPONENT_CLASS)); + connectorFactories.forEach(connectorFactory -> assertChildComponentExists(connectorFactory, ConfiguredFilebasedSslProvider.COMPONENT_CLASS)); } @Test @@ -222,6 +226,37 @@ public class JettyContainerModelBuilderTest extends ContainerModelBuilderTestBas assertTrue(sslProvider.ssl().enabled()); } + @Test + public void verify_that_container_setup_additional_tls4443(){ + Element clusterElem = DomBuilderTest.parse( + "<jdisc id='default' version='1.0' jetty='true'>", + " <http>", + " <server port='9000' id='ssl'>", + " <ssl>", + " <private-key-file>/foo/key</private-key-file>", + " <certificate-file>/foo/cert</certificate-file>", + " </ssl>", + " </server>", + " </http>", + nodesXml, + "", + "</jdisc>"); + + DeployState deployState = new DeployState.Builder().properties(new TestProperties().setHostedVespa(true).setTlsSecrets(Optional.of(new TlsSecrets("CERT", "KEY")))).build(); + createModel(root, deployState, null, clusterElem); + ConnectorConfig sslProvider = root.getConfig(ConnectorConfig.class, "default/http/jdisc-jetty/ssl"); + assertTrue(sslProvider.ssl().enabled()); + assertEquals("", sslProvider.ssl().certificate()); + assertEquals("", sslProvider.ssl().privateKey()); + + ConnectorConfig providedTls = root.getConfig(ConnectorConfig.class, "default/http/jdisc-jetty/tls4443"); + assertTrue(providedTls.ssl().enabled()); + assertEquals("CERT", providedTls.ssl().certificate()); + assertEquals("KEY", providedTls.ssl().privateKey()); + assertEquals(4443, providedTls.listenPort()); + + } + private static void assertChildComponentExists(ConnectorFactory connectorFactory, String className) { Optional<SimpleComponent> simpleComponent = connectorFactory.getChildren().values().stream() .map(z -> (SimpleComponent) z) diff --git a/config-provisioning/src/main/java/com/yahoo/config/provision/zone/ZoneList.java b/config-provisioning/src/main/java/com/yahoo/config/provision/zone/ZoneList.java index 5f3f2e10898..776f925c424 100644 --- a/config-provisioning/src/main/java/com/yahoo/config/provision/zone/ZoneList.java +++ b/config-provisioning/src/main/java/com/yahoo/config/provision/zone/ZoneList.java @@ -1,10 +1,13 @@ // Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.config.provision.zone; +import com.google.common.collect.ImmutableList; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.RegionName; +import com.yahoo.config.provision.Zone; import java.util.List; +import java.util.stream.Collectors; /** * Provides filters for and access to a list of ZoneIds. @@ -32,7 +35,9 @@ public interface ZoneList extends ZoneFilter { /** Returns the ZoneApi of all zones in this list. */ List<? extends ZoneApi> zones(); - /** Returns the id of all zones in this list as — you guessed it — a list. */ - List<ZoneId> ids(); + /** Returns the ZoneIds of all zones in this list. */ + default List<ZoneId> ids() { + return zones().stream().map(ZoneApi::getId).collect(Collectors.toList()); + } } diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/GlobalComponentRegistry.java b/configserver/src/main/java/com/yahoo/vespa/config/server/GlobalComponentRegistry.java index d420c3f21fe..1eb18773898 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/GlobalComponentRegistry.java +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/GlobalComponentRegistry.java @@ -7,6 +7,7 @@ import com.yahoo.config.model.api.ConfigDefinitionRepo; import com.yahoo.config.provision.Provisioner; import com.yahoo.config.provision.TenantName; import com.yahoo.config.provision.Zone; +import com.yahoo.container.jdisc.secretstore.SecretStore; import com.yahoo.vespa.config.server.application.PermanentApplicationPackage; import com.yahoo.vespa.config.server.host.HostRegistries; import com.yahoo.vespa.config.server.modelfactory.ModelFactoryRegistry; @@ -46,4 +47,5 @@ public interface GlobalComponentRegistry { StripedExecutor<TenantName> getZkWatcherExecutor(); FlagSource getFlagSource(); ExecutorService getZkCacheExecutor(); + SecretStore getSecretStore(); } diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/InjectedGlobalComponentRegistry.java b/configserver/src/main/java/com/yahoo/vespa/config/server/InjectedGlobalComponentRegistry.java index ff76afd1c98..9badd19009f 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/InjectedGlobalComponentRegistry.java +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/InjectedGlobalComponentRegistry.java @@ -9,6 +9,7 @@ import com.yahoo.config.model.api.ConfigDefinitionRepo; import com.yahoo.config.provision.Provisioner; import com.yahoo.config.provision.TenantName; import com.yahoo.config.provision.Zone; +import com.yahoo.container.jdisc.secretstore.SecretStore; import com.yahoo.vespa.config.server.application.PermanentApplicationPackage; import com.yahoo.vespa.config.server.host.HostRegistries; import com.yahoo.vespa.config.server.modelfactory.ModelFactoryRegistry; @@ -48,6 +49,7 @@ public class InjectedGlobalComponentRegistry implements GlobalComponentRegistry private final Zone zone; private final ConfigServerDB configServerDB; private final FlagSource flagSource; + private final SecretStore secretStore; private final StripedExecutor<TenantName> zkWatcherExecutor; private final ExecutorService zkCacheExecutor; @@ -67,7 +69,8 @@ public class InjectedGlobalComponentRegistry implements GlobalComponentRegistry HostProvisionerProvider hostProvisionerProvider, Zone zone, ConfigServerDB configServerDB, - FlagSource flagSource) { + FlagSource flagSource, + SecretStore secretStore) { this.curator = curator; this.configCurator = configCurator; this.metrics = metrics; @@ -82,6 +85,7 @@ public class InjectedGlobalComponentRegistry implements GlobalComponentRegistry this.zone = zone; this.configServerDB = configServerDB; this.flagSource = flagSource; + this.secretStore = secretStore; this.zkWatcherExecutor = new StripedExecutor<>(); this.zkCacheExecutor = Executors.newFixedThreadPool(1, ThreadFactoryFactory.getThreadFactory(TenantRepository.class.getName())); } @@ -137,4 +141,9 @@ public class InjectedGlobalComponentRegistry implements GlobalComponentRegistry public ExecutorService getZkCacheExecutor() { return zkCacheExecutor; } + + @Override + public SecretStore getSecretStore() { + return secretStore; + } } diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/ModelContextImpl.java b/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/ModelContextImpl.java index 4627d350eb2..d875385d14d 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/ModelContextImpl.java +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/ModelContextImpl.java @@ -11,6 +11,7 @@ import com.yahoo.config.model.api.ContainerEndpoint; import com.yahoo.config.model.api.HostProvisioner; import com.yahoo.config.model.api.Model; import com.yahoo.config.model.api.ModelContext; +import com.yahoo.config.model.api.TlsSecrets; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.HostName; import com.yahoo.config.provision.Rotation; @@ -134,6 +135,7 @@ public class ModelContextImpl implements ModelContext { private final boolean useFdispatchByDefault; private final boolean useAdaptiveDispatch; private final boolean dispatchWithProtobuf; + private final Optional<TlsSecrets> tlsSecrets; public Properties(ApplicationId applicationId, boolean multitenantFromConfig, @@ -147,7 +149,8 @@ public class ModelContextImpl implements ModelContext { Set<ContainerEndpoint> endpoints, boolean isBootstrap, boolean isFirstTimeDeployment, - FlagSource flagSource) { + FlagSource flagSource, + Optional<TlsSecrets> tlsSecrets) { this.applicationId = applicationId; this.multitenant = multitenantFromConfig || hostedVespa || Boolean.getBoolean("multitenant"); this.configServerSpecs = configServerSpecs; @@ -168,6 +171,7 @@ public class ModelContextImpl implements ModelContext { .with(FetchVector.Dimension.APPLICATION_ID, applicationId.serializedForm()).value(); this.useAdaptiveDispatch = Flags.USE_ADAPTIVE_DISPATCH.bindTo(flagSource) .with(FetchVector.Dimension.APPLICATION_ID, applicationId.serializedForm()).value(); + this.tlsSecrets = tlsSecrets; } @Override @@ -222,6 +226,8 @@ public class ModelContextImpl implements ModelContext { @Override public boolean useAdaptiveDispatch() { return useAdaptiveDispatch; } + @Override + public Optional<TlsSecrets> tlsSecrets() { return tlsSecrets; } } } diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ActivatedModelsBuilder.java b/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ActivatedModelsBuilder.java index 117a9e0cac5..94cd30de28b 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ActivatedModelsBuilder.java +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ActivatedModelsBuilder.java @@ -13,6 +13,7 @@ import com.yahoo.config.provision.AllocatedHosts; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.HostName; import com.yahoo.config.provision.TenantName; +import com.yahoo.container.jdisc.secretstore.SecretStore; import com.yahoo.log.LogLevel; import com.yahoo.vespa.config.server.ConfigServerSpec; import com.yahoo.vespa.config.server.GlobalComponentRegistry; @@ -28,6 +29,7 @@ import com.yahoo.vespa.config.server.session.SilentDeployLogger; import com.yahoo.vespa.config.server.tenant.ContainerEndpointsCache; import com.yahoo.vespa.config.server.tenant.Rotations; import com.yahoo.vespa.config.server.tenant.TenantRepository; +import com.yahoo.vespa.config.server.tenant.TlsSecretsKeys; import com.yahoo.vespa.curator.Curator; import com.yahoo.vespa.flags.FlagSource; @@ -55,6 +57,7 @@ public class ActivatedModelsBuilder extends ModelsBuilder<Application> { private final Curator curator; private final DeployLogger logger; private final FlagSource flagSource; + private final SecretStore secretStore; public ActivatedModelsBuilder(TenantName tenant, long appGeneration, @@ -73,6 +76,7 @@ public class ActivatedModelsBuilder extends ModelsBuilder<Application> { this.curator = globalComponentRegistry.getCurator(); this.logger = new SilentDeployLogger(); this.flagSource = globalComponentRegistry.getFlagSource(); + this.secretStore = globalComponentRegistry.getSecretStore(); } @Override @@ -132,7 +136,8 @@ public class ActivatedModelsBuilder extends ModelsBuilder<Application> { ImmutableSet.copyOf(new ContainerEndpointsCache(TenantRepository.getTenantPath(tenant), curator).read(applicationId)), false, // We may be bootstrapping, but we only know and care during prepare false, // Always false, assume no one uses it when activating - flagSource); + flagSource, + new TlsSecretsKeys(curator, TenantRepository.getTenantPath(tenant), secretStore).readTlsSecretsKeyFromZookeeper(applicationId)); } } diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/PrepareParams.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/PrepareParams.java index 00a7625ee87..5bf70c55f9e 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/session/PrepareParams.java +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/PrepareParams.java @@ -35,6 +35,7 @@ public final class PrepareParams { static final String VESPA_VERSION_PARAM_NAME = "vespaVersion"; static final String ROTATIONS_PARAM_NAME = "rotations"; static final String CONTAINER_ENDPOINTS_PARAM_NAME = "containerEndpoints"; + static final String TLS_SECRETS_KEY_NAME_PARAM_NAME = "tlsSecretsKeyName"; private final ApplicationId applicationId; private final TimeoutBudget timeoutBudget; @@ -45,10 +46,11 @@ public final class PrepareParams { private final Optional<Version> vespaVersion; private final Set<Rotation> rotations; private final List<ContainerEndpoint> containerEndpoints; + private final Optional<String> tlsSecretsKeyName; private PrepareParams(ApplicationId applicationId, TimeoutBudget timeoutBudget, boolean ignoreValidationErrors, - boolean dryRun, boolean verbose, boolean isBootstrap, Optional<Version> vespaVersion, - Set<Rotation> rotations, List<ContainerEndpoint> containerEndpoints) { + boolean dryRun, boolean verbose, boolean isBootstrap, Optional<Version> vespaVersion, Set<Rotation> rotations, + List<ContainerEndpoint> containerEndpoints, Optional<String> tlsSecretsKeyName) { this.timeoutBudget = timeoutBudget; this.applicationId = applicationId; this.ignoreValidationErrors = ignoreValidationErrors; @@ -61,6 +63,7 @@ public final class PrepareParams { if ((rotations != null && !rotations.isEmpty()) && !containerEndpoints.isEmpty()) { throw new IllegalArgumentException("Cannot set both rotations and containerEndpoints"); } + this.tlsSecretsKeyName = tlsSecretsKeyName; } public static class Builder { @@ -74,6 +77,7 @@ public final class PrepareParams { private Optional<Version> vespaVersion = Optional.empty(); private Set<Rotation> rotations; private List<ContainerEndpoint> containerEndpoints = List.of(); + private Optional<String> tlsSecretsKeyName = Optional.empty(); public Builder() { } @@ -136,12 +140,18 @@ public final class PrepareParams { if (serialized == null) return this; Slime slime = SlimeUtils.jsonToSlime(serialized); containerEndpoints = ContainerEndpointSerializer.endpointListFromSlime(slime); + return this; + } + + public Builder tlsSecretsKeyName(String tlsSecretsKeyName) { + this.tlsSecretsKeyName = Optional.ofNullable(tlsSecretsKeyName) + .filter(s -> ! s.isEmpty()); return this; } public PrepareParams build() { return new PrepareParams(applicationId, timeoutBudget, ignoreValidationErrors, dryRun, - verbose, isBootstrap, vespaVersion, rotations, containerEndpoints); + verbose, isBootstrap, vespaVersion, rotations, containerEndpoints, tlsSecretsKeyName); } } @@ -155,6 +165,7 @@ public final class PrepareParams { .vespaVersion(request.getProperty(VESPA_VERSION_PARAM_NAME)) .rotations(request.getProperty(ROTATIONS_PARAM_NAME)) .containerEndpoints(request.getProperty(CONTAINER_ENDPOINTS_PARAM_NAME)) + .tlsSecretsKeyName(request.getProperty(TLS_SECRETS_KEY_NAME_PARAM_NAME)) .build(); } @@ -212,4 +223,7 @@ public final class PrepareParams { return timeoutBudget; } + public Optional<String> tlsSecretsKeyName() { + return tlsSecretsKeyName; + } } diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionPreparer.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionPreparer.java index 30ba9989343..54c96c0461d 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionPreparer.java +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionPreparer.java @@ -13,11 +13,13 @@ import com.yahoo.config.application.api.DeploymentSpec; import com.yahoo.config.application.api.FileRegistry; import com.yahoo.config.model.api.ConfigDefinitionRepo; import com.yahoo.config.model.api.ModelContext; +import com.yahoo.config.model.api.TlsSecrets; import com.yahoo.config.provision.AllocatedHosts; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.HostName; import com.yahoo.config.provision.Rotation; import com.yahoo.config.provision.Zone; +import com.yahoo.container.jdisc.secretstore.SecretStore; import com.yahoo.lang.SettableOptional; import com.yahoo.log.LogLevel; import com.yahoo.path.Path; @@ -34,6 +36,7 @@ import com.yahoo.vespa.config.server.provision.HostProvisionerProvider; import com.yahoo.config.model.api.ContainerEndpoint; import com.yahoo.vespa.config.server.tenant.ContainerEndpointsCache; import com.yahoo.vespa.config.server.tenant.Rotations; +import com.yahoo.vespa.config.server.tenant.TlsSecretsKeys; import com.yahoo.vespa.curator.Curator; import com.yahoo.vespa.flags.FlagSource; import org.xml.sax.SAXException; @@ -69,6 +72,7 @@ public class SessionPreparer { private final Curator curator; private final Zone zone; private final FlagSource flagSource; + private final SecretStore secretStore; @Inject public SessionPreparer(ModelFactoryRegistry modelFactoryRegistry, @@ -79,7 +83,8 @@ public class SessionPreparer { ConfigDefinitionRepo configDefinitionRepo, Curator curator, Zone zone, - FlagSource flagSource) { + FlagSource flagSource, + SecretStore secretStore) { this.modelFactoryRegistry = modelFactoryRegistry; this.fileDistributionFactory = fileDistributionFactory; this.hostProvisionerProvider = hostProvisionerProvider; @@ -89,6 +94,7 @@ public class SessionPreparer { this.curator = curator; this.zone = zone; this.flagSource = flagSource; + this.secretStore = secretStore; } /** @@ -112,6 +118,7 @@ public class SessionPreparer { if ( ! params.isDryRun()) { preparation.writeStateZK(); preparation.writeRotZK(); + preparation.writeTlsZK(); var globalServiceId = context.getApplicationPackage().getDeployment() .map(DeploymentSpec::fromXml) .flatMap(DeploymentSpec::globalServiceId); @@ -145,6 +152,8 @@ public class SessionPreparer { final Set<Rotation> rotationsSet; final Set<ContainerEndpoint> endpointsSet; final ModelContext.Properties properties; + private final TlsSecretsKeys tlsSecretsKeys; + private final Optional<TlsSecrets> tlsSecrets; private ApplicationPackage applicationPackage; private List<PreparedModelsBuilder.PreparedModelResult> modelResultList; @@ -165,7 +174,10 @@ public class SessionPreparer { this.rotations = new Rotations(curator, tenantPath); this.containerEndpoints = new ContainerEndpointsCache(tenantPath, curator); this.rotationsSet = getRotations(params.rotations()); + this.tlsSecretsKeys = new TlsSecretsKeys(curator, tenantPath, secretStore); + this.tlsSecrets = tlsSecretsKeys.getTlsSecrets(params.tlsSecretsKeyName(), applicationId); this.endpointsSet = getEndpoints(params.containerEndpoints()); + this.properties = new ModelContextImpl.Properties(params.getApplicationId(), configserverConfig.multitenant(), ConfigServerSpec.fromConfig(configserverConfig), @@ -178,7 +190,8 @@ public class SessionPreparer { endpointsSet, params.isBootstrap(), ! currentActiveApplicationSet.isPresent(), - context.getFlagSource()); + context.getFlagSource(), + tlsSecrets); this.preparedModelsBuilder = new PreparedModelsBuilder(modelFactoryRegistry, permanentApplicationPackage, configDefinitionRepo, @@ -238,6 +251,11 @@ public class SessionPreparer { checkTimeout("write rotations to zookeeper"); } + void writeTlsZK() { + tlsSecretsKeys.writeTlsSecretsKeyToZooKeeper(applicationId, params.tlsSecretsKeyName().orElse(null)); + checkTimeout("write tlsSecretsKey to zookeeper"); + } + void writeContainerEndpointsZK(Optional<String> globalServiceId) { if (!params.containerEndpoints().isEmpty()) { // Use endpoints from parameter when explicitly given containerEndpoints.write(applicationId, params.containerEndpoints()); diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/tenant/TlsSecretsKeys.java b/configserver/src/main/java/com/yahoo/vespa/config/server/tenant/TlsSecretsKeys.java new file mode 100644 index 00000000000..eaa4916d8fc --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/tenant/TlsSecretsKeys.java @@ -0,0 +1,86 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.tenant; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.yahoo.config.model.api.TlsSecrets; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.container.jdisc.secretstore.SecretStore; +import com.yahoo.path.Path; +import com.yahoo.vespa.curator.Curator; +import com.yahoo.vespa.curator.transaction.CuratorOperations; +import com.yahoo.vespa.curator.transaction.CuratorTransaction; + +import java.util.Optional; + +/** + * TLS Secret keys for applications (used to retrieve actual certificate/key from secret store). Persisted in ZooKeeper. + * + * @author andreer + */ +public class TlsSecretsKeys { + + private final Path path; + private final SecretStore secretStore; + private final Curator curator; + + public TlsSecretsKeys(Curator curator, Path tenantPath, SecretStore secretStore) { + this.curator = curator; + this.path = tenantPath.append("tlsSecretsKeys/"); + this.secretStore = secretStore; + } + + public Optional<TlsSecrets> readTlsSecretsKeyFromZookeeper(ApplicationId application) { + try { + Optional<byte[]> data = curator.getData(tlsSecretsKeyOf(application)); + if (data.isEmpty() || data.get().length == 0) return Optional.empty(); + String tlsSecretsKey = new ObjectMapper().readValue(data.get(), new TypeReference<String>() {}); + return readFromSecretStore(Optional.ofNullable(tlsSecretsKey)); + } catch (Exception e) { + throw new RuntimeException("Error reading TLS secret key of " + application, e); + } + } + + public void writeTlsSecretsKeyToZooKeeper(ApplicationId application, String tlsSecretsKey) { + if (tlsSecretsKey == null) return; + try { + byte[] data = new ObjectMapper().writeValueAsBytes(tlsSecretsKey); + curator.set(tlsSecretsKeyOf(application), data); + } catch (Exception e) { + throw new RuntimeException("Could not write TLS secret key of " + application, e); + } + } + + public Optional<TlsSecrets> getTlsSecrets(Optional<String> secretKeyname, ApplicationId applicationId) { + if (secretKeyname == null || secretKeyname.isEmpty()) { + return readTlsSecretsKeyFromZookeeper(applicationId); + } + return readFromSecretStore(secretKeyname); + } + + private Optional<TlsSecrets> readFromSecretStore(Optional<String> secretKeyname) { + if(secretKeyname.isEmpty()) return Optional.empty(); + TlsSecrets tlsSecretParameters = TlsSecrets.MISSING; + try { + String cert = secretStore.getSecret(secretKeyname.get() + "-cert"); + String key = secretStore.getSecret(secretKeyname.get() + "-key"); + tlsSecretParameters = new TlsSecrets(cert, key); + } catch (RuntimeException e) { + // Assume not ready yet +// log.log(LogLevel.DEBUG, "Could not fetch certificate/key with prefix: " + secretKeyname.get(), e); + } + return Optional.of(tlsSecretParameters); + } + + /** Returns a transaction which deletes these tls secrets key if they exist */ + public CuratorTransaction delete(ApplicationId application) { + if (!curator.exists(tlsSecretsKeyOf(application))) return CuratorTransaction.empty(curator); + return CuratorTransaction.from(CuratorOperations.delete(tlsSecretsKeyOf(application).getAbsolute()), curator); + } + + /** Returns the path storing the tls secrets key for an application */ + private Path tlsSecretsKeyOf(ApplicationId application) { + return path.append(application.serializedForm()); + } + +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/InjectedGlobalComponentRegistryTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/InjectedGlobalComponentRegistryTest.java index 9b113cae715..e4ff8702ff1 100644 --- a/configserver/src/test/java/com/yahoo/vespa/config/server/InjectedGlobalComponentRegistryTest.java +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/InjectedGlobalComponentRegistryTest.java @@ -78,7 +78,7 @@ public class InjectedGlobalComponentRegistryTest { globalComponentRegistry = new InjectedGlobalComponentRegistry(curator, configCurator, metrics, modelFactoryRegistry, sessionPreparer, rpcServer, configserverConfig, generationCounter, defRepo, permanentApplicationPackage, hostRegistries, hostProvisionerProvider, zone, - new ConfigServerDB(configserverConfig), new InMemoryFlagSource()); + new ConfigServerDB(configserverConfig), new InMemoryFlagSource(), new MockSecretStore()); } @Test diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/MockSecretStore.java b/configserver/src/test/java/com/yahoo/vespa/config/server/MockSecretStore.java new file mode 100644 index 00000000000..8a77b53875e --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/MockSecretStore.java @@ -0,0 +1,35 @@ +// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import com.yahoo.container.jdisc.secretstore.SecretStore; + +import java.util.HashMap; +import java.util.Map; + +public class MockSecretStore implements SecretStore { + Map<String, String> secrets = new HashMap<>(); + + @Override + public String getSecret(String key) { + if(secrets.containsKey(key)) + return secrets.get(key); + throw new RuntimeException("Key not found: " + key); + } + + @Override + public String getSecret(String key, int version) { + return getSecret(key); + } + + public void put(String key, String value) { + secrets.put(key, value); + } + + public void remove(String key) { + secrets.remove(key); + } + + public void clear() { + secrets.clear(); + } +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/ModelContextImplTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/ModelContextImplTest.java index b483705e3f5..860bbdc134c 100644 --- a/configserver/src/test/java/com/yahoo/vespa/config/server/ModelContextImplTest.java +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/ModelContextImplTest.java @@ -62,7 +62,8 @@ public class ModelContextImplTest { endpoints, false, false, - flagSource), + flagSource, + null), Optional.empty(), new Version(6), new Version(6)); diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/TestComponentRegistry.java b/configserver/src/test/java/com/yahoo/vespa/config/server/TestComponentRegistry.java index 62685734a47..a304f74858b 100644 --- a/configserver/src/test/java/com/yahoo/vespa/config/server/TestComponentRegistry.java +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/TestComponentRegistry.java @@ -5,12 +5,12 @@ import com.google.common.io.Files; import com.yahoo.cloud.config.ConfigserverConfig; import com.yahoo.concurrent.InThreadExecutorService; import com.yahoo.concurrent.StripedExecutor; -import com.yahoo.concurrent.ThreadFactoryFactory; import com.yahoo.config.model.NullConfigModelRegistry; import com.yahoo.config.model.api.ConfigDefinitionRepo; import com.yahoo.config.provision.Provisioner; import com.yahoo.config.provision.TenantName; import com.yahoo.config.provision.Zone; +import com.yahoo.container.jdisc.secretstore.SecretStore; import com.yahoo.vespa.config.server.application.PermanentApplicationPackage; import com.yahoo.vespa.config.server.host.HostRegistries; import com.yahoo.vespa.config.server.modelfactory.ModelFactoryRegistry; @@ -21,7 +21,6 @@ import com.yahoo.vespa.config.server.session.MockFileDistributionFactory; import com.yahoo.vespa.config.server.session.SessionPreparer; import com.yahoo.vespa.config.server.tenant.MockTenantListener; import com.yahoo.vespa.config.server.tenant.TenantListener; -import com.yahoo.vespa.config.server.tenant.TenantRepository; import com.yahoo.vespa.config.server.tenant.TenantRequestHandlerTest; import com.yahoo.vespa.config.server.zookeeper.ConfigCurator; import com.yahoo.vespa.curator.Curator; @@ -34,7 +33,6 @@ import java.time.Clock; import java.util.Collections; import java.util.Optional; import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; /** @@ -60,6 +58,7 @@ public class TestComponentRegistry implements GlobalComponentRegistry { private final ConfigServerDB configServerDB; private final StripedExecutor<TenantName> zkWatcherExecutor; private final ExecutorService zkCacheExecutor; + private final SecretStore secretStore; private TestComponentRegistry(Curator curator, ConfigCurator configCurator, Metrics metrics, ModelFactoryRegistry modelFactoryRegistry, @@ -73,7 +72,8 @@ public class TestComponentRegistry implements GlobalComponentRegistry { ReloadListener reloadListener, TenantListener tenantListener, Zone zone, - Clock clock) { + Clock clock, + SecretStore secretStore) { this.curator = curator; this.configCurator = configCurator; this.metrics = metrics; @@ -92,6 +92,7 @@ public class TestComponentRegistry implements GlobalComponentRegistry { this.configServerDB = new ConfigServerDB(configserverConfig); this.zkWatcherExecutor = new StripedExecutor<>(new InThreadExecutorService()); this.zkCacheExecutor = new InThreadExecutorService(); + this.secretStore = secretStore; } public static class Builder { @@ -161,14 +162,15 @@ public class TestComponentRegistry implements GlobalComponentRegistry { .orElse(new MockFileDistributionFactory(configserverConfig)); HostProvisionerProvider hostProvisionerProvider = hostProvisioner. map(HostProvisionerProvider::withProvisioner).orElseGet(HostProvisionerProvider::empty); + SecretStore secretStore = new MockSecretStore(); SessionPreparer sessionPreparer = new SessionPreparer(modelFactoryRegistry, fileDistributionFactory, hostProvisionerProvider, permApp, configserverConfig, defRepo, curator, - zone, new InMemoryFlagSource()); + zone, new InMemoryFlagSource(), secretStore); return new TestComponentRegistry(curator, ConfigCurator.create(curator), metrics, modelFactoryRegistry, permApp, fileDistributionFactory, hostRegistries, configserverConfig, sessionPreparer, hostProvisioner, defRepo, reloadListener, tenantListener, - zone, clock); + zone, clock, secretStore); } } @@ -220,6 +222,11 @@ public class TestComponentRegistry implements GlobalComponentRegistry { return zkCacheExecutor; } + @Override + public SecretStore getSecretStore() { + return secretStore; + } + public FileDistributionFactory getFileDistributionFactory() { return fileDistributionFactory; } } diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionPreparerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionPreparerTest.java index 74415993c52..88baf1b8d74 100644 --- a/configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionPreparerTest.java +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionPreparerTest.java @@ -4,6 +4,7 @@ package com.yahoo.vespa.config.server.session; import com.yahoo.component.Version; import com.yahoo.config.application.api.DeployLogger; import com.yahoo.config.model.api.ModelContext; +import com.yahoo.config.model.api.TlsSecrets; import com.yahoo.config.model.application.provider.BaseDeployLogger; import com.yahoo.config.model.application.provider.FilesApplicationPackage; import com.yahoo.config.provision.ApplicationId; @@ -16,6 +17,7 @@ import com.yahoo.log.LogLevel; import com.yahoo.path.Path; import com.yahoo.slime.Slime; import com.yahoo.vespa.config.server.MockReloadHandler; +import com.yahoo.vespa.config.server.MockSecretStore; import com.yahoo.vespa.config.server.TestComponentRegistry; import com.yahoo.vespa.config.server.TimeoutBudgetTest; import com.yahoo.vespa.config.server.application.PermanentApplicationPackage; @@ -29,6 +31,7 @@ import com.yahoo.vespa.config.server.provision.HostProvisionerProvider; import com.yahoo.config.model.api.ContainerEndpoint; import com.yahoo.vespa.config.server.tenant.ContainerEndpointsCache; import com.yahoo.vespa.config.server.tenant.Rotations; +import com.yahoo.vespa.config.server.tenant.TlsSecretsKeys; import com.yahoo.vespa.config.server.zookeeper.ConfigCurator; import com.yahoo.vespa.curator.mock.MockCurator; import com.yahoo.vespa.flags.InMemoryFlagSource; @@ -71,7 +74,7 @@ public class SessionPreparerTest { private SessionPreparer preparer; private TestComponentRegistry componentRegistry; private MockFileDistributionFactory fileDistributionFactory; - + private MockSecretStore secretStore = new MockSecretStore(); @Rule public TemporaryFolder folder = new TemporaryFolder(); @@ -106,7 +109,8 @@ public class SessionPreparerTest { componentRegistry.getStaticConfigDefinitionRepo(), curator, componentRegistry.getZone(), - flagSource); + flagSource, + secretStore); } @Test(expected = InvalidApplicationException.class) @@ -256,6 +260,49 @@ public class SessionPreparerTest { assertEquals(expected, readContainerEndpoints(applicationId)); } + @Test + public void require_that_tlssecretkey_is_written() throws IOException { + var tlskey = "vespa.tlskeys.tenant1--app1"; + var applicationId = applicationId("test"); + var params = new PrepareParams.Builder().applicationId(applicationId).tlsSecretsKeyName(tlskey).build(); + secretStore.put(tlskey+"-cert", "CERT"); + secretStore.put(tlskey+"-key", "KEY"); + prepare(new File("src/test/resources/deploy/hosted-app"), params); + + // Read from zk and verify cert and key are available + Optional<TlsSecrets> tlsSecrets = new TlsSecretsKeys(curator, tenantPath, secretStore).readTlsSecretsKeyFromZookeeper(applicationId); + assertTrue(tlsSecrets.isPresent()); + assertEquals("KEY", tlsSecrets.get().key()); + assertEquals("CERT", tlsSecrets.get().certificate()); + } + + @Test + public void require_that_tlssecretkey_is_missing_when_not_in_secretstore() throws IOException { + var tlskey = "vespa.tlskeys.tenant1--app1"; + var applicationId = applicationId("test"); + var params = new PrepareParams.Builder().applicationId(applicationId).tlsSecretsKeyName(tlskey).build(); + prepare(new File("src/test/resources/deploy/hosted-app"), params); + + // Read from zk and verify key/cert is missing + Optional<TlsSecrets> tlsSecrets = new TlsSecretsKeys(curator, tenantPath, secretStore).readTlsSecretsKeyFromZookeeper(applicationId); + assertTrue(tlsSecrets.isPresent()); + assertTrue(tlsSecrets.get().isMissing()); + } + + @Test + public void require_that_tlssecretkey_is_missing_when_certificate_not_in_secretstore() throws IOException { + var tlskey = "vespa.tlskeys.tenant1--app1"; + var applicationId = applicationId("test"); + var params = new PrepareParams.Builder().applicationId(applicationId).tlsSecretsKeyName(tlskey).build(); + secretStore.put(tlskey+"-key", "KEY"); + prepare(new File("src/test/resources/deploy/hosted-app"), params); + + // Read from zk and verify key/cert is missing + Optional<TlsSecrets> tlsSecrets = new TlsSecretsKeys(curator, tenantPath, secretStore).readTlsSecretsKeyFromZookeeper(applicationId); + assertTrue(tlsSecrets.isPresent()); + assertTrue(tlsSecrets.get().isMissing()); + } + private void prepare(File app) throws IOException { prepare(app, new PrepareParams.Builder().build()); } diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionTest.java index 95f6c7718e2..b2ad0af8f9a 100644 --- a/configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionTest.java +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionTest.java @@ -21,7 +21,7 @@ public class SessionTest { public boolean isPrepared = false; public MockSessionPreparer() { - super(null, null, null, null, null, null, new MockCurator(), null, null); + super(null, null, null, null, null, null, new MockCurator(), null, null, null); } @Override diff --git a/container-disc/src/main/java/com/yahoo/container/jdisc/DeprecatedSecretStoreProvider.java b/container-disc/src/main/java/com/yahoo/container/jdisc/DeprecatedSecretStoreProvider.java new file mode 100644 index 00000000000..0f47bfe2eb1 --- /dev/null +++ b/container-disc/src/main/java/com/yahoo/container/jdisc/DeprecatedSecretStoreProvider.java @@ -0,0 +1,34 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.jdisc; + +import com.yahoo.container.di.componentgraph.Provider; + +/** + * An secret store provider which provides a factory which throws exception on + * invocation - as no secret store is currently provided by default. + * The purpose of this is to provide a secret store for injection in the case where + * no secret store component is provided. + * + * @author bratseth + */ +@SuppressWarnings({"deprecation", "unused"}) +public class DeprecatedSecretStoreProvider implements Provider<com.yahoo.jdisc.http.SecretStore> { + + private static final ThrowingSecretStore instance = new ThrowingSecretStore(); + + @Override + public com.yahoo.jdisc.http.SecretStore get() { return instance; } + + @Override + public void deconstruct() { } + + private static final class ThrowingSecretStore implements com.yahoo.jdisc.http.SecretStore { + + @Override + public String getSecret(String key) { + throw new UnsupportedOperationException("A secret store is not available"); + } + + } + +} diff --git a/container-disc/src/main/java/com/yahoo/container/jdisc/SecretStoreProvider.java b/container-disc/src/main/java/com/yahoo/container/jdisc/SecretStoreProvider.java index d966e66f502..6012fbe394c 100644 --- a/container-disc/src/main/java/com/yahoo/container/jdisc/SecretStoreProvider.java +++ b/container-disc/src/main/java/com/yahoo/container/jdisc/SecretStoreProvider.java @@ -1,34 +1,29 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.container.jdisc; +import com.yahoo.container.jdisc.secretstore.SecretStore; import com.yahoo.container.di.componentgraph.Provider; -/** - * An secret store provider which provides a factory which throws exception on - * invocation - as no secret store is currently provided by default. - * The purpose of this is to provide a secret store for injection in the case where - * no secret store component is provided. - * - * @author bratseth - */ -@SuppressWarnings({"deprecation", "unused"}) -public class SecretStoreProvider implements Provider<com.yahoo.jdisc.http.SecretStore> { +public class SecretStoreProvider implements Provider<SecretStore> { private static final ThrowingSecretStore instance = new ThrowingSecretStore(); @Override - public com.yahoo.jdisc.http.SecretStore get() { return instance; } + public SecretStore get() { return instance; } @Override public void deconstruct() { } - private static final class ThrowingSecretStore implements com.yahoo.jdisc.http.SecretStore { + private static final class ThrowingSecretStore implements SecretStore { @Override public String getSecret(String key) { throw new UnsupportedOperationException("A secret store is not available"); } + @Override + public String getSecret(String key, int version) { + throw new UnsupportedOperationException("A secret store is not available"); + } } - } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Application.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Application.java index b4e3b8b1a9a..c168d09a15a 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Application.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Application.java @@ -15,13 +15,12 @@ import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationV import com.yahoo.vespa.hosted.controller.api.integration.organization.IssueId; import com.yahoo.vespa.hosted.controller.api.integration.organization.User; import com.yahoo.vespa.hosted.controller.application.ApplicationActivity; +import com.yahoo.vespa.hosted.controller.application.AssignedRotation; import com.yahoo.vespa.hosted.controller.application.Change; import com.yahoo.vespa.hosted.controller.application.Deployment; import com.yahoo.vespa.hosted.controller.application.DeploymentJobs; -import com.yahoo.vespa.hosted.controller.application.Endpoint; import com.yahoo.vespa.hosted.controller.application.EndpointList; import com.yahoo.vespa.hosted.controller.application.RotationStatus; -import com.yahoo.vespa.hosted.controller.rotation.Rotation; import com.yahoo.vespa.hosted.controller.rotation.RotationId; import java.time.Instant; @@ -58,8 +57,7 @@ public class Application { private final OptionalInt majorVersion; private final ApplicationMetrics metrics; private final Optional<String> pemDeployKey; - private final Optional<RotationId> legacyRotation; - private final List<RotationId> rotations; + private final List<AssignedRotation> rotations; private final Map<HostName, RotationStatus> rotationStatus; /** Creates an empty application */ @@ -68,7 +66,7 @@ public class Application { new DeploymentJobs(OptionalLong.empty(), Collections.emptyList(), Optional.empty(), false), Change.empty(), Change.empty(), Optional.empty(), Optional.empty(), OptionalInt.empty(), new ApplicationMetrics(0, 0), - Optional.empty(), Optional.empty(), Collections.emptyList(), Collections.emptyMap()); + Optional.empty(), Collections.emptyList(), Collections.emptyMap()); } /** Used from persistence layer: Do not use */ @@ -76,18 +74,18 @@ public class Application { List<Deployment> deployments, DeploymentJobs deploymentJobs, Change change, Change outstandingChange, Optional<IssueId> ownershipIssueId, Optional<User> owner, OptionalInt majorVersion, ApplicationMetrics metrics, Optional<String> pemDeployKey, - Optional<RotationId> legacyRotation, List<RotationId> rotations, Map<HostName, RotationStatus> rotationStatus) { + List<AssignedRotation> rotations, Map<HostName, RotationStatus> rotationStatus) { this(id, createdAt, deploymentSpec, validationOverrides, deployments.stream().collect(Collectors.toMap(Deployment::zone, Function.identity())), deploymentJobs, change, outstandingChange, ownershipIssueId, owner, majorVersion, - metrics, pemDeployKey, legacyRotation, rotations, rotationStatus); + metrics, pemDeployKey, rotations, rotationStatus); } Application(ApplicationId id, Instant createdAt, DeploymentSpec deploymentSpec, ValidationOverrides validationOverrides, Map<ZoneId, Deployment> deployments, DeploymentJobs deploymentJobs, Change change, Change outstandingChange, Optional<IssueId> ownershipIssueId, Optional<User> owner, OptionalInt majorVersion, ApplicationMetrics metrics, Optional<String> pemDeployKey, - Optional<RotationId> legacyRotation, List<RotationId> rotations, Map<HostName, RotationStatus> rotationStatus) { + List<AssignedRotation> rotations, Map<HostName, RotationStatus> rotationStatus) { this.id = Objects.requireNonNull(id, "id cannot be null"); this.createdAt = Objects.requireNonNull(createdAt, "instant of creation cannot be null"); this.deploymentSpec = Objects.requireNonNull(deploymentSpec, "deploymentSpec cannot be null"); @@ -101,7 +99,6 @@ public class Application { this.majorVersion = Objects.requireNonNull(majorVersion, "majorVersion cannot be null"); this.metrics = Objects.requireNonNull(metrics, "metrics cannot be null"); this.pemDeployKey = pemDeployKey; - this.legacyRotation = Objects.requireNonNull(legacyRotation, "legacyRotation cannot be null"); this.rotations = List.copyOf(Objects.requireNonNull(rotations, "rotations cannot be null")); this.rotationStatus = ImmutableMap.copyOf(Objects.requireNonNull(rotationStatus, "rotationStatus cannot be null")); } @@ -200,11 +197,20 @@ public class Application { /** Returns the global rotation id of this, if present */ public Optional<RotationId> legacyRotation() { - return legacyRotation; + return rotations.stream() + .map(AssignedRotation::rotationId) + .findFirst(); } /** Returns all rotations for this application */ public List<RotationId> rotations() { + return rotations.stream() + .map(AssignedRotation::rotationId) + .collect(Collectors.toList()); + } + + /** Returns all assigned rotations for this application */ + public List<AssignedRotation> assignedRotations() { return rotations; } 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 782a76b684e..8345aa91685 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 @@ -43,9 +43,11 @@ import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordName; import com.yahoo.vespa.hosted.controller.api.integration.routing.RoutingEndpoint; import com.yahoo.vespa.hosted.controller.api.integration.routing.RoutingGenerator; import com.yahoo.vespa.hosted.controller.application.ApplicationPackage; +import com.yahoo.vespa.hosted.controller.application.AssignedRotation; import com.yahoo.vespa.hosted.controller.application.Deployment; import com.yahoo.vespa.hosted.controller.application.DeploymentMetrics; import com.yahoo.vespa.hosted.controller.application.Endpoint; +import com.yahoo.vespa.hosted.controller.application.EndpointId; import com.yahoo.vespa.hosted.controller.application.EndpointList; import com.yahoo.vespa.hosted.controller.application.JobList; import com.yahoo.vespa.hosted.controller.application.JobStatus; @@ -439,7 +441,7 @@ public class ApplicationController { if (zone.environment() == Environment.prod && application.get().deploymentSpec().globalServiceId().isPresent()) { try (RotationLock rotationLock = rotationRepository.lock()) { Rotation rotation = rotationRepository.getOrAssignRotation(application.get(), rotationLock); - application = application.with(rotation.id()); + application = application.with(List.of(new AssignedRotation(new ClusterSpec.Id(application.get().deploymentSpec().globalServiceId().get()), EndpointId.default_(), rotation.id()))); store(application); // store assigned rotation even if deployment fails boolean redirectLegacyDns = redirectLegacyDnsFlag.with(FetchVector.Dimension.APPLICATION_ID, application.get().id().serializedForm()) 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 5f958b74c39..02b0afdd48f 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 @@ -15,6 +15,7 @@ import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType; import com.yahoo.vespa.hosted.controller.api.integration.organization.IssueId; import com.yahoo.vespa.hosted.controller.api.integration.organization.User; import com.yahoo.config.provision.zone.ZoneId; +import com.yahoo.vespa.hosted.controller.application.AssignedRotation; import com.yahoo.vespa.hosted.controller.application.Change; import com.yahoo.vespa.hosted.controller.application.ClusterInfo; import com.yahoo.vespa.hosted.controller.application.ClusterUtilization; @@ -56,8 +57,7 @@ public class LockedApplication { private final OptionalInt majorVersion; private final ApplicationMetrics metrics; private final Optional<String> pemDeployKey; - private final Optional<RotationId> legacyRotation; - private final List<RotationId> rotations; + private final List<AssignedRotation> rotations; private final Map<HostName, RotationStatus> rotationStatus; /** @@ -72,7 +72,7 @@ public class LockedApplication { application.deployments(), application.deploymentJobs(), application.change(), application.outstandingChange(), application.ownershipIssueId(), application.owner(), application.majorVersion(), application.metrics(), - application.pemDeployKey(), application.legacyRotation(), application.rotations(), application.rotationStatus()); + application.pemDeployKey(), application.assignedRotations(), application.rotationStatus()); } private LockedApplication(Lock lock, ApplicationId id, Instant createdAt, @@ -80,7 +80,7 @@ public class LockedApplication { Map<ZoneId, Deployment> deployments, DeploymentJobs deploymentJobs, Change change, Change outstandingChange, Optional<IssueId> ownershipIssueId, Optional<User> owner, OptionalInt majorVersion, ApplicationMetrics metrics, Optional<String> pemDeployKey, - Optional<RotationId> legacyRotation, List<RotationId> rotations, Map<HostName, RotationStatus> rotationStatus) { + List<AssignedRotation> rotations, Map<HostName, RotationStatus> rotationStatus) { this.lock = lock; this.id = id; this.createdAt = createdAt; @@ -95,7 +95,6 @@ public class LockedApplication { this.majorVersion = majorVersion; this.metrics = metrics; this.pemDeployKey = pemDeployKey; - this.legacyRotation = legacyRotation; this.rotations = rotations; this.rotationStatus = rotationStatus; } @@ -104,35 +103,35 @@ public class LockedApplication { public Application get() { return new Application(id, createdAt, deploymentSpec, validationOverrides, deployments, deploymentJobs, change, outstandingChange, ownershipIssueId, owner, majorVersion, metrics, pemDeployKey, - legacyRotation, rotations, rotationStatus); + rotations, rotationStatus); } public LockedApplication withBuiltInternally(boolean builtInternally) { return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, deployments, deploymentJobs.withBuiltInternally(builtInternally), change, outstandingChange, ownershipIssueId, owner, majorVersion, metrics, pemDeployKey, - legacyRotation, rotations, rotationStatus); + rotations, rotationStatus); } public LockedApplication withProjectId(OptionalLong projectId) { return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, deployments, deploymentJobs.withProjectId(projectId), change, outstandingChange, ownershipIssueId, owner, majorVersion, metrics, pemDeployKey, - legacyRotation, rotations, rotationStatus); + rotations, rotationStatus); } public LockedApplication withDeploymentIssueId(IssueId issueId) { return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, deployments, deploymentJobs.with(issueId), change, outstandingChange, ownershipIssueId, owner, majorVersion, metrics, pemDeployKey, - legacyRotation, rotations, rotationStatus); + rotations, rotationStatus); } public LockedApplication withJobPause(JobType jobType, OptionalLong pausedUntil) { return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, deployments, deploymentJobs.withPause(jobType, pausedUntil), change, outstandingChange, ownershipIssueId, owner, majorVersion, metrics, pemDeployKey, - legacyRotation, rotations, rotationStatus); + rotations, rotationStatus); } public LockedApplication withJobCompletion(long projectId, JobType jobType, JobStatus.JobRun completion, @@ -140,14 +139,14 @@ public class LockedApplication { return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, deployments, deploymentJobs.withCompletion(projectId, jobType, completion, jobError), change, outstandingChange, ownershipIssueId, owner, majorVersion, metrics, - pemDeployKey, legacyRotation, rotations, rotationStatus); + pemDeployKey, rotations, rotationStatus); } public LockedApplication withJobTriggering(JobType jobType, JobStatus.JobRun job) { return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, deployments, deploymentJobs.withTriggering(jobType, job), change, outstandingChange, ownershipIssueId, owner, majorVersion, metrics, pemDeployKey, - legacyRotation, rotations, rotationStatus); + rotations, rotationStatus); } public LockedApplication withNewDeployment(ZoneId zone, ApplicationVersion applicationVersion, Version version, @@ -198,45 +197,45 @@ public class LockedApplication { return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, deployments, deploymentJobs.without(jobType), change, outstandingChange, ownershipIssueId, owner, majorVersion, metrics, pemDeployKey, - legacyRotation, rotations, rotationStatus); + rotations, rotationStatus); } public LockedApplication with(DeploymentSpec deploymentSpec) { return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, deployments, deploymentJobs, change, outstandingChange, ownershipIssueId, owner, majorVersion, metrics, pemDeployKey, - legacyRotation, rotations, rotationStatus); + rotations, rotationStatus); } public LockedApplication with(ValidationOverrides validationOverrides) { return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, deployments, deploymentJobs, change, outstandingChange, ownershipIssueId, owner, majorVersion, - metrics, pemDeployKey, legacyRotation, rotations, rotationStatus); + metrics, pemDeployKey, rotations, rotationStatus); } public LockedApplication withChange(Change change) { return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, deployments, deploymentJobs, change, outstandingChange, ownershipIssueId, owner, majorVersion, - metrics, pemDeployKey, legacyRotation, rotations, rotationStatus); + metrics, pemDeployKey, rotations, rotationStatus); } public LockedApplication withOutstandingChange(Change outstandingChange) { return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, deployments, deploymentJobs, change, outstandingChange, ownershipIssueId, owner, majorVersion, - metrics, pemDeployKey, legacyRotation, rotations, rotationStatus); + metrics, pemDeployKey, rotations, rotationStatus); } public LockedApplication withOwnershipIssueId(IssueId issueId) { return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, deployments, deploymentJobs, change, outstandingChange, Optional.ofNullable(issueId), owner, - majorVersion, metrics, pemDeployKey, legacyRotation, rotations, rotationStatus); + majorVersion, metrics, pemDeployKey, rotations, rotationStatus); } public LockedApplication withOwner(User owner) { return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, deployments, deploymentJobs, change, outstandingChange, ownershipIssueId, Optional.ofNullable(owner), majorVersion, metrics, pemDeployKey, - legacyRotation, rotations, rotationStatus); + rotations, rotationStatus); } /** Set a major version for this, or set to null to remove any major version override */ @@ -244,31 +243,31 @@ public class LockedApplication { return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, deployments, deploymentJobs, change, outstandingChange, ownershipIssueId, owner, majorVersion == null ? OptionalInt.empty() : OptionalInt.of(majorVersion), - metrics, pemDeployKey, legacyRotation, rotations, rotationStatus); + metrics, pemDeployKey, rotations, rotationStatus); } public LockedApplication with(MetricsService.ApplicationMetrics metrics) { return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, deployments, deploymentJobs, change, outstandingChange, ownershipIssueId, owner, majorVersion, - metrics, pemDeployKey, legacyRotation, rotations, rotationStatus); + metrics, pemDeployKey, rotations, rotationStatus); } public LockedApplication withPemDeployKey(String pemDeployKey) { return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, deployments, deploymentJobs, change, outstandingChange, ownershipIssueId, owner, majorVersion, - metrics, Optional.ofNullable(pemDeployKey), legacyRotation, rotations, rotationStatus); + metrics, Optional.ofNullable(pemDeployKey), rotations, rotationStatus); } - public LockedApplication with(RotationId rotation) { + public LockedApplication with(List<AssignedRotation> assignedRotations) { return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, deployments, deploymentJobs, change, outstandingChange, ownershipIssueId, owner, majorVersion, - metrics, pemDeployKey, Optional.of(rotation), List.of(rotation), rotationStatus); + metrics, pemDeployKey, assignedRotations, rotationStatus); } public LockedApplication withRotationStatus(Map<HostName, RotationStatus> rotationStatus) { return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, deployments, deploymentJobs, change, outstandingChange, ownershipIssueId, owner, majorVersion, - metrics, pemDeployKey, legacyRotation, rotations, rotationStatus); + metrics, pemDeployKey, rotations, rotationStatus); } /** Don't expose non-leaf sub-objects. */ @@ -281,7 +280,7 @@ public class LockedApplication { private LockedApplication with(Map<ZoneId, Deployment> deployments) { return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, deployments, deploymentJobs, change, outstandingChange, ownershipIssueId, owner, majorVersion, - metrics, pemDeployKey, legacyRotation, rotations, rotationStatus); + metrics, pemDeployKey, rotations, rotationStatus); } @Override diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/AssignedRotation.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/AssignedRotation.java new file mode 100644 index 00000000000..e1ed278a79e --- /dev/null +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/AssignedRotation.java @@ -0,0 +1,61 @@ +package com.yahoo.vespa.hosted.controller.application; + +import com.yahoo.config.provision.ClusterSpec; +import com.yahoo.vespa.hosted.controller.rotation.RotationId; + +import java.util.Objects; + +/** + * Contains the tuple of [clusterId, endpointId, rotationId], to keep track + * of which services have assigned which rotations under which name. + * + * @author ogronnesby + */ +public class AssignedRotation { + private final ClusterSpec.Id clusterId; + private final EndpointId endpointId; + private final RotationId rotationId; + + public AssignedRotation(ClusterSpec.Id clusterId, EndpointId endpointId, RotationId rotationId) { + this.clusterId = requireNonEmpty(clusterId, clusterId.value(), "clusterId"); + this.endpointId = Objects.requireNonNull(endpointId); + this.rotationId = Objects.requireNonNull(rotationId); + } + + public ClusterSpec.Id clusterId() { return clusterId; } + public EndpointId endpointId() { return endpointId; } + public RotationId rotationId() { return rotationId; } + + @Override + public String toString() { + return "AssignedRotation{" + + "clusterId=" + clusterId + + ", endpointId='" + endpointId + '\'' + + ", rotationId=" + rotationId + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + AssignedRotation that = (AssignedRotation) o; + return clusterId.equals(that.clusterId) && + endpointId.equals(that.endpointId) && + rotationId.equals(that.rotationId); + } + + @Override + public int hashCode() { + return Objects.hash(clusterId, endpointId, rotationId); + } + + private static <T> T requireNonEmpty(T object, String value, String field) { + Objects.requireNonNull(object); + Objects.requireNonNull(value); + if (value.isEmpty()) { + throw new IllegalArgumentException("Field '" + field + "' was empty"); + } + return object; + } +} diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/EndpointId.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/EndpointId.java new file mode 100644 index 00000000000..13c242c7b5f --- /dev/null +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/EndpointId.java @@ -0,0 +1,53 @@ +package com.yahoo.vespa.hosted.controller.application; + +import java.util.Objects; + +/** + * A type to represent the ID of an endpoint. This is typically the first part of + * an endpoint name. + * + * @author ogronnesby + */ +public class EndpointId { + private static final EndpointId DEFAULT = new EndpointId("default"); + + private final String id; + + public EndpointId(String id) { + this.id = requireNotEmpty(id); + } + + public String id() { return id; } + + @Override + public String toString() { + return "EndpointId{" + + "id='" + id + '\'' + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + EndpointId that = (EndpointId) o; + return Objects.equals(id, that.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + + private static String requireNotEmpty(String input) { + Objects.requireNonNull(input); + if (input.isEmpty()) { + throw new IllegalArgumentException("The value EndpointId was empty"); + } + return input; + } + + public static EndpointId default_() { return DEFAULT; } + + public static EndpointId of(String id) { return new EndpointId(id); } +} diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ResourceMeterMaintainer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ResourceMeterMaintainer.java index 9302ecbe738..c4f0597572b 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ResourceMeterMaintainer.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ResourceMeterMaintainer.java @@ -3,6 +3,7 @@ package com.yahoo.vespa.hosted.controller.maintenance; import com.yahoo.config.provision.CloudName; import com.yahoo.config.provision.SystemName; +import com.yahoo.config.provision.zone.ZoneApi; import com.yahoo.jdisc.Metric; import com.yahoo.vespa.hosted.controller.Controller; import com.yahoo.config.provision.ApplicationId; @@ -81,8 +82,8 @@ public class ResourceMeterMaintainer extends Maintainer { private List<NodeRepositoryNode> getNodes() { return controller().zoneRegistry().zones() .ofCloud(CloudName.from("aws")) - .reachable().ids().stream() - .flatMap(zoneId -> uncheck(() -> nodeRepository.listNodes(zoneId, true).nodes().stream())) + .reachable().zones().stream() + .flatMap(zone -> uncheck(() -> nodeRepository.listNodes(zone.getId(), true).nodes().stream())) .filter(node -> node.getOwner() != null && !node.getOwner().getTenant().equals("hosted-vespa")) .filter(node -> node.getState() == NodeState.active) .collect(Collectors.toList()); diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializer.java index 1f20bdf5533..271221e15b2 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializer.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializer.java @@ -21,6 +21,7 @@ import com.yahoo.vespa.hosted.controller.api.integration.deployment.SourceRevisi import com.yahoo.vespa.hosted.controller.api.integration.organization.IssueId; import com.yahoo.vespa.hosted.controller.api.integration.organization.User; import com.yahoo.config.provision.zone.ZoneId; +import com.yahoo.vespa.hosted.controller.application.AssignedRotation; import com.yahoo.vespa.hosted.controller.application.Change; import com.yahoo.vespa.hosted.controller.application.ClusterInfo; import com.yahoo.vespa.hosted.controller.application.ClusterUtilization; @@ -29,6 +30,7 @@ import com.yahoo.vespa.hosted.controller.application.DeploymentActivity; import com.yahoo.vespa.hosted.controller.application.DeploymentJobs; import com.yahoo.vespa.hosted.controller.application.DeploymentJobs.JobError; import com.yahoo.vespa.hosted.controller.application.DeploymentMetrics; +import com.yahoo.vespa.hosted.controller.application.EndpointId; import com.yahoo.vespa.hosted.controller.application.JobStatus; import com.yahoo.vespa.hosted.controller.application.RotationStatus; import com.yahoo.vespa.hosted.controller.rotation.RotationId; @@ -38,6 +40,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Optional; @@ -77,6 +80,10 @@ public class ApplicationSerializer { private final String writeQualityField = "writeQuality"; private final String queryQualityField = "queryQuality"; private final String pemDeployKeyField = "pemDeployKey"; + private final String assignedRotationsField = "assignedRotations"; + private final String assignedRotationEndpointField = "endpointId"; + private final String assignedRotationClusterField = "clusterId"; + private final String assignedRotationRotationField = "rotationId"; private final String rotationsField = "endpoints"; private final String deprecatedRotationField = "rotation"; private final String rotationStatusField = "rotationStatus"; @@ -171,8 +178,8 @@ public class ApplicationSerializer { root.setDouble(writeQualityField, application.metrics().writeServiceQuality()); application.pemDeployKey().ifPresent(pemDeployKey -> root.setString(pemDeployKeyField, pemDeployKey)); application.legacyRotation().ifPresent(rotation -> root.setString(deprecatedRotationField, rotation.asString())); - Cursor rotations = root.setArray(rotationsField); - application.rotations().forEach(rotation -> rotations.addString(rotation.asString())); + rotationsToSlime(application.assignedRotations(), root, rotationsField); + assignedRotationsToSlime(application.assignedRotations(), root, assignedRotationsField); toSlime(application.rotationStatus(), root.setArray(rotationStatusField)); return slime; } @@ -320,6 +327,21 @@ public class ApplicationSerializer { }); } + private void rotationsToSlime(List<AssignedRotation> rotations, Cursor parent, String fieldName) { + final var rotationsArray = parent.setArray(fieldName); + rotations.forEach(rot -> rotationsArray.addString(rot.rotationId().asString())); + } + + private void assignedRotationsToSlime(List<AssignedRotation> rotations, Cursor parent, String fieldName) { + final var rotationsArray = parent.setArray(fieldName); + for (var rotation : rotations) { + final var object = rotationsArray.addObject(); + object.setString(assignedRotationEndpointField, rotation.endpointId().id()); + object.setString(assignedRotationRotationField, rotation.rotationId().asString()); + object.setString(assignedRotationClusterField, rotation.clusterId().value()); + } + } + // ------------------ Deserialization public Application fromSlime(Slime slime) { @@ -339,13 +361,12 @@ public class ApplicationSerializer { ApplicationMetrics metrics = new ApplicationMetrics(root.field(queryQualityField).asDouble(), root.field(writeQualityField).asDouble()); Optional<String> pemDeployKey = optionalString(root.field(pemDeployKeyField)); - Optional<RotationId> legacyRotation = optionalString(root.field(deprecatedRotationField)).map(RotationId::new); - List<RotationId> rotations = rotationsFromSlime(root); + List<AssignedRotation> assignedRotations = assignedRotationsFromSlime(deploymentSpec, root); Map<HostName, RotationStatus> rotationStatus = rotationStatusFromSlime(root.field(rotationStatusField)); return new Application(id, createdAt, deploymentSpec, validationOverrides, deployments, deploymentJobs, deploying, outstandingChange, ownershipIssueId, owner, majorVersion, metrics, - pemDeployKey, legacyRotation, rotations, rotationStatus); + pemDeployKey, assignedRotations, rotationStatus); } private List<Deployment> deploymentsFromSlime(Inspector array) { @@ -525,15 +546,36 @@ public class ApplicationSerializer { Instant.ofEpochMilli(object.field(atField).asLong()))); } - private List<RotationId> rotationsFromSlime(Inspector root) { - final var rotations = rotationListFromSlime(root.field(rotationsField)); + private List<AssignedRotation> assignedRotationsFromSlime(DeploymentSpec deploymentSpec, Inspector root) { + final var assignedRotations = new LinkedHashSet<AssignedRotation>(); + + // Add the legacy rotation field to the set - this needs to be first + // TODO: Remove when we retire the rotations field final var legacyRotation = legacyRotationFromSlime(root.field(deprecatedRotationField)); + if (legacyRotation.isPresent() && deploymentSpec.globalServiceId().isPresent()) { + final var clusterId = new ClusterSpec.Id(deploymentSpec.globalServiceId().get()); + assignedRotations.add(new AssignedRotation(clusterId, EndpointId.default_(), legacyRotation.get())); + } - if (legacyRotation.isPresent() && ! rotations.contains(legacyRotation.get())) { - rotations.add(legacyRotation.get()); + // Now add the same entries from "stupid" list of rotations + // TODO: Remove when we retire the rotations field + final var rotations = rotationListFromSlime(root.field(rotationsField)); + for (var rotation : rotations) { + if (deploymentSpec.globalServiceId().isPresent()) { + final var clusterId = new ClusterSpec.Id(deploymentSpec.globalServiceId().get()); + assignedRotations.add(new AssignedRotation(clusterId, EndpointId.default_(), rotation)); + } } - return rotations; + // Last - add the actual entries we want. Do _not_ remove this during clean-up + root.field(assignedRotationsField).traverse((ArrayTraverser) (idx, inspector) -> { + final var clusterId = new ClusterSpec.Id(inspector.field(assignedRotationClusterField).asString()); + final var endpointId = EndpointId.of(inspector.field(assignedRotationEndpointField).asString()); + final var rotationId = new RotationId(inspector.field(assignedRotationRotationField).asString()); + assignedRotations.add(new AssignedRotation(clusterId, endpointId, rotationId)); + }); + + return List.copyOf(assignedRotations); } private List<RotationId> rotationListFromSlime(Inspector field) { diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ConfigServerRestExecutorImpl.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ConfigServerRestExecutorImpl.java index a208249b410..73a029ad3b3 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ConfigServerRestExecutorImpl.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ConfigServerRestExecutorImpl.java @@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.inject.Inject; import com.yahoo.config.provision.Environment; +import com.yahoo.config.provision.zone.ZoneApi; import com.yahoo.config.provision.zone.ZoneId; import com.yahoo.config.provision.zone.ZoneList; import com.yahoo.jdisc.http.HttpRequest.Method; @@ -114,9 +115,9 @@ public class ConfigServerRestExecutorImpl implements ConfigServerRestExecutor { if ( ! environmentName.isEmpty()) zones = zones.in(Environment.from(environmentName)); - for (ZoneId zoneId : zones.ids()) { + for (ZoneApi zone : zones.zones()) { responseStructure.uris.add(proxyRequest.getScheme() + "://" + proxyRequest.getControllerPrefix() + - zoneId.environment().value() + "/" + zoneId.region().value()); + zone.getEnvironment().value() + "/" + zone.getRegionName().value()); } JsonNode node = mapper.valueToTree(responseStructure); return new ProxyResponse(proxyRequest, node.toString(), 200, Optional.empty(), "application/json"); diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/cost/CostCalculator.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/cost/CostCalculator.java index 18c00d69b62..c44a80f7a20 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/cost/CostCalculator.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/cost/CostCalculator.java @@ -2,6 +2,7 @@ package com.yahoo.vespa.hosted.controller.restapi.cost; import com.yahoo.config.provision.CloudName; import com.yahoo.config.provision.Environment; +import com.yahoo.config.provision.zone.ZoneApi; import com.yahoo.vespa.hosted.controller.Controller; import com.yahoo.vespa.hosted.controller.api.identifiers.Property; import com.yahoo.vespa.hosted.controller.api.integration.noderepository.NodeOwner; @@ -34,8 +35,8 @@ public class CostCalculator { String date = LocalDate.now(clock).toString(); List<NodeRepositoryNode> nodes = controller.zoneRegistry().zones() - .reachable().in(Environment.prod).ofCloud(cloudName).ids().stream() - .flatMap(zoneId -> uncheck(() -> nodeRepository.listNodes(zoneId, true).nodes().stream())) + .reachable().in(Environment.prod).ofCloud(cloudName).zones().stream() + .flatMap(zone -> uncheck(() -> nodeRepository.listNodes(zone.getId(), true).nodes().stream())) .filter(node -> node.getOwner() != null && !node.getOwner().getTenant().equals("hosted-vespa")) .collect(Collectors.toList()); diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/os/OsApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/os/OsApiHandler.java index 5454d71185a..bc360fe3c6f 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/os/OsApiHandler.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/os/OsApiHandler.java @@ -5,6 +5,7 @@ import com.yahoo.component.Version; import com.yahoo.config.provision.CloudName; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.RegionName; +import com.yahoo.config.provision.zone.ZoneApi; import com.yahoo.container.jdisc.HttpRequest; import com.yahoo.container.jdisc.HttpResponse; import com.yahoo.io.IOUtils; @@ -30,6 +31,7 @@ import java.util.List; import java.util.Set; import java.util.StringJoiner; import java.util.logging.Level; +import java.util.stream.Collectors; /** * This implements the /os/v1 API which provides operators with information about, and scheduling of OS upgrades for @@ -123,7 +125,7 @@ public class OsApiHandler extends AuditLoggingRequestHandler { ZoneList zones = controller.zoneRegistry().zones().controllerUpgraded(); if (path.get("region") != null) zones = zones.in(RegionName.from(path.get("region"))); if (path.get("environment") != null) zones = zones.in(Environment.from(path.get("environment"))); - return zones.ids(); + return zones.zones().stream().map(ZoneApi::getId).collect(Collectors.toList()); } private Slime setOsVersion(HttpRequest request) { diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/ZoneApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/ZoneApiHandler.java index b115e659c28..6cfaed93fa9 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/ZoneApiHandler.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/ZoneApiHandler.java @@ -3,6 +3,8 @@ package com.yahoo.vespa.hosted.controller.restapi.zone.v1; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.RegionName; +import com.yahoo.config.provision.Zone; +import com.yahoo.config.provision.zone.ZoneApi; import com.yahoo.config.provision.zone.ZoneId; import com.yahoo.container.jdisc.HttpRequest; import com.yahoo.container.jdisc.HttpResponse; @@ -70,8 +72,8 @@ public class ZoneApiHandler extends LoggingRequestHandler { } private HttpResponse root(HttpRequest request) { - List<Environment> environments = zoneRegistry.zones().all().ids().stream() - .map(ZoneId::environment) + List<Environment> environments = zoneRegistry.zones().all().zones().stream() + .map(ZoneApi::getEnvironment) .distinct() .sorted(Comparator.comparing(Environment::value)) .collect(Collectors.toList()); @@ -90,17 +92,16 @@ public class ZoneApiHandler extends LoggingRequestHandler { } private HttpResponse environment(HttpRequest request, Environment environment) { - List<ZoneId> zones = zoneRegistry.zones().all().in(environment).ids(); Slime slime = new Slime(); Cursor root = slime.setArray(); - zones.forEach(zone -> { + zoneRegistry.zones().all().in(environment).zones().forEach(zone -> { Cursor object = root.addObject(); - object.setString("name", zone.region().value()); + object.setString("name", zone.getRegionName().value()); object.setString("url", request.getUri() .resolve("/zone/v2/environment/") .resolve(environment.value() + "/") .resolve("region/") - .resolve(zone.region().value()) + .resolve(zone.getRegionName().value()) .toString()); }); return new SlimeJsonResponse(slime); diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/ZoneApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/ZoneApiHandler.java index 9d95383fbfb..f0259fc4d51 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/ZoneApiHandler.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/ZoneApiHandler.java @@ -94,16 +94,16 @@ public class ZoneApiHandler extends AuditLoggingRequestHandler { Cursor root = slime.setObject(); Cursor uris = root.setArray("uris"); ZoneList zoneList = zoneRegistry.zones().reachable(); - zoneList.ids().forEach(zoneId -> uris.addString(request.getUri() + zoneList.zones().forEach(zone -> uris.addString(request.getUri() .resolve("/zone/v2/") - .resolve(zoneId.environment().value() + "/") - .resolve(zoneId.region().value()) + .resolve(zone.getEnvironment().value() + "/") + .resolve(zone.getRegionName().value()) .toString())); Cursor zones = root.setArray("zones"); - zoneList.ids().forEach(zoneId -> { + zoneList.zones().forEach(zone -> { Cursor object = zones.addObject(); - object.setString("environment", zoneId.environment().value()); - object.setString("region", zoneId.region().value()); + object.setString("environment", zone.getEnvironment().value()); + object.setString("region", zone.getRegionName().value()); }); return new SlimeJsonResponse(slime); } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VersionStatus.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VersionStatus.java index 87f35d3b2c1..ab5fd2714e5 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VersionStatus.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VersionStatus.java @@ -6,6 +6,7 @@ import com.yahoo.collections.ListMap; import com.yahoo.component.Version; import com.yahoo.component.Vtag; import com.yahoo.config.provision.HostName; +import com.yahoo.config.provision.zone.ZoneApi; import com.yahoo.config.provision.zone.ZoneId; import com.yahoo.log.LogLevel; import com.yahoo.vespa.hosted.controller.Application; @@ -155,20 +156,17 @@ public class VersionStatus { } private static ListMap<Version, HostName> findSystemApplicationVersions(Controller controller) { - List<ZoneId> zones = controller.zoneRegistry().zones() - .controllerUpgraded() - .ids(); ListMap<Version, HostName> versions = new ListMap<>(); - for (ZoneId zone : zones) { + for (ZoneApi zone : controller.zoneRegistry().zones().controllerUpgraded().zones()) { for (SystemApplication application : SystemApplication.all()) { List<Node> eligibleForUpgradeApplicationNodes = controller.configServer().nodeRepository() - .list(zone, application.id()).stream() + .list(zone.getId(), application.id()).stream() .filter(SystemUpgrader::eligibleForUpgrade) .collect(Collectors.toList()); if (eligibleForUpgradeApplicationNodes.isEmpty()) continue; - boolean configConverged = application.configConvergedIn(zone, controller, Optional.empty()); + boolean configConverged = application.configConvergedIn(zone.getId(), controller, Optional.empty()); if (!configConverged) { log.log(LogLevel.WARNING, "Config for " + application.id() + " in " + zone + " has not converged"); } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTester.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTester.java index 3ce32347e35..887406ecba8 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTester.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTester.java @@ -5,6 +5,7 @@ import com.yahoo.component.Version; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.TenantName; +import com.yahoo.config.provision.zone.ZoneApi; import com.yahoo.config.provision.zone.ZoneId; import com.yahoo.test.ManualClock; import com.yahoo.vespa.hosted.controller.Application; @@ -123,10 +124,10 @@ public class DeploymentTester { /** Upgrade system applications in all zones to given version */ public void upgradeSystemApplications(Version version) { - for (ZoneId zone : tester.zoneRegistry().zones().all().ids()) { + for (ZoneApi zone : tester.zoneRegistry().zones().all().zones()) { for (SystemApplication application : SystemApplication.all()) { - tester.configServer().setVersion(application.id(), zone, version); - tester.configServer().convergeServices(application.id(), zone); + tester.configServer().setVersion(application.id(), zone.getId(), version); + tester.configServer().convergeServices(application.id(), zone.getId()); } } computeVersionStatus(); diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ZoneFilterMock.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ZoneFilterMock.java index 57f29fb72af..00e6162d5e5 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ZoneFilterMock.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ZoneFilterMock.java @@ -82,11 +82,6 @@ public class ZoneFilterMock implements ZoneList { } @Override - public List<ZoneId> ids() { - return List.copyOf(zones.stream().map(ZoneApi::getId).collect(Collectors.toList())); - } - - @Override public ZoneList ofCloud(CloudName cloud) { return filter(zone -> zone.getCloudName().equals(cloud)); } 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 be9624fc693..67b6b1ac61c 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 @@ -16,6 +16,7 @@ import com.yahoo.vespa.hosted.controller.api.integration.deployment.SourceRevisi import com.yahoo.vespa.hosted.controller.api.integration.organization.IssueId; import com.yahoo.vespa.hosted.controller.api.integration.organization.User; import com.yahoo.config.provision.zone.ZoneId; +import com.yahoo.vespa.hosted.controller.application.AssignedRotation; import com.yahoo.vespa.hosted.controller.application.Change; import com.yahoo.vespa.hosted.controller.application.ClusterInfo; import com.yahoo.vespa.hosted.controller.application.ClusterUtilization; @@ -24,6 +25,7 @@ import com.yahoo.vespa.hosted.controller.application.DeploymentActivity; import com.yahoo.vespa.hosted.controller.application.DeploymentJobs; import com.yahoo.vespa.hosted.controller.application.DeploymentJobs.JobError; import com.yahoo.vespa.hosted.controller.application.DeploymentMetrics; +import com.yahoo.vespa.hosted.controller.application.EndpointId; import com.yahoo.vespa.hosted.controller.application.JobStatus; import com.yahoo.vespa.hosted.controller.application.RotationStatus; import com.yahoo.vespa.hosted.controller.rotation.RotationId; @@ -116,8 +118,7 @@ public class ApplicationSerializerTest { OptionalInt.of(7), new MetricsService.ApplicationMetrics(0.5, 0.9), Optional.of("-----BEGIN PUBLIC KEY-----\n∠( ᐛ 」∠)_\n-----END PUBLIC KEY-----"), - Optional.of(new RotationId("my-rotation")), - List.of(new RotationId("my-rotation")), + List.of(new AssignedRotation(new ClusterSpec.Id("foo"), EndpointId.default_(), new RotationId("my-rotation"))), rotationStatus); Application serialized = applicationSerializer.fromSlime(applicationSerializer.toSlime(original)); @@ -261,14 +262,21 @@ public class ApplicationSerializerTest { rotations.addString("multiple-rotation-1"); rotations.addString("multiple-rotation-2"); + final var assignedRotations = cursor.setArray("assignedRotations"); + final var assignedRotation = assignedRotations.addObject(); + assignedRotation.setString("clusterId", "foobar"); + assignedRotation.setString("endpointId", "nice-endpoint"); + assignedRotation.setString("rotationId", "assigned-rotation"); + // Parse and test the output from parsing contains both legacy rotation and multiple rotations final var application = applicationSerializer.fromSlime(slime); assertEquals( List.of( - new RotationId("multiple-rotation-1"), - new RotationId("multiple-rotation-2"), - new RotationId("single-rotation") + new RotationId("single-rotation"), + new RotationId("multiple-rotation-1"), + new RotationId("multiple-rotation-2"), + new RotationId("assigned-rotation") ), application.rotations() ); @@ -276,6 +284,16 @@ public class ApplicationSerializerTest { assertEquals( Optional.of(new RotationId("single-rotation")), application.legacyRotation() ); + + assertEquals( + List.of( + new AssignedRotation(new ClusterSpec.Id("foo"), EndpointId.of("default"), new RotationId("single-rotation")), + new AssignedRotation(new ClusterSpec.Id("foo"), EndpointId.of("default"), new RotationId("multiple-rotation-1")), + new AssignedRotation(new ClusterSpec.Id("foo"), EndpointId.of("default"), new RotationId("multiple-rotation-2")), + new AssignedRotation(new ClusterSpec.Id("foobar"), EndpointId.of("nice-endpoint"), new RotationId("assigned-rotation")) + ), + application.assignedRotations() + ); } @Test diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ContainerTester.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ContainerTester.java index ef86ffa125f..c7be543dd00 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ContainerTester.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ContainerTester.java @@ -6,6 +6,7 @@ import com.yahoo.application.container.handler.Request; import com.yahoo.application.container.handler.Response; import com.yahoo.component.ComponentSpecification; import com.yahoo.component.Version; +import com.yahoo.config.provision.zone.ZoneApi; import com.yahoo.config.provision.zone.ZoneId; import com.yahoo.container.http.filter.FilterChainRepository; import com.yahoo.jdisc.http.filter.SecurityRequestFilter; @@ -59,10 +60,10 @@ public class ContainerTester { public void upgradeSystem(Version version) { controller().curator().writeControllerVersion(controller().hostname(), version); - for (ZoneId zone : controller().zoneRegistry().zones().all().ids()) { + for (ZoneApi zone : controller().zoneRegistry().zones().all().zones()) { for (SystemApplication application : SystemApplication.all()) { - configServer().setVersion(application.id(), zone, version); - configServer().convergeServices(application.id(), zone); + configServer().setVersion(application.id(), zone.getId(), version); + configServer().convergeServices(application.id(), zone.getId()); } } computeVersionStatus(); diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/rotation/RotationRepositoryTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/rotation/RotationRepositoryTest.java index 02a82e35f10..8f02fa74c6e 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/rotation/RotationRepositoryTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/rotation/RotationRepositoryTest.java @@ -15,7 +15,6 @@ import org.junit.rules.ExpectedException; import java.net.URI; import java.util.List; -import java.util.Optional; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/versions/VersionStatusTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/versions/VersionStatusTest.java index 8e3dc24193f..655c16ccceb 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/versions/VersionStatusTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/versions/VersionStatusTest.java @@ -6,6 +6,7 @@ import com.yahoo.component.Version; import com.yahoo.component.Vtag; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.HostName; +import com.yahoo.config.provision.zone.ZoneApi; import com.yahoo.vespa.hosted.controller.Application; import com.yahoo.vespa.hosted.controller.Controller; import com.yahoo.vespa.hosted.controller.ControllerTester; @@ -60,10 +61,10 @@ public class VersionStatusTest { Version version0 = Version.fromString("6.1"); Version version1 = Version.fromString("6.5"); // Upgrade some config servers - for (ZoneId zone : tester.zoneRegistry().zones().all().ids()) { - for (Node node : tester.configServer().nodeRepository().list(zone, SystemApplication.configServer.id())) { - tester.configServer().nodeRepository().putByHostname(zone, new Node(node.hostname(), node.state(), node.type(), - node.owner(), version1, node.wantedVersion())); + for (ZoneApi zone : tester.zoneRegistry().zones().all().zones()) { + for (Node node : tester.configServer().nodeRepository().list(zone.getId(), SystemApplication.configServer.id())) { + Node upgradedNode = new Node(node.hostname(), node.state(), node.type(), node.owner(), version1, node.wantedVersion()); + tester.configServer().nodeRepository().putByHostname(zone.getId(), upgradedNode); break; } } @@ -105,10 +106,10 @@ public class VersionStatusTest { // Downgrade one config server in each zone Version ancientVersion = Version.fromString("5.1"); - for (ZoneId zone : tester.controller().zoneRegistry().zones().all().ids()) { - for (Node node : tester.configServer().nodeRepository().list(zone, SystemApplication.configServer.id())) { - tester.configServer().nodeRepository().putByHostname(zone, new Node(node.hostname(), node.state(), node.type(), - node.owner(), ancientVersion, node.wantedVersion())); + for (ZoneApi zone : tester.controller().zoneRegistry().zones().all().zones()) { + for (Node node : tester.configServer().nodeRepository().list(zone.getId(), SystemApplication.configServer.id())) { + Node downgradedNode = new Node(node.hostname(), node.state(), node.type(), node.owner(), ancientVersion, node.wantedVersion()); + tester.configServer().nodeRepository().putByHostname(zone.getId(), downgradedNode); break; } } diff --git a/hosted-api/pom.xml b/hosted-api/pom.xml index f20244a8816..928a173f9d8 100644 --- a/hosted-api/pom.xml +++ b/hosted-api/pom.xml @@ -34,9 +34,13 @@ </dependency> <dependency> - <groupId>junit</groupId> - <artifactId>junit</artifactId> - <version>4.12</version> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter-engine</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.junit.vintage</groupId> + <artifactId>junit-vintage-engine</artifactId> <scope>test</scope> </dependency> </dependencies> diff --git a/hosted-api/src/test/java/ai/vespa/hosted/api/MultiPartStreamerTest.java b/hosted-api/src/test/java/ai/vespa/hosted/api/MultiPartStreamerTest.java index a55c0d91cd3..bfc544e82f8 100644 --- a/hosted-api/src/test/java/ai/vespa/hosted/api/MultiPartStreamerTest.java +++ b/hosted-api/src/test/java/ai/vespa/hosted/api/MultiPartStreamerTest.java @@ -1,9 +1,8 @@ // Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package ai.vespa.hosted.api; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; import java.io.IOException; import java.net.URI; @@ -12,16 +11,13 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.List; -import static org.junit.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; -public class MultiPartStreamerTest { - - @Rule - public TemporaryFolder tmp = new TemporaryFolder(); +class MultiPartStreamerTest { @Test - public void test() throws IOException { - Path file = tmp.newFile().toPath(); + void test(@TempDir Path tmp) throws IOException { + Path file = tmp.resolve("file"); Files.write(file, new byte[]{0x48, 0x69}); MultiPartStreamer streamer = new MultiPartStreamer("My boundary"); diff --git a/hosted-api/src/test/java/ai/vespa/hosted/api/SignaturesTest.java b/hosted-api/src/test/java/ai/vespa/hosted/api/SignaturesTest.java index 0a0d4a48edf..6749fb902f9 100644 --- a/hosted-api/src/test/java/ai/vespa/hosted/api/SignaturesTest.java +++ b/hosted-api/src/test/java/ai/vespa/hosted/api/SignaturesTest.java @@ -1,7 +1,7 @@ // Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package ai.vespa.hosted.api; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.io.ByteArrayInputStream; import java.io.InputStream; @@ -16,9 +16,9 @@ import java.time.ZoneOffset; import static ai.vespa.hosted.api.Signatures.sha256Digest; import static ai.vespa.hosted.api.Signatures.sha256Digester; import static java.nio.charset.StandardCharsets.UTF_8; -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; /** * Tests that messages can be signed and verified, and that the keys used for this can be parsed. @@ -32,7 +32,7 @@ import static org.junit.Assert.assertTrue; * * @author jonmv */ -public class SignaturesTest { +class SignaturesTest { private static final String ecPemPublicKey = "-----BEGIN PUBLIC KEY-----\n" + "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuKVFA8dXk43kVfYKzkUqhEY2rDT9\n" + @@ -58,7 +58,7 @@ public class SignaturesTest { "∠( ᐛ 」∠)_").getBytes(UTF_8); @Test - public void testHashing() throws Exception { + void testHashing() throws Exception { byte[] hash1 = MessageDigest.getInstance("SHA-256").digest(message); byte[] hash2 = sha256Digest(() -> new ByteArrayInputStream(message)); DigestInputStream digester = sha256Digester(new ByteArrayInputStream(message)); @@ -70,7 +70,7 @@ public class SignaturesTest { } @Test - public void testSigning() { + void testSigning() { Clock clock = Clock.fixed(Instant.EPOCH, ZoneOffset.UTC); RequestSigner signer = new RequestSigner(ecPemPrivateKey, "myKey", clock); diff --git a/jdisc_http_service/abi-spec.json b/jdisc_http_service/abi-spec.json index 04e6d22a445..a326b5792be 100644 --- a/jdisc_http_service/abi-spec.json +++ b/jdisc_http_service/abi-spec.json @@ -78,7 +78,9 @@ "public void <init>(com.yahoo.jdisc.http.ConnectorConfig$Ssl)", "public com.yahoo.jdisc.http.ConnectorConfig$Ssl$Builder enabled(boolean)", "public com.yahoo.jdisc.http.ConnectorConfig$Ssl$Builder privateKeyFile(java.lang.String)", + "public com.yahoo.jdisc.http.ConnectorConfig$Ssl$Builder privateKey(java.lang.String)", "public com.yahoo.jdisc.http.ConnectorConfig$Ssl$Builder certificateFile(java.lang.String)", + "public com.yahoo.jdisc.http.ConnectorConfig$Ssl$Builder certificate(java.lang.String)", "public com.yahoo.jdisc.http.ConnectorConfig$Ssl$Builder caCertificateFile(java.lang.String)", "public com.yahoo.jdisc.http.ConnectorConfig$Ssl$Builder clientAuth(com.yahoo.jdisc.http.ConnectorConfig$Ssl$ClientAuth$Enum)", "public com.yahoo.jdisc.http.ConnectorConfig$Ssl build()" @@ -131,7 +133,9 @@ "public void <init>(com.yahoo.jdisc.http.ConnectorConfig$Ssl$Builder)", "public boolean enabled()", "public java.lang.String privateKeyFile()", + "public java.lang.String privateKey()", "public java.lang.String certificateFile()", + "public java.lang.String certificate()", "public java.lang.String caCertificateFile()", "public com.yahoo.jdisc.http.ConnectorConfig$Ssl$ClientAuth$Enum clientAuth()" ], diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/impl/ConfiguredSslContextFactoryProvider.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/impl/ConfiguredSslContextFactoryProvider.java index facb54bc37a..2021105fc52 100644 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/impl/ConfiguredSslContextFactoryProvider.java +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/impl/ConfiguredSslContextFactoryProvider.java @@ -60,15 +60,23 @@ public class ConfiguredSslContextFactoryProvider implements SslContextFactoryPro private static void validateConfig(ConnectorConfig.Ssl config) { if (!config.enabled()) return; - if (config.certificateFile().isEmpty()) { - throw new IllegalArgumentException("Missing certificate file."); - } - if (config.privateKeyFile().isEmpty()) { - throw new IllegalArgumentException("Missing private key file."); - } + if(hasBoth(config.certificate(), config.certificateFile())) + throw new IllegalArgumentException("Specified both certificate and certificate file."); + + if(hasBoth(config.privateKey(), config.privateKeyFile())) + throw new IllegalArgumentException("Specified both private key and private key file."); + + if(hasNeither(config.certificate(), config.certificateFile())) + throw new IllegalArgumentException("Specified neither certificate or certificate file."); + + if(hasNeither(config.privateKey(), config.privateKeyFile())) + throw new IllegalArgumentException("Specified neither private key or private key file."); } + private static boolean hasBoth(String a, String b) { return !a.isBlank() && !b.isBlank(); } + private static boolean hasNeither(String a, String b) { return a.isBlank() && b.isBlank(); } + private static KeyStore createTruststore(ConnectorConfig.Ssl sslConfig) { List<X509Certificate> caCertificates = X509CertificateUtils.certificateListFromPem(readToString(sslConfig.caCertificateFile())); return KeyStoreBuilder.withType(KeyStoreType.JKS) @@ -77,11 +85,21 @@ public class ConfiguredSslContextFactoryProvider implements SslContextFactoryPro } private static KeyStore createKeystore(ConnectorConfig.Ssl sslConfig) { - PrivateKey privateKey = KeyUtils.fromPemEncodedPrivateKey(readToString(sslConfig.privateKeyFile())); - List<X509Certificate> certificates = X509CertificateUtils.certificateListFromPem(readToString(sslConfig.certificateFile())); + PrivateKey privateKey = KeyUtils.fromPemEncodedPrivateKey(getPrivateKey(sslConfig)); + List<X509Certificate> certificates = X509CertificateUtils.certificateListFromPem(getCertificate(sslConfig)); return KeyStoreBuilder.withType(KeyStoreType.JKS).withKeyEntry("default", privateKey, certificates).build(); } + private static String getPrivateKey(ConnectorConfig.Ssl config) { + if(!config.privateKey().isBlank()) return config.privateKey(); + return readToString(config.privateKeyFile()); + } + + private static String getCertificate(ConnectorConfig.Ssl config) { + if(!config.certificate().isBlank()) return config.certificate(); + return readToString(config.certificateFile()); + } + private static String readToString(String filename) { try { return Files.readString(Paths.get(filename), StandardCharsets.UTF_8); diff --git a/jdisc_http_service/src/main/resources/configdefinitions/jdisc.http.connector.def b/jdisc_http_service/src/main/resources/configdefinitions/jdisc.http.connector.def index 7735420d803..c6c6fad345b 100644 --- a/jdisc_http_service/src/main/resources/configdefinitions/jdisc.http.connector.def +++ b/jdisc_http_service/src/main/resources/configdefinitions/jdisc.http.connector.def @@ -56,12 +56,18 @@ throttling.idleTimeout double default=-1.0 # Whether to enable SSL for this connector. ssl.enabled bool default=false -# File with private key in PEM format +# File with private key in PEM format. Specify either this or privateKey, but not both ssl.privateKeyFile string default="" -# File with certificate in PEM format +# Private key in PEM format. Specify either this or privateKeyFile, but not both +ssl.privateKey string default="" + +# File with certificate in PEM format. Specify either this or certificate, but not both ssl.certificateFile string default="" +# Certificate in PEM format. Specify either this or certificateFile, but not both +ssl.certificate string default="" + # with trusted CA certificates in PEM format. Used to verify clients ssl.caCertificateFile string default="" diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/lb/LoadBalancerService.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/lb/LoadBalancerService.java index 6f45403f0e6..f6398c04e61 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/lb/LoadBalancerService.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/lb/LoadBalancerService.java @@ -13,8 +13,17 @@ import java.util.Set; */ public interface LoadBalancerService { - /** Create a load balancer for given application cluster. Implementations are expected to be idempotent */ - LoadBalancerInstance create(ApplicationId application, ClusterSpec.Id cluster, Set<Real> reals); + /** + * Create a load balancer for given application cluster. Implementations are expected to be idempotent + * + * @param application Application owning the LB + * @param cluster Target cluster of the LB + * @param reals Reals that should be configured on the LB + * @param force Whether reconfiguration should be forced (e.g. allow configuring an empty set of reals on a + * pre-existing load balancer). + * @return The provisioned load balancer instance + */ + LoadBalancerInstance create(ApplicationId application, ClusterSpec.Id cluster, Set<Real> reals, boolean force); /** Permanently remove load balancer for given application cluster */ void remove(ApplicationId application, ClusterSpec.Id cluster); diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/lb/LoadBalancerServiceMock.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/lb/LoadBalancerServiceMock.java index e89a4dc8bf8..91f02a31f6b 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/lb/LoadBalancerServiceMock.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/lb/LoadBalancerServiceMock.java @@ -29,10 +29,10 @@ public class LoadBalancerServiceMock implements LoadBalancerService { } @Override - public LoadBalancerInstance create(ApplicationId application, ClusterSpec.Id cluster, Set<Real> reals) { + public LoadBalancerInstance create(ApplicationId application, ClusterSpec.Id cluster, Set<Real> reals, boolean force) { var id = new LoadBalancerId(application, cluster); var oldInstance = instances.get(id); - if (oldInstance != null && !oldInstance.reals().isEmpty() && reals.isEmpty()) { + if (!force && oldInstance != null && !oldInstance.reals().isEmpty() && reals.isEmpty()) { throw new IllegalArgumentException("Refusing to remove all reals from load balancer " + id); } var instance = new LoadBalancerInstance( diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/lb/SharedLoadBalancerService.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/lb/SharedLoadBalancerService.java index 87b7c73386e..331ffe7e202 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/lb/SharedLoadBalancerService.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/lb/SharedLoadBalancerService.java @@ -32,7 +32,7 @@ public class SharedLoadBalancerService implements LoadBalancerService { } @Override - public LoadBalancerInstance create(ApplicationId application, ClusterSpec.Id cluster, Set<Real> reals) { + public LoadBalancerInstance create(ApplicationId application, ClusterSpec.Id cluster, Set<Real> reals, boolean force) { final var proxyNodes = nodeRepository.getNodes(NodeType.proxy); proxyNodes.sort(hostnameComparator); diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/NodeRepositoryMaintenance.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/NodeRepositoryMaintenance.java index d0dc090bc74..52e7a28acc8 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/NodeRepositoryMaintenance.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/NodeRepositoryMaintenance.java @@ -76,7 +76,7 @@ public class NodeRepositoryMaintenance extends AbstractComponent { metricsReporter = new MetricsReporter(nodeRepository, metric, orchestrator, serviceMonitor, periodicApplicationMaintainer::pendingDeployments, durationFromEnv("metrics_interval").orElse(defaults.metricsInterval)); infrastructureProvisioner = new InfrastructureProvisioner(nodeRepository, infraDeployer, durationFromEnv("infrastructure_provision_interval").orElse(defaults.infrastructureProvisionInterval)); loadBalancerExpirer = provisionServiceProvider.getLoadBalancerService().map(lbService -> - new LoadBalancerExpirer(nodeRepository, durationFromEnv("load_balancer_expiry").orElse(defaults.loadBalancerExpiry), lbService)); + new LoadBalancerExpirer(nodeRepository, durationFromEnv("load_balancer_expirer_interval").orElse(defaults.loadBalancerExpirerInterval), lbService)); hostProvisionMaintainer = provisionServiceProvider.getHostProvisioner().map(hostProvisioner -> new HostProvisionMaintainer(nodeRepository, durationFromEnv("host_provisioner_interval").orElse(defaults.hostProvisionerInterval), hostProvisioner, flagSource)); hostDeprovisionMaintainer = provisionServiceProvider.getHostProvisioner().map(hostProvisioner -> @@ -143,7 +143,7 @@ public class NodeRepositoryMaintenance extends AbstractComponent { private final Duration metricsInterval; private final Duration retiredInterval; private final Duration infrastructureProvisionInterval; - private final Duration loadBalancerExpiry; + private final Duration loadBalancerExpirerInterval; private final Duration hostProvisionerInterval; private final Duration hostDeprovisionerInterval; @@ -161,7 +161,7 @@ public class NodeRepositoryMaintenance extends AbstractComponent { metricsInterval = Duration.ofMinutes(1); infrastructureProvisionInterval = Duration.ofMinutes(1); throttlePolicy = NodeFailer.ThrottlePolicy.hosted; - loadBalancerExpiry = Duration.ofMinutes(10); + loadBalancerExpirerInterval = Duration.ofMinutes(10); reservationExpiry = Duration.ofMinutes(20); // Need to be long enough for deployment to be finished for all config model versions hostProvisionerInterval = Duration.ofMinutes(5); hostDeprovisionerInterval = Duration.ofMinutes(5); diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/CuratorDatabaseClient.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/CuratorDatabaseClient.java index 61ca19a4cb9..c68e086dfb9 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/CuratorDatabaseClient.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/CuratorDatabaseClient.java @@ -107,7 +107,7 @@ public class CuratorDatabaseClient { CuratorTransaction curatorTransaction = curatorDatabase.newCuratorTransactionIn(transaction); for (Node node : nodes) { if (node.state() != expectedState) - throw new IllegalArgumentException(node + " is not in the " + node.state() + " state"); + throw new IllegalArgumentException(node + " is not in the " + expectedState + " state"); node = node.with(node.history().recordStateTransition(null, expectedState, Agent.system, clock.instant())); curatorTransaction.add(CuratorOperations.create(toPath(node).getAbsolute(), nodeSerializer.toJson(node))); diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/LoadBalancerProvisioner.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/LoadBalancerProvisioner.java index ca7ee1b13a1..0828f3369a2 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/LoadBalancerProvisioner.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/LoadBalancerProvisioner.java @@ -103,10 +103,12 @@ public class LoadBalancerProvisioner { try (var loadBalancersLock = db.lockLoadBalancers()) { var id = new LoadBalancerId(application, clusterId); var now = nodeRepository.clock().instant(); - var instance = create(application, clusterId, allocatedContainers(application, clusterId)); var loadBalancer = db.readLoadBalancers().get(id); + if (loadBalancer == null && activate) return; // Nothing to activate as this load balancer was never prepared + + var force = loadBalancer != null && loadBalancer.state() != LoadBalancer.State.active; + var instance = create(application, clusterId, allocatedContainers(application, clusterId), force); if (loadBalancer == null) { - if (activate) return; // Nothing to activate as this load balancer was never prepared loadBalancer = new LoadBalancer(id, instance, LoadBalancer.State.reserved, now); } else { var newState = activate ? LoadBalancer.State.active : loadBalancer.state(); @@ -117,7 +119,7 @@ public class LoadBalancerProvisioner { } } - private LoadBalancerInstance create(ApplicationId application, ClusterSpec.Id cluster, List<Node> nodes) { + private LoadBalancerInstance create(ApplicationId application, ClusterSpec.Id cluster, List<Node> nodes, boolean force) { Map<HostName, Set<String>> hostnameToIpAdresses = nodes.stream() .collect(Collectors.toMap(node -> HostName.from(node.hostname()), this::reachableIpAddresses)); @@ -126,12 +128,12 @@ public class LoadBalancerProvisioner { ipAddresses.forEach(ipAddress -> reals.add(new Real(hostname, ipAddress))); }); log.log(LogLevel.INFO, "Creating load balancer for " + cluster + " in " + application.toShortString() + - ", targeting: " + nodes); + ", targeting: " + reals); try { - return service.create(application, cluster, reals); + return service.create(application, cluster, reals, force); } catch (Exception e) { throw new LoadBalancerServiceException("Failed to (re)configure load balancer for " + cluster + " in " + - application + ", targeting: " + nodes + ". The operation will be " + + application + ", targeting: " + reals + ". The operation will be " + "retried on next deployment", e); } } diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/lb/SharedLoadBalancerServiceTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/lb/SharedLoadBalancerServiceTest.java index 40c307c6bef..5344fbc3c5f 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/lb/SharedLoadBalancerServiceTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/lb/SharedLoadBalancerServiceTest.java @@ -29,7 +29,7 @@ public class SharedLoadBalancerServiceTest { @Test public void test_create_lb() { tester.makeReadyNodes(2, "default", NodeType.proxy); - final var lb = loadBalancerService.create(applicationId, clusterId, reals); + final var lb = loadBalancerService.create(applicationId, clusterId, reals, false); assertEquals(HostName.from("host-1.yahoo.com"), lb.hostname()); assertEquals(Optional.empty(), lb.dnsZone()); @@ -39,7 +39,7 @@ public class SharedLoadBalancerServiceTest { @Test(expected = IllegalStateException.class) public void test_exception_on_missing_proxies() { - loadBalancerService.create(applicationId, clusterId, reals); + loadBalancerService.create(applicationId, clusterId, reals, false); } @Test diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/LoadBalancerProvisionerTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/LoadBalancerProvisionerTest.java index 0b3c3d209be..77273f98f76 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/LoadBalancerProvisionerTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/LoadBalancerProvisionerTest.java @@ -9,23 +9,28 @@ import com.yahoo.config.provision.ClusterSpec; import com.yahoo.config.provision.HostName; import com.yahoo.config.provision.HostSpec; import com.yahoo.config.provision.NodeResources; +import com.yahoo.config.provision.NodeType; import com.yahoo.transaction.NestedTransaction; import com.yahoo.vespa.hosted.provision.Node; import com.yahoo.vespa.hosted.provision.lb.LoadBalancer; import com.yahoo.vespa.hosted.provision.lb.LoadBalancerInstance; import com.yahoo.vespa.hosted.provision.lb.Real; import com.yahoo.vespa.hosted.provision.node.Agent; +import com.yahoo.vespa.hosted.provision.node.IP; import org.junit.Test; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.function.Supplier; import java.util.stream.Collectors; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; @@ -37,6 +42,8 @@ public class LoadBalancerProvisionerTest { private final ApplicationId app1 = ApplicationId.from("tenant1", "application1", "default"); private final ApplicationId app2 = ApplicationId.from("tenant2", "application2", "default"); + private final ApplicationId infraApp1 = ApplicationId.from("vespa", "tenant-host", "default"); + private ProvisioningTester tester = new ProvisioningTester.Builder().build(); @Test @@ -131,19 +138,94 @@ public class LoadBalancerProvisionerTest { .orElseThrow()); } + @Test + public void provision_load_balancers_with_dynamic_node_provisioning() { + var nodes = prepare(app1, Capacity.fromCount(2, new NodeResources(1, 1, 1), false, true), + true, + clusterRequest(ClusterSpec.Type.container, ClusterSpec.Id.from("qrs"))); + Supplier<LoadBalancer> lb = () -> tester.nodeRepository().loadBalancers().owner(app1).asList().get(0); + assertTrue("Load balancer provisioned with empty reals", tester.loadBalancerService().instances().get(lb.get().id()).reals().isEmpty()); + assignIps(tester.nodeRepository().getNodes(app1)); + tester.activate(app1, nodes); + assertFalse("Load balancer is reconfigured with reals", tester.loadBalancerService().instances().get(lb.get().id()).reals().isEmpty()); + + // Application is removed, nodes are deleted and load balancer is deactivated + NestedTransaction removeTransaction = new NestedTransaction(); + tester.provisioner().remove(removeTransaction, app1); + removeTransaction.commit(); + tester.nodeRepository().database().removeNodes(tester.nodeRepository().getNodes()); + assertTrue("Nodes are deleted", tester.nodeRepository().getNodes().isEmpty()); + assertSame("Load balancer is deactivated", LoadBalancer.State.inactive, lb.get().state()); + + // Application is redeployed + nodes = prepare(app1, Capacity.fromCount(2, new NodeResources(1, 1, 1), false, true), + true, + clusterRequest(ClusterSpec.Type.container, ClusterSpec.Id.from("qrs"))); + assertTrue("Load balancer is reconfigured with empty reals", tester.loadBalancerService().instances().get(lb.get().id()).reals().isEmpty()); + assignIps(tester.nodeRepository().getNodes(app1)); + tester.activate(app1, nodes); + assertFalse("Load balancer is reconfigured with reals", tester.loadBalancerService().instances().get(lb.get().id()).reals().isEmpty()); + } + + @Test + public void does_not_provision_load_balancers_for_non_tenant_node_type() { + tester.activate(infraApp1, prepare(infraApp1, Capacity.fromRequiredNodeType(NodeType.host), + false, + clusterRequest(ClusterSpec.Type.container, + ClusterSpec.Id.from("tenant-host")))); + assertTrue("No load balancer provisioned", tester.loadBalancerService().instances().isEmpty()); + assertEquals(List.of(), tester.nodeRepository().loadBalancers().owner(infraApp1).asList()); + } + + @Test + public void does_not_provision_load_balancers_for_non_container_cluster() { + tester.activate(app1, prepare(app1, clusterRequest(ClusterSpec.Type.content, + ClusterSpec.Id.from("tenant-host")))); + assertTrue("No load balancer provisioned", tester.loadBalancerService().instances().isEmpty()); + assertEquals(List.of(), tester.nodeRepository().loadBalancers().owner(app1).asList()); + } + private void dirtyNodesOf(ApplicationId application) { tester.nodeRepository().setDirty(tester.nodeRepository().getNodes(application), Agent.system, this.getClass().getSimpleName()); } private Set<HostSpec> prepare(ApplicationId application, ClusterSpec... specs) { - tester.makeReadyNodes(specs.length * 2, "d-1-1-1"); + return prepare(application, Capacity.fromCount(2, new NodeResources(1, 1, 1), false, true), false, specs); + } + + private Set<HostSpec> prepare(ApplicationId application, Capacity capacity, boolean dynamicDockerNodes, ClusterSpec... specs) { + if (dynamicDockerNodes) { + makeDynamicDockerNodes(specs.length * 2, capacity.type()); + } else { + tester.makeReadyNodes(specs.length * 2, "d-1-1-1", capacity.type()); + } Set<HostSpec> allNodes = new LinkedHashSet<>(); for (ClusterSpec spec : specs) { - allNodes.addAll(tester.prepare(application, spec, Capacity.fromCount(2, new NodeResources(1, 1, 1), false, true), 1, false)); + allNodes.addAll(tester.prepare(application, spec, capacity, 1, false)); } return allNodes; } + private void makeDynamicDockerNodes(int n, NodeType nodeType) { + List<Node> nodes = new ArrayList<>(n); + for (int i = 1; i <= n; i++) { + var node = Node.createDockerNode(Set.of(), Set.of(), "node" + i, Optional.empty(), + NodeResources.fromLegacyName("d-1-1-1"), nodeType); + nodes.add(node); + } + nodes = tester.nodeRepository().database().addNodesInState(nodes, Node.State.reserved); + nodes = tester.nodeRepository().setDirty(nodes, Agent.system, getClass().getSimpleName()); + tester.nodeRepository().setReady(nodes, Agent.system, getClass().getSimpleName()); + } + + private void assignIps(List<Node> nodes) { + try (var lock = tester.nodeRepository().lockAllocation()) { + for (int i = 0; i < nodes.size(); i++) { + tester.nodeRepository().write(nodes.get(i).with(IP.Config.EMPTY.with(Set.of("127.0.0." + i))), lock); + } + } + } + private static ClusterSpec clusterRequest(ClusterSpec.Type type, ClusterSpec.Id id) { return ClusterSpec.request(type, id, Version.fromString("6.42"), false); } diff --git a/parent/pom.xml b/parent/pom.xml index 1855553bc20..e2012214d89 100644 --- a/parent/pom.xml +++ b/parent/pom.xml @@ -515,6 +515,16 @@ <version>${curator.version}</version> </dependency> <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter-engine</artifactId> + <version>${junit.version}</version> + </dependency> + <dependency> + <groupId>org.junit.vintage</groupId> + <artifactId>junit-vintage-engine</artifactId> + <version>${junit.version}</version> + </dependency> + <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> @@ -765,7 +775,8 @@ <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <test.hide>true</test.hide> <doclint>all</doclint> - <surefire.version>2.21.0</surefire.version> + <surefire.version>2.22.0</surefire.version> + <junit.version>5.4.2</junit.version> <protobuf.version>3.7.0</protobuf.version> </properties> diff --git a/tenant-base/pom.xml b/tenant-base/pom.xml index b829089465f..10c42ba8acd 100644 --- a/tenant-base/pom.xml +++ b/tenant-base/pom.xml @@ -35,7 +35,8 @@ <vespaversion>${project.version}</vespaversion> <target_jdk_version>11</target_jdk_version> <compiler_plugin_version>3.8.0</compiler_plugin_version> - <surefire_version>2.22.0</surefire_version> <!-- NOTE bjorncs 15.06.2017: Version 2.20 has OoM issues --> + <surefire_version>2.22.0</surefire_version> + <junit.jupiter.version>5.4.2</junit.jupiter.version> <endpoint>https://api.vespa-external.aws.oath.cloud:4443</endpoint> </properties> @@ -72,6 +73,13 @@ <version>${vespaversion}</version> <scope>test</scope> </dependency> + + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter-engine</artifactId> + <version>${junit.jupiter.version}</version> + <scope>test</scope> + </dependency> </dependencies> <profiles> diff --git a/tenant-cd/src/main/java/ai/vespa/hosted/cd/ProductionTest.java b/tenant-cd/src/main/java/ai/vespa/hosted/cd/ProductionTest.java index 6cf5fb07f58..0dfeab5d327 100644 --- a/tenant-cd/src/main/java/ai/vespa/hosted/cd/ProductionTest.java +++ b/tenant-cd/src/main/java/ai/vespa/hosted/cd/ProductionTest.java @@ -16,6 +16,9 @@ package ai.vespa.hosted.cd; */ public interface ProductionTest { + /** Use with JUnit 5 @Tag to have this run in the production jobs in the pipeline. */ + String name = "ai.vespa.hosted.cd.ProductionTest"; + // Want to verify metrics (Vespa). // Want to verify external metrics (YAMAS, other). // May want to verify search gives expected results. diff --git a/tenant-cd/src/main/java/ai/vespa/hosted/cd/StagingTest.java b/tenant-cd/src/main/java/ai/vespa/hosted/cd/StagingTest.java index 40377da30ef..6e1487ced0f 100644 --- a/tenant-cd/src/main/java/ai/vespa/hosted/cd/StagingTest.java +++ b/tenant-cd/src/main/java/ai/vespa/hosted/cd/StagingTest.java @@ -17,6 +17,10 @@ package ai.vespa.hosted.cd; * @author jonmv */ public interface StagingTest { + + /** Use with JUnit 5 @Tag to have this run in the staging test job in the pipeline. */ + String name = "ai.vespa.hosted.cd.StagingTest"; + // Want to verify documents are not damaged by upgrade. // May want to verify metrics during upgrade. } diff --git a/tenant-cd/src/main/java/ai/vespa/hosted/cd/SystemTest.java b/tenant-cd/src/main/java/ai/vespa/hosted/cd/SystemTest.java index c67d86fc8de..f2f06d53515 100644 --- a/tenant-cd/src/main/java/ai/vespa/hosted/cd/SystemTest.java +++ b/tenant-cd/src/main/java/ai/vespa/hosted/cd/SystemTest.java @@ -15,6 +15,10 @@ package ai.vespa.hosted.cd; * @author jonmv */ public interface SystemTest { + + /** Use with JUnit 5 @Tag to have this run in the system test job in the pipeline. */ + String name = "ai.vespa.hosted.cd.SystemTest"; + // Want to feed some documents. // Want to verify document processing and routing is as expected. // Want to check recall on those documents. diff --git a/vespa-testrunner-components/src/main/java/com/yahoo/vespa/hosted/testrunner/PomXmlGenerator.java b/vespa-testrunner-components/src/main/java/com/yahoo/vespa/hosted/testrunner/PomXmlGenerator.java index 221ff2bc9a4..4ebabe63c1d 100644 --- a/vespa-testrunner-components/src/main/java/com/yahoo/vespa/hosted/testrunner/PomXmlGenerator.java +++ b/vespa-testrunner-components/src/main/java/com/yahoo/vespa/hosted/testrunner/PomXmlGenerator.java @@ -36,16 +36,22 @@ public class PomXmlGenerator { " <version>1.0.0</version>\n" + "\n" + " <properties>\n" + - " <maven_version>4.12</maven_version>\n" + + " <junit_version>5.4.2</junit_version>\n" + " <surefire_version>2.22.0</surefire_version>\n" + "%PROPERTIES%" + " </properties>\n" + "\n" + " <dependencies>\n" + " <dependency>\n" + - " <groupId>junit</groupId>\n" + - " <artifactId>junit</artifactId>\n" + - " <version>${maven_version}</version>\n" + + " <groupId>org.junit.vintage</groupId>\n" + + " <artifactId>junit-vintage-engine</artifactId>\n" + + " <version>${junit_version}</version>\n" + + " <scope>test</scope>\n" + + " </dependency>\n" + + " <dependency>\n" + + " <groupId>org.junit.jupiter</groupId>\n" + + " <artifactId>junit-jupiter-engine</artifactId>\n" + + " <version>${junit_version}</version>\n" + " <scope>test</scope>\n" + " </dependency>\n" + "%DEPENDENCIES%" + diff --git a/vespa-testrunner-components/src/test/resources/pom.xml_system_tests b/vespa-testrunner-components/src/test/resources/pom.xml_system_tests index 86c36afd636..263bd27a4f3 100644 --- a/vespa-testrunner-components/src/test/resources/pom.xml_system_tests +++ b/vespa-testrunner-components/src/test/resources/pom.xml_system_tests @@ -6,7 +6,7 @@ <version>1.0.0</version> <properties> - <maven_version>4.12</maven_version> + <junit_version>5.4.2</junit_version> <surefire_version>2.22.0</surefire_version> <my-comp.jar.path>components/my-comp.jar</my-comp.jar.path> <main.jar.path>main.jar</main.jar.path> @@ -14,9 +14,15 @@ <dependencies> <dependency> - <groupId>junit</groupId> - <artifactId>junit</artifactId> - <version>${maven_version}</version> + <groupId>org.junit.vintage</groupId> + <artifactId>junit-vintage-engine</artifactId> + <version>${junit_version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter-engine</artifactId> + <version>${junit_version}</version> <scope>test</scope> </dependency> <dependency> |